Tacview Wiki
Advertisement

Introduction[]

This document is an attempt to document formulas used in Tacview 1.x. Some of our professional and enthusiast home customers, sometime, want to know which formulas are used for very specific calculations. So, they can either reproduce it, or verify the accuracy of it.

For the sake of clarity, the formulas are presented here in a simplified C/C++ language and without error and edge cases handling.

If you need more information, or find missing items, or errors, send us an email with detailed explanation at support@tacview.net

System of Measurement[]

Any data input into Tacview is automatically converted to the metric system. Aside from some very old reference formulas (typically from Nasa), all calculations are made in metric system. The results of these calculations are then converted back into the user preferred system when displaying or exporting data.

Here is an example of the units used by Tacview:

  • Distance and coordinates: meters.
  • Speeds: meters per second.
  • Angles: radian.
  • Time: floating point Unix timestamp in seconds

Accuracy[]

Tacview 1 uses 64-bit floating point for most calculation to ensure accurate results (for example, longitude and latitude are calculated with 64-bit floating point numbers). When less accuracy is required and speed or memory are critical, Tacview 1 uses 32-bit floating point for calculations or storage (for example, angles and some altitudes are stored using 32-bit floating point numbers).

Mathematical Constants[]

Tacview 1 uses the following mathematical constants. It partially uses the WGS84 system. While it properly and accurately supports WGS84 coordinate when importing data via AEG/GAE and NMEA GPS files, for other mathematical calculations, Tacview 1 assumes the Earth is perfectly spherical and uses only the semi-major axis as a reference. This was for performances reasons at the time Tacview 1 has been created and gives accurate enough result for most cases.

// WGS84 semi-major axis (https://en.wikipedia.org/wiki/Geodetic_datum)

const float WGS84_EARTH_RADIUS = 6378137.0;

// g in m/s² (https://en.wikipedia.org/wiki/Standard_gravity)

const float EARTH_G = 9.80665;

Tacview coordinate system is the same as OpenGL. Here are the corresponding axe constants:

const vec3 ForwardVector(0.0, 0.0, -1.0);
const vec3 UpVector(0.0, 1.0, 0.0);
const vec3 RightVector(1.0, 0.0, 0.0);

Geodetic vs native coordinates[]

Tacview can playback data recorded from both real life and flight simulations.

Geodetic coordinates[]

In the case of real-life data, objects coordinate sources are either:

  • WGS84 cartesian coordinates (X, Y, Z relative to the center of Earth)
  • Geodetic coordinates (longitude, latitude, altitude)

Both are stored as geodetic coordinates (longitude, latitude, altitude) in radian and meters above sea level.

Some flight simulations are natively working with geodetic coordinates. This includes the following simulators:

  • Microsoft Flight Simulator
  • Prepar3D (which is a branch of Microsoft Flight Simulator X)
  • XPlane

Native “Flat” coordinates[]

Other flight simulators, for legacy performance reasons are working with cartesian coordinates. This is akin to a flat earth terrain.

In Tacview, these coordinates are named U, V, Altitude. The altitude is above sea level and identical to the geodetic altitude.

Most combat simulators are using flat coordinates:

  • DCS World
  • Falcon 4 / BMS
  • IL-2 Sturmovik

Geodetic or native?[]

Geodetic coordinates are mandatory. When they are not available, they are calculated depending on the map where the action is taking place. Tacview uses geodetic coordinates for display and general calculations. Whenever native coordinates are available, Tacview will use the in priority for calculations, to get identical reading as withing the source flight simulation. Otherwise, since geodetic coordinates are estimated in flat world simulations, there would give typically distorted readings for speed and everything involving distances.

To summarize:

  • When native coordinates are available, Tacview uses them in propriety to get accurate calculations.
  • Otherwise, geodetic coordinates will be used (which is perfectly fine for all other sources of data).

Geodetic coordinates in formulas[]

Geodetic coordinates are mainly used for 3D display. However, when the time comes to calculate data such as speed, range, etc., geodetic coordinates are first converted to Cartesian coordinates (X, Y, Z). For performance reasons, Tacview 1, does a simple spherical to Cartesian conversion using the Earth equatorial radius.

void LongitudeLatitudeToCartesian(float & x, float & y, float & z, float longitude, float latitude, float relativeAltitude)
{
	float SinLongitude;
	float CosLongitude;
	float SinLatitude;
	float CosLatitude;

	SinCos(longitude, SinLongitude, CosLongitude);
	SinCos(latitude, SinLatitude, CosLatitude);

	float absoluteAltitude = WGS84_EARTH_RADIUS + relativeAltitude;
	float altitudeCosLatitude = absoluteAltitude * CosLatitude;

	x = altitudeCosLatitude * CosLongitude;
	y = absoluteAltitude * SinLatitude;
	z = -altitudeCosLatitude * SinLongitude;
}

Linear algebra[]

While most of the calculations are done using basic operations, some of them use linear algebra. Linear algebra is the branch of math which uses vectors. It is simpler than it sounds, except maybe for the dot product which is less intuitive when you are not used to it.

Linear algebra is useful in our case, because in Tacview 1, the Earth is spherical like in real-life, and all objects are really placed around the world with all sorts of orientation and positions. For example, unlike in flat worlds simulations, the ground is not necessarily underneath, and even less at the coordinate z=0! With linear algebra tools, we can easily do all the necessary calculations without any hacks, and they will always work regardless of the orientation or position in space. I also find that some calculations are even simpler via linear algebra and require less operations to reach the same results as with regular arithmetic, which often requires some hacks to reach the same result.

Advanced telemetry[]

It is important to note that whenever any advanced telemetry is available, Tacview will use that data instead of calculating manually the corresponding variables. This is to make sure that the replay is as identical as possible to the original flight.

For example, if the Mach number has been recorded in the telemetry file, then Tacview will display it. Otherwise, it will attempt to estimate it based on a standard atmosphere.

Some of the advanced telemetry data will be interpolated to ensure smooth playback, while others are output as they were recorded. This document indicates which values are interpolated or not.

Distances[]

Range (distance between two objects)[]

The distance between two objects is displayed both at the top of the UI between the object selection boxes, and in the 3D view halfway between the two selected objects.

This range is calculated as a straight line between the two objects. This is the shortest 3D distance between the two objects, mainly used for combat situations. This is not the great-circle distance which could be calculated to navigate onto earth surface.

The distance between two objects is always positive.

float GetDistance(vec3 primaryObjectPos, vec3 secondaryObjectPos)
{
	float dx = primaryObjectPos.x - secondaryObjectPos.x;
	float dy = primaryObjectPos.y - secondaryObjectPos.y;
	float dz = primaryObjectPos.z - secondaryObjectPos.z;

	return sqrt(dx * dx + dy * dy + dz * dz);
}

Rate of Closure[]

The rate of closure is the speed at which two objects get closer to each other. It is positive when the objects are getting closer, negative when they are going away, and zero when are keeping a constant distance between each other.

To calculate it, Tacview calculates the speed at which the range between the two objects is changing.

float GetRateOfClosure(handle primaryObject, float currentTime)
{
	float timeInterval = 1.0;

	vec3 primaryObjectPos = GetPosition(primaryObject, currentTime);
	vec3 previousPimaryObjectPos = GetPosition(primaryObject, currentTime  timeInterval);

	vec3 secondaryObjectPos = GetPosition(secondaryObject, currentTime);
	vec3 previousSecondaryObjectPos = GetPosition(secondaryObject, currentTime  timeInterval);

	float currentRange = GetDistance(primaryObjectPos, secondaryObjectPos);
	float previousRange = GetDistance(previousPrimaryObjectPos, previousSecondaryObjectPos);

	float rateOfClosure = (PreviousRange - CurrentRange) / timeInterval;

	return rateOfClosure;
}

Speed and Acceleration[]

Speed[]

When available in telemetry, the speed is output as-this without interpolation. If no speed data has been explicitly recorded in the telemetry, Tacview calculates it (which is often the case for CAS, Mach number, …)

In Tacview 1, the base speed (True Air Speed) is calculated based on two positions. It is averaged over 1.0 second.

float GetSpeed(handle primaryObject, float currentTime)
{
	float timeInterval = 1.0;

	vec3 previousObjectPos = GetPosition(primaryObject, currentTime  timeInterval);
	vec3 currentObjectPos = GetPosition(primaryObject, currentTime);

	float distance = GetDistance(previousObjectPos, currentObjectPos);

	float speed = distance / timeInterval;

	return speed;
}

Calibrated Air Speed (CAS)[]

If the CAS has not been recorded in the telemetry data, then Tacview will first try to deduce it from the Mach number. If the Mach number is not available either, it will attempt to calculate the CAS based on the TAS.

The formula to calculate CAS from TAS has been taken from Falcon 4 source code. See the function CalcKIAS() from Simobj.cpp for more details. Tacview uses this function because it gives good estimates, especially for supersonic speed.

Float GetCASFromTAS(float TAS, float altitude)
{
	altitude = MetersToFeet(altitude);
	TAS = MetersToFeet(TAS);

	const float AASL = 1116.44;
	const float PASL = 2116.22;
	const float AASLK = 661.48;

	float ttheta, rsigma;
	float mach, vcas, pa;
	float qpasl1, qc;

	/*-----------------------------------------------*/
	/* calculate temperature ratio and density ratio */
	/*-----------------------------------------------*/

	if (altitude <= 36089.0)
	{
		ttheta = 1.0 - 0.000006875 * altitude;
		rsigma = Power(ttheta, 4.256);
	}
	else
	{
		ttheta = 0.7519;
		rsigma = 0.2971 * Power(2.718, 0.00004806 * (36089.0 - altitude));
	}

	mach = TAS / (Sqrt(ttheta) * AASL);
	pa = ttheta * rsigma * PASL;

	/*-------------------------------*/
	/* calculate calibrated airspeed */
	/*-------------------------------*/

	if (mach <= 1.0)
	{
		qc = (Power((1.0 + 0.2 * mach * mach), 3.5) - 1.0) * pa;
	}
	else
	{
		qc = ((166.9 * mach * mach) / (Power((7.0 - 1.0 / (mach * mach)), 2.5)) - 1.0) * pa;
	}

	qpasl1 = qc / PASL + 1.0;
	vcas = 1479.12 * Sqrt(pow(qpasl1, 0.285714) - 1.0);

	if (qc > 1889.64)
	{
		float oper = qpasl1 * Power((7.0 - AASLK * AASLK / (vcas * vcas)), 2.5);

		if (oper < 0.0)
		{
			oper = 0.1;
		}

		vcas = 51.1987 * Sqrt(oper);
	}

	vcas = KnotsToMeterPerSec(vcas);

	return vcas;
}

To calculate the CAS from the Mach number, Tacview uses a formula from https://aerotoolbox.com/airspeed-conversions/

float GetCASFromMach(float mach, float altitude)
{
	static float altitudeArray[] = { 0, 11000, 20000, 32000, 47000, 51000, 71000, 84852 };
	static float presRelsArray[] = { 1, 2.23361105092158e-1, 5.403295010784876e-2, 8.566678359291667e-3, 1.0945601337771144e-3, 6.606353132858367e-4, 3.904683373343926e-5, 3.6850095235747942e-6 };
	static float tempsArray[] = { 288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65,186.946 };
	static float tempGradArray[] = { -6.5, 0, 1, 2.8, 0, -2.8, -2, 0 };

	int i = 0;

	while (i <countof(altitudeArray) && altitude > altitudeArray[i + 1])
	{
		++i;
	}

	if (i >= countof(altitudeArray))
	{
		return 0.0;
	}

	// i defines the array position required for the calculations

	float alts = altitudeArray[i];
	float presRels = presRelsArray[i];
	float temps = tempsArray[i];
	float tempGrad = tempGradArray[i] / 1000.0;

	float deltaAlt = altitude - alts;
	float stdTemp = temps + (deltaAlt * tempGrad);	// this is the standard temperature at STP

	const float airMol = 28.9644;
	const float rGas = 8.31432; // kg / Mol / K
	const float g = 9.80665; // m / s2

	const float pSL = 101325; // Pa
	float gMR = g * airMol / rGas;

	float relPres;

	if (Abs(tempGrad) < 1e-10)
	{
		relPres = presRels * Exp(-1 * gMR * deltaAlt / 1000.0 / temps);
	}
	else
	{
		relPres = presRels * Power(temps / stdTemp, gMR / tempGrad / 1000.0);
	}

	float pressureSI = pSL * relPres;

	float qc = pressureSI * (Power(1.0 + 0.2 * mach * mach, 7.0 / 2.0) - 1.0);

	// Standard atmospheric values that remain constant

	const float P0 = 101325;
	const float a0 = 340.29;

	float cas = a0 * Sqrt(5.0 * (Power(qc / P0 + 1.0, 2.0 / 7.0) - 1.0));

	return cas;
}

Acceleration[]

The acceleration of an object is the rate of change of its speed. In short, Tacview calculates the speed before and after a given point in time, and then calculates the rate of change between these two speeds.

This variable is not displayed as such. But rather used for the calculation of G-forces for example.

float GetAcceleration(handle primaryObject, float currentTime)
{
	float timeInterval = 1.0;

	float currentSpeed = GetSpeed(primaryObject, currentTime);
	float nextSpeed = GetSpeed(primaryObject, currentTime + timeInterval);

	float acceleration = (nextSpeed  currentSpeed) / timeInterval;

	return acceleration;
}

More often, Tacview uses the object acceleration vector in 3D:

vec3 GetAccelerationVector(handle primaryObject, float currentTime)
{
	float timeInterval = 1.0;

	vec3 previousPosition = GetPosition(primaryObject, currentTime  timeInterval);
	vec3 currentPosition = GetPosition(primaryObject, currentTime);
	vec3 nextPosition = GetPosition(primaryObject, currentTime + timeInterval);

	vec3 previousVelocity = (currentPosition  previousPosition) / timeInterval;
	vec3 currentVelocity = (nextPosition  currentPosition) / timeInterval;

	vec3 accelerationVector = (currentVelocity  previousVelocity) / timeInterval;

	return accelerationVector;
}

G-forces[]

To calculate and display like in a cockpit, Tacview requires valid rotation information for the corresponding object. Otherwise, it will simply output the raw acceleration of the object in space. That would be perfectly fine for a rocket but will not match the typical g-force reading from a cockpit.

Tacview takes the current acceleration of the aircraft in 3D, combines it with the Earth gravity 3D vector, and projects it on the axis we want to calculate the displayed G-Force (longitudinal, lateral, vertical).

float GetGForce(handle primaryObject, float currentTime)
{
	vec3 currentPosition = GetPosition(primaryObject, currentTime);
	vec3 earthGravityVector = Normalize( -currentPosition ) * EARTH_G;
	vec3 accelerationVector = GetAccelerationVector(primaryObject, currentTime);
	vec3 totalAcceleration = accelerationVector  earthGravityVector;

	// we use a localReferenceAxis which depends on the g-force we are looking for:

	vec3 localReferenceAxis = UpVector;		// for vertical g-force
	vec3 localReferenceAxis = -RightVector;		// for lateral g-force
	vec3 localReferenceAxis = ForwardVector;	// for longitudinal g-force

	// Converts local reference axis into global vector
	// (applies plane roll, pitch, yaw then applies another
	// pitch & yaw depending on longitude & latitude to be tangent to earth surface)

	vec3 globalReferenceAxis = GetGlobalVector(localReferenceAxis);

	float projectedAcceleration = DotProduct(globalReferenceAxis, totalAcceleration);

	float displayedGForce = projectedAcceleration / EARTH_G;	// Normalize value in G

	return displayedGForce;
}

Angles[]

Relative Bearings[]

Tacview can display two kinds of relative bearing. The one named “navigation” is a 2D bearing useful for general navigation. The other one named “combat” is calculated in 3D and can be useful for Beyond Visual Range and Within Range combat. For example, to know if a target is within the sight of a missile sensor.

Navigation bearing ignores the altitude objects, it uses proper trigonometry to calculate the bearing from one object to another on a spherical earth. If the source data provides native flat-world coordinates, when Tacview will use vector calculations in the flat world to determinate the bearing.

float CalculateBearing(vec3 primaryGeodeticPos, vec3 secondaryGeodeticPos)
{
    float lon1 = primaryGeodeticPos.x;
    float lat1 = primaryGeodeticPos.y;
    float lon2 = secondaryGeodeticPos.x;
    float lat2 = secondaryGeodeticPos.y;

    float dLon = lon2 - lon1;

    float x = sin(dLon) * cos(lat2);
    float y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);

    float bearing = atan(y, x);

    return bearing;
}

To retrieve the final relative bearing, Tacview simply subtract the absolute bearing to the primary object current heading:

float GetNavigationRelativeBearing(handle primaryObject, handle seconadryObject, float currentTime)
{
	vec3 primaryObjectGeodeticPosition = GetGeodeticPosition(primaryObject, currentTime);
	vec3 secondaryObjectGeodeticPosition = GetGeodeticPosition(secondaryObject, currentTime);

	float absoluteBearingNativation = CalculateBearing(primaryObjectGeodeticPosition, secondaryObjectGeodeticPosition);

	float primaryObjectHeading = GetHeading(primaryObject);

	float navigationRelativeBearing = SubstractAngles(absoluteBearingNativation, primaryObjectHeading);

	return navigationRelativeBearing;
}

The combat relative bearing is the delta angle between the primary object 3D forward vector and the vector from the primary object to the secondary object:

float GetCombatRelativeBearing(handle primaryObject, float currentTime)
{
	vec3 primaryObjectPos = GetPosition(primaryObject, currentTime);
	vec3 secondaryObjectPos = GetPosition(secondaryObject, currentTime);

	vec3 vectorToTarget = Normalize(secondaryObjectPos  primaryObjectPos);
	vec3 primaryObjectForwardVector = GetForwardVector(primaryObject, currentTime);

	float combatRelativeBearing = ArcCos(DotProduct(vectorToTarget, primaryObjectForwardVector));

	return combatRelativeBearing;
}

Line of Sight Rate[]

The line-of-sight rate is the speed at which the bearing from one primary object to another secondary object varies.

float GetLineOfSightRate(handle primaryObject, handle secondaryObject, float currentTime)
{
	float timeInterval = 1.0;

	vec3 primaryObjectPrevPos = GetPosition(primaryObject, currentTime - timeInterval);
	vec3 secondaryObjectPrevPos = GetPosition(secondaryObject, currentTime - timeInterval);

	vec3 primaryObjectPos = GetPosition(primaryObject, currentTime);
	vec3 secondaryObjectPos = GetPosition(secondaryObject, currentTime);

	float previousBearing = CalculateBearing(primaryObjectPrevPos, secondaryObjectPrevPos);
	float currentBearing = CalculateBearing(primaryObjectPos, secondaryObjectPos);

	float bearingDelta = SubstractAngles(currentBearing, previousBearing);

	float lineOfSightRate = bearingDelta / timeInterval;

	return lineOfSightRate;
}

Aspect Angle[]

The Aspect Angle (AA – US Air Force) is the angle from the bearing line of the fighter to the tail of the target. It is also measured in direction right or left of the target's flight path. 0 degrees means in the tail, 90R degrees means primary object is in the right wing of the target object.

float GetAspectAngle(handle primaryObject, handle secondaryObject, float currentTime)
{
	vec3 primaryObjectPos = GetPosition(primaryObject, currentTime);
	vec3 targetObjectPos = GetPosition(targetObject, currentTime);

	vec3 primaryObjectToTargetVector = primaryObjectPos  targetObjectPos;

	vec3 targetForwardVector = GetForwardVector2D(targetObject, currentTime);
	vec3 targetRightVector = GetRightVector2D(targetObject, currentTime);

	float forwardProjection = DotProduct(targetForwardVector, primaryObjectToTargetVector);
	float rightProjection = DotProduct(targetRightVector, primaryObjectToTargetVector);

	float aspectAngle = AddAngles(Pi / 2, ArcTan(forwardProjection, rightProjection));

	return aspectAngle;
}

AoA[]

The angle of attack (α) is the angle between the longitudinal axis of the plane and its vector of motion taking the plane orientation into consideration. Tacview needs valid roll, pitch, yaw information to calculate the AoA as typically displayed in the plane cockpit. Otherwise, it will return the angle without taking into consideration the plane orientation.

float GetAngleOfAttack(handle primaryObject, float currentTime)
{
	float timeInterval = 1.0;

	float previousTime = currentTime  timeInterval;
	float nextTime = currentTime + timeInterval;

	vec3 previousPosition = GetPosition(primaryObject, previousTime);
	vec3 currentPosition = GetPosition(primaryObject, currentTime);
	vec3 nextPosition = GetPosition(primaryObject, nextTime);

	vec3 currentCourse = Normalize(nextPosition  previousPosition);

	vec3 aircraftVerticalAxis = GetUpVector(primaryObject, currentTime);
	vec3 aircraftLongitudinalAxis = GetForwardVector(primaryObject, currentTime);

	float courseProjectedOnVertical = DotProduct(aircraftVerticalAxis, currentCourse);
	float courseProjectedOnLongitudinal = DotProduct(aircraftLongitudinalAxis, currentCourse);

	float aoa = ArcTan(courseProjectedOnVertical, courseProjectedOnLongitudinal);

	return aoa;
}
Advertisement