Height conversion for Cesium

Hello! I have a CSV from FlightRadar with the altitudes. The altitude at ground is defined as 0, which I think is mean sea level (MSL). Now I need to convert that to meters on the ellipsoid (which leads to an increased altitude), but for some reason the coordinates at ground level are under the ground in Cesium.

What am I missing here? What do I need to handle as well in order for Cesium to show these coordinates on the ground of the world map? I do all my preprocessing in Python, so if someone has an understanding of what I need to do I can put that in my Python script.

@FastFishy96 The “height” in Cesium for Unreal is “ellipsoidal height.” You can read more about that here. To convert between the two, there’s a few options:

  • You can directly sample the height from the tileset using SampleHeightMostDetailed. This will load the highest level-of-detail for the terrain tiles under each point you pass in and obtains the exact height of each point on the terrain. This is suitable for small numbers of points and/or points very close together, but a lot of points or a number of points very far apart might result in an unacceptable delay while it loads all of the tiles needed to perform the sampling operation.
  • You can use the EGM96 grid using the EarthGravitationalModel1996Grid class. You’ll need to use this with a copy of WW15MGH.DAC, which you can grab from here. EGM96 is a grid of values describing the difference between the ellipsoidal height and the orthometric height (aka Mean Sea Level) at a given longitude and latitude value. So, for a Longitude, Latitude, Height value of x, you would place it at Cartographic(x.longitude, x.latitude, egm96Grid.sampleHeight(x) + x.height).

I tried the EGM96 grid, but it seems I’m still missing something. When looking at Schiphol Airport for example, I determine the height of the terrain with respect of the ellipsiod. This turns out to be 43.0375 meters. However, when using that in the CSV file, the points are still under de ground for some reason. With around 55 meters they rise above the ground.

Is there something else I’m missing here?

Hi @FastFishy96,

EGM96 won’t give you the height of terrain. It provides the height of Mean Sea Level relative to the WGS84 ellipsoid. The actual terrain surface will likely be higher than that.

If you need a terrain height, you’ll have to use SampleHeightMostDetailed.

Hello. Is there a way to handle this during the pre-processing in Python? Or how can I handle this in Unreal Engine itself? I’m loading in a CSV with all the altitudes.

Hello. It’s very difficult in Python to do, so I will have to do it in Unreal itself. Do you happen to have an example of how to implement it in the C++ code? I think it should be in the code of the PlaneTrack of the tutoirial ( Build a Flight Tracker with Cesium for Unreal – Cesium ).

The input in altitude I give is in radar altitude, hence altitude above ground. I need to add the height of the terrain at each coordinate to the altitude given as input.

Hi @FastFishy96,

You can find some sample code for querying heights from C++ in our tests:

Hello. I tried it, but I cannot figure it out. In the .cpp-file I add a piece of code that should handle it:

But this will not work without the use of:

But this only works with the include: #include “CesiumRuntime/Public/Cesium3DTileset.h”. However, the moment I do that both the BeginPlay and Tick show errors:

image

I have no idea how to fix this. I set the includes Cesium3DTileset.h and CesiumGeoreference.h to the .cpp-file and used class in the header-file, but that also did not work.

Do you have any idea how to solve this?

You need to add CesiumRuntime to PublicDependencyModuleNamesin your project’s .build.cs file. Then just:

#include "Cesium3DTileset.h"

Also, those errors look like Intellisense errors, not real compiler errors. Always be sure you’re compling and looking at the Output log, because Intellisense errors are sometimes misleading.

I had to be clearer in the issue, my apologies. The build does not succeed at all.

First I build everything without the changes, that works:

image

Now I make the adjustments. First I have the CesiumRuntime I need to add:

image

Then, I add the code to the header-file:

Including of course the property:

image

Then I add the code needed in the .cpp-file:

Building this leads to a fail:

image

The three files I adjusted don’t show errors and the error list is not helping:

The log files show the same thing:

Is there something else I can do here?

Update: the moment I add the Cesium3DTileset include, the errors occur.

image

Now I simply added the include only, no further code.

I found the issue after a while: the C++ version must be set to 20 (default is 17). That should solve the compiling issues:

image

So now I have the code working the CPP-file:

However, the spline is still showing values below the surface with an altitude of 0, while the points should be on the ground in that case. Do you happen to know if I did something wrong here?

Hey @FastFishy96,

Your code is doing:

FVector(Results[0].LongitudeLatitudeHeight.X, Results[1].LongitudeLatitudeHeight.Y, Results[2].LongitudeLatitudeHeight.Z))

That means you’ll be sampling from three different height sample results, instead of just Results[0]. Is that what you want?

Hello!

Ah yes I see it, I just fixed it to Results[0] for all three and compiled it again:

This does not solve the issue however.

Hi @FastFishy96,

Sorry we haven’t had the chance to look at this sooner. I’m seeing some strange behavior on my side, so it’s possible there is a bug in the plugin, but I’m not 100% positive. I’ll keep investigating this when I can and update if I have a working solution.

Hey @FastFishy96,

I have something working on my end where the spline can snap to the ground.

In PlaneTrack.h I made a function called SnapToTerrain and added the CesiumTileset parameter that you had in your script:

	UFUNCTION(BlueprintCallable, Category = "FlightTracker")
	void SnapToTerrain();

Then I implemented it in PlaneTrack.cpp like so:



void APlaneTrack::SnapToTerrain() {
	if (this->CesiumTileset) {
		TArray<FVector> SamplePoints;
		for (auto& row : this->AircraftsRawDataTable->GetRowMap())
		{
			FAircraftRawData* Point = (FAircraftRawData*)row.Value;
			// Get row data point in lat/long/alt and transform it into Unreal points 
			double PointLatitude = Point->Latitude;
			double PointLongitude = Point->Longitude;
			double PointHeight = Point->Height;
			SamplePoints.Add({ PointLongitude, PointLatitude, PointHeight });
		}

		this->CesiumTileset->SampleHeightMostDetailed(SamplePoints, FCesiumSampleHeightMostDetailedCallback::CreateLambda(
			[this](ACesium3DTileset* Tileset, const TArray<FCesiumSampleHeightResult>& Results, const TArray<FString>& Warnings) {
				this->SplineTrack->ClearSplinePoints();

				for (int32 PointIndex = 0; PointIndex < Results.Num(); PointIndex++) {
					FVector Result = Results[PointIndex].LongitudeLatitudeHeight;
					// Compute the position in UE coordinates 
					FVector SplinePointPosition = this->CesiumGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal(Result);
					this->SplineTrack->AddSplinePointAtIndex(SplinePointPosition, PointIndex, ESplineCoordinateSpace::World, false);

					// Get the up vector at the position to orient the aircraft 
					const CesiumGeospatial::Ellipsoid& Ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
					glm::dvec3 upVector = Ellipsoid.geodeticSurfaceNormal(CesiumGeospatial::Cartographic(FMath::DegreesToRadians(Result.X), FMath::DegreesToRadians(Result.Y), FMath::DegreesToRadians(Result.Z)));

					// Compute the up vector at each point to correctly orient the plane 
					FVector4 ecefUp(upVector.x, upVector.y, upVector.z, 0.0);
					FMatrix ecefToUnreal = this->CesiumGeoreference->ComputeEarthCenteredEarthFixedToUnrealTransformation();
					FVector4 unrealUp = ecefToUnreal.TransformFVector4(ecefUp);
					this->SplineTrack->SetUpVectorAtSplinePoint(PointIndex, FVector(unrealUp), ESplineCoordinateSpace::World, false);
				}

				this->SplineTrack->UpdateSpline();
			}));
	}
}

At first it looked like it was making the spline disappear, but that’s because the spline was literally right on the terrain surface, so you couldn’t always see the points. Here’s how it looks when I hide Cesium World Terrain at runtime

Before:

After:

Hello @janine ,

Thanks for the explanation and code. You won’t believe it, but it still does not work on my end for some reason. The code does compile, but literally nothing has changed. For the purpose of completeness, I will share my entire .h and .cpp code, just to be sure (with your code included of course). Be aware that I made some changes to the naming (PlaneTrack = RouteTrack for example). I have some other additions to the code, which should not affect this part of the code but I’m showing it here just to be sure.

RouteTrack.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SplineComponent.h"
#include "CesiumRuntime/Public/Cesium3DTileset.h"
#include "CesiumRuntime/Public/CesiumGeoreference.h"
#include "Engine/DataTable.h"
#include <glm/vec3.hpp>
#include "CesiumGeospatial/Ellipsoid.h"
#include "CesiumGeospatial/Cartographic.h"
#include "RouteTrack.generated.h"

USTRUCT(BlueprintType)
struct FAircraftRawData : public FTableRowBase
{
	GENERATED_USTRUCT_BODY()

public:
	FAircraftRawData()
		: TimestampRaw("")
		, Latitude(0.0)
		, Longitude(0.0)
		, Altitude(0.0)
		, Velocity(0)
		, Heading(0)
		, Pitch(0)
		, Roll(0)
	{
	}

	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	FString TimestampRaw;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	double Latitude;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	double Longitude;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	double Altitude;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	int Velocity;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	int Heading;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	int Pitch;
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	int Roll;
};

UCLASS()
class FLIGHTTRACKER_API ARouteTrack : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARouteTrack();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

public:
	// Spline variable to represent the plane track
	UPROPERTY(BlueprintReadOnly, Category = "FlightTracker")
	USplineComponent* SplineTrack;

	// Cesium class that contain many useful  coordinate conversion functions
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	ACesiumGeoreference* CesiumGeoreference;

	// An Unreal Engine data table to store the raw flight data
	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	UDataTable* AircraftRawDataTable;

	UPROPERTY(EditAnywhere, Category = "FlightTracker")
	ACesium3DTileset* CesiumTileset;

	UFUNCTION(BlueprintCallable, Category = "FlightTracker")
	void SnapToTerrain();


public:
	// Function to parse the data table and create the spline track
	UFUNCTION(BlueprintCallable, Category = "FlightTracker")
	void LoadSplineTrackPoints();

	UFUNCTION(BlueprintCallable, Category = "FlightTracker")
	void GetTimestampAndRotByIndex(int32 Index, FString& OutTimestamp, float& OutHeading, float& OutAltitude, float& OutVelocity, float& OutPitch, float& OutRoll);

	UFUNCTION(BlueprintCallable, Category = "FlightTracker")
	void GetDataAtSliderPosition(float SliderValue, FString& OutTimestamp, FVector& OutPosition, float& OutHeading, float& OutVelocity, float& OutPitch, float& OutRoll);

	UPROPERTY(BlueprintReadOnly, Category = "Flight Data")
	int32 TotalRowCount = 0;
};

RouteTrack.cpp (Height = Altitude, also in the CSV).

// Fill out your copyright notice in the Description page of Project Settings.


#include "RouteTrack.h"

// Sets default values
ARouteTrack::ARouteTrack()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	// Initialize the track
	SplineTrack = CreateDefaultSubobject<USplineComponent>(TEXT("SplineTrack"));
	// This lets us visualize the spline in Play mode
	SplineTrack->SetDrawDebug(true);
	// Set the color of the spline
	SplineTrack->SetUnselectedSplineSegmentColor(FLinearColor(1.f, 0.f, 0.f));

}

// Called when the game starts or when spawned
void ARouteTrack::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ARouteTrack::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
    // Update the slider automatically

}

void ARouteTrack::LoadSplineTrackPoints()
{
    if (this->AircraftRawDataTable != nullptr && this->CesiumGeoreference != nullptr)
    {
        TotalRowCount = AircraftRawDataTable->GetRowMap().Num();

        int32 PointIndex = 0;
        for (auto& row : this->AircraftRawDataTable->GetRowMap())
        {
            FAircraftRawData* Point = (FAircraftRawData*)row.Value;
            // Get row data point in lat/long/alt and transform it into UE4 points
            double PointLatitude = Point->Latitude;
            double PointLongitude = Point->Longitude;
            double PointAltitude = Point->Altitude;

            // Compute the position in UE coordinates
            FVector UECoords = this->CesiumGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal(FVector(PointLongitude, PointLatitude, PointAltitude));
            FVector SplinePointPosition = FVector(UECoords.X, UECoords.Y, UECoords.Z);
            this->SplineTrack->AddSplinePointAtIndex(SplinePointPosition, PointIndex, ESplineCoordinateSpace::World, false);

            // Get the up vector at the position to orient the aircraft
            const CesiumGeospatial::Ellipsoid& Ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
            glm::dvec3 upVector = Ellipsoid.geodeticSurfaceNormal(CesiumGeospatial::Cartographic(FMath::DegreesToRadians(PointLongitude), FMath::DegreesToRadians(PointLatitude), FMath::DegreesToRadians(PointAltitude)));

            // Compute the up vector at each point to correctly orient the plane
            glm::dvec4 ecefUp(upVector, 0.0);
            const GeoTransforms& geoTransforms = this->CesiumGeoreference->GetGeoTransforms();
            const glm::dmat4& ecefToUnreal = geoTransforms.GetEllipsoidCenteredToAbsoluteUnrealWorldTransform();
            glm::dvec4 unrealUp = ecefToUnreal * ecefUp;
            this->SplineTrack->SetUpVectorAtSplinePoint(PointIndex, FVector(unrealUp.x, unrealUp.y, unrealUp.z), ESplineCoordinateSpace::World, false);

            PointIndex++;
        }
        this->SplineTrack->UpdateSpline();
    }
}

void ARouteTrack::SnapToTerrain() {
    if (this->CesiumTileset) {
        TArray<FVector> SamplePoints;
        for (auto& row : this->AircraftRawDataTable->GetRowMap())
        {
            FAircraftRawData* Point = (FAircraftRawData*)row.Value;
            // Get row data point in lat/long/alt and transform it into Unreal points 
            double PointLatitude = Point->Latitude;
            double PointLongitude = Point->Longitude;
            double PointHeight = Point->Altitude;
            SamplePoints.Add({ PointLongitude, PointLatitude, PointHeight });
        }

        this->CesiumTileset->SampleHeightMostDetailed(SamplePoints, FCesiumSampleHeightMostDetailedCallback::CreateLambda(
            [this](ACesium3DTileset* Tileset, const TArray<FCesiumSampleHeightResult>& Results, const TArray<FString>& Warnings) {
                this->SplineTrack->ClearSplinePoints();

                for (int32 PointIndex = 0; PointIndex < Results.Num(); PointIndex++) {
                    FVector Result = Results[PointIndex].LongitudeLatitudeHeight;
                    // Compute the position in UE coordinates 
                    FVector SplinePointPosition = this->CesiumGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal(Result);
                    this->SplineTrack->AddSplinePointAtIndex(SplinePointPosition, PointIndex, ESplineCoordinateSpace::World, false);

                    // Get the up vector at the position to orient the aircraft 
                    const CesiumGeospatial::Ellipsoid& Ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
                    glm::dvec3 upVector = Ellipsoid.geodeticSurfaceNormal(CesiumGeospatial::Cartographic(FMath::DegreesToRadians(Result.X), FMath::DegreesToRadians(Result.Y), FMath::DegreesToRadians(Result.Z)));

                    // Compute the up vector at each point to correctly orient the plane 
                    FVector4 ecefUp(upVector.x, upVector.y, upVector.z, 0.0);
                    FMatrix ecefToUnreal = this->CesiumGeoreference->ComputeEarthCenteredEarthFixedToUnrealTransformation();
                    FVector4 unrealUp = ecefToUnreal.TransformFVector4(ecefUp);
                    this->SplineTrack->SetUpVectorAtSplinePoint(PointIndex, FVector(unrealUp), ESplineCoordinateSpace::World, false);
                }

                this->SplineTrack->UpdateSpline();
            }));
    }
}

void ARouteTrack::GetTimestampAndRotByIndex(int32 Index, FString& OutTimestamp, float& OutHeading, float& OutAltitude, float& OutVelocity, float& OutPitch, float& OutRoll)
{
    OutTimestamp = TEXT("");
    OutHeading = 0;
    OutAltitude = 0;
    OutPitch = 0;
    OutRoll = 0;

    if (!this->AircraftRawDataTable) return;

    const TMap<FName, uint8*>& RowMap = this->AircraftRawDataTable->GetRowMap();
    if (Index < 0 || Index >= RowMap.Num()) return;

    int32 CurrentIndex = 0;
    for (const TPair<FName, uint8*>& Row : RowMap)
    {
        if (CurrentIndex == Index)
        {
            FAircraftRawData* RowData = (FAircraftRawData*)Row.Value;
            OutTimestamp = RowData->TimestampRaw;
            OutHeading = RowData->Heading;
            OutAltitude = RowData->Altitude;
            OutVelocity = RowData->Velocity;
            OutPitch = RowData->Pitch;
            OutRoll = RowData->Roll;
            return;
        }
       
        ++CurrentIndex;

        
        // <-- Slider-controlled increment
    }
}
void ARouteTrack::GetDataAtSliderPosition(float SliderValue, FString& OutTimestamp, FVector& OutPosition, float& OutHeading, float& OutVelocity, float& OutPitch, float& OutRoll)
{
    OutTimestamp = TEXT("");
    OutHeading = 0.f;
    OutVelocity = 0.f;
    OutPitch = 0.f;
    OutRoll = 0.f;
    OutPosition = FVector::ZeroVector;

    if (!AircraftRawDataTable || SliderValue < 0.f || SliderValue > 1.f) return;

    // Get row map and total number of rows
    const TMap<FName, uint8*>& RowMap = AircraftRawDataTable->GetRowMap();
    int32 NumRows = RowMap.Num();
    if (NumRows == 0) return;

    // Clamp slider just in case
    SliderValue = FMath::Clamp(SliderValue, 0.f, 1.f);

    // Map slider to an index
    float FloatIndex = SliderValue * (NumRows - 1);
    int32 IndexA = FMath::FloorToInt(FloatIndex);
    int32 IndexB = FMath::Min(IndexA + 1, NumRows - 1);
    float Alpha = FloatIndex - IndexA; // interpolation factor

    // Fetch data from both rows
    TArray<FAircraftRawData*> SortedRows;
    SortedRows.Reserve(NumRows);
    for (const TPair<FName, uint8*>& Row : RowMap)
    {
        SortedRows.Add((FAircraftRawData*)Row.Value);
    }

    // DataTable rows are not guaranteed to be in CSV order unless sorted
    // Optional: sort by timestamp if necessary
    SortedRows.Sort([](const FAircraftRawData& A, const FAircraftRawData& B) {
        return A.TimestampRaw < B.TimestampRaw;
        });

    FAircraftRawData* DataA = SortedRows[IndexA];
    FAircraftRawData* DataB = SortedRows[IndexB];

    // Interpolate lat/lon/alt to Unreal position
    FVector PosA = CesiumGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal(FVector(DataA->Longitude, DataA->Latitude, DataA->Altitude));
    FVector PosB = CesiumGeoreference->TransformLongitudeLatitudeHeightPositionToUnreal(FVector(DataB->Longitude, DataB->Latitude, DataB->Altitude));
    OutPosition = FMath::Lerp(PosA, PosB, Alpha);

    // Interpolate other values
    OutHeading = FMath::Lerp(DataA->Heading, DataB->Heading, Alpha);
    OutVelocity = FMath::Lerp(DataA->Velocity, DataB->Velocity, Alpha);
    OutPitch = FMath::Lerp(DataA->Pitch, DataB->Pitch, Alpha);
    OutRoll = FMath::Lerp(DataA->Roll, DataB->Roll, Alpha);

    // Output timestamp (you can interpolate if needed, but strings are usually discrete)
    OutTimestamp = (Alpha < 0.5f) ? DataA->TimestampRaw : DataB->TimestampRaw;
}

.buid.cs

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class FlightTracker : ModuleRules
{
	public FlightTracker(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "CesiumRuntime" });

        // Tell Unreal Engine to use C++17
        CppStandard = CppStandardVersion.Cpp20;

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

The result in Unreal Engine:

Am I missing something here?

Hi @FastFishy96,

That’s really strange :confused: Everything you shared makes sense to me, so I have no idea what could be going wrong. Maybe try these things?

  • Uninstalling and reinstalling the plugin
  • Create a new sample project to test the script
  • Adding UE_LOGs to print the success of each point, just to double-check that the samples are actually going through.

If none of that helps, I could try to reproduce the behavior from the same data (if you’re willing to share)?

Hello @janine,

I tried all those things, but nothing seem to happen. I have multiple UE_LOG’s inside the function but they’re not triggered for some reason.

I can share my project with you, but then I prefer to send it to you privately. If you find out what is wrong, then we can share the findings here. Is that okay?

@FastFishy96 if you’re not seeing log statements that you have added to code that you’re sure is executing, that points to some kind of build issue. Are you sure your code is compiling correctly, and that you’re running the copy that you’re editing?

Another thing to check… You might also want to use Warning, i.e., UE_LOG(LogTemp, Warning, TEXT("Whatever")) to make sure your message isn’t being suppressed from the log because it is low priority. And of course make sure your Output window is configured to show Warnings!