Elevation Contours & Elevation Ramp in Unity

I’m trying to replicate the elevation contour lines and elevation ramp materials from the Bathymetry demo here: https://sandcastle.cesium.com/?src=Bathymetry.html in Unity. Both of these materials require the per-vertex height (in meters above or below the WGS84 ellipsoid).

My question is: Is there a way to get the per-vertex height in a Unity shader (graph)? Perhaps there is a way to encode the height in one of the unused vertex attributes? (Or maybe it’s already there, I just don’t know where to look).

Hi @jpvanoosten,

I’m not sure there is a good way to compute per-vertex ellipsoid height in a Unity shader. Although we can do it on the CPU, it require a lot of math that is probably too computationally expensive for the GPU.

But, your idea of encoding the height in an unused vertex attribute sounds promising. We don’t do that already, but you could do something like so:

  • You can intercept each tile when it loads using the Cesium3DTileset.OnTileGameObjectCreated event.
  • When a tile loads, get the mesh renderers in its primitives. (Primitives will be children game objects of the tile).
  • For each mesh renderer, iterate through all of the points and convert them to Longitude, Latitude, Height coordinates. You can use CesiumGeoreference.TransformUnityPositionToEarthCenteredEarthFixed and CesiumWgs84Ellipsoid.EarthCenteredEarthFixedToLongitudeLatitudeHeight to do so.
  • Set the height value in one of the unused vertex attributes of the mesh. (Probably easiest to choose one of the texcoord attributes.)

If you decide to try this, let us know how that goes!

Thanks @janine! I’m going to try this today and I’ll let you know how it goes.

1 Like

So I’ve created a Unity component to compute the height values for a terrain tile using the following code:

public class AddHeightTiles : MonoBehaviour
{
    public Cesium3DTileset Tileset;
    public CesiumGeoreference Georeference;

    public void Awake() {
        if (Tileset) {
            Tileset.OnTileGameObjectCreated += Tileset_OnTileGameObjectCreated;
        }
    }

    private void Tileset_OnTileGameObjectCreated(GameObject go) {
        foreach (var meshFilter in go.GetComponentsInChildren<MeshFilter>()) {
            var mesh = meshFilter.sharedMesh;
            var meshDataArray = Mesh.AcquireReadOnlyMeshData(mesh);
            var meshData = meshDataArray[0];

            // Allocate storage for vertex and height data.
            var verts = new NativeArray<Vector3>(meshData.vertexCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
            // The resulting longitude, latitude, and height values.
            var lonLatHeight = new NativeArray<Vector3>(meshData.vertexCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);

            // Get vertex positions. 
            meshData.GetVertices(verts);

            for (int i = 0; i < verts.Length; i++) {
                var v3 = verts[i];
                var d3 = double3(v3);
                // Convert vertex to ECEF.
                d3 = Georeference.TransformUnityPositionToEarthCenteredEarthFixed(d3);
                // Convert to Longitude, Latitude, and Height.
                d3 = CesiumWgs84Ellipsoid.EarthCenteredEarthFixedToLongitudeLatitudeHeight(d3);
                // Store Lon, Lat, Height values.
                lonLatHeight[i] = new Vector3((float)d3.x, (float)d3.y, (float)d3.z);
            }

            meshDataArray.Dispose();

            // Assign the LonLatHeight data to the mesh's UV3 texture coords.
            mesh.SetUVs(3, lonLatHeight);

            lonLatHeight.Dispose();
            verts.Dispose();
        }
    }
}

Note: I’m using AcquireReadOnlyMeshData because I was using a IJobParallelFor on the vertices of the mesh, but for debugging, I switched to the for loop (for now)

I feel like this should work, but I’m not seeing the correct values in the output.

Without going into too many technical details, I’ve created a shader that reads the UV3 coordinate, extracts the height component, normalizes (using the same min/max values as in the Bathymetry demo linked in the OP), then uses the normalized hight to sample the elevation ramp. The result is the image below:

The stripes on the globe are the elevation height ramp texture, but clearly, the UV’s are not correct…

Note: I also tried to visualize the normalized longitude, and latitude values from the UV3 texture coordinates, but they are also not correct.

Is it guaranteed that the vertices of the mesh coming from the OnTileGameObjectCreated event are in Unity coordinates?

And also, (not sure it’s related), but I also get an error when I assign a custom material to the 3D Tileset:

Destroying assets is not permitted to avoid data loss.
If you really want to remove an asset use DestroyImmediate (theObject, true);
UnityEngine.Object:Destroy (UnityEngine.Object)
Reinterop.ReinteropInitializer:UnityEngine_Object_CallDestroy_x7aQMuJcEpatC9689ghI4A (intptr) (at ./Packages/com.cesium.unity/Runtime/generated/Reinterop/Reinterop.RoslynSourceGenerator/ReinteropInitializer.cs:39144)
CesiumForUnity.Cesium3DTileset:OnDisable () (at ./Packages/com.cesium.unity/Runtime/generated/Reinterop/Reinterop.RoslynSourceGenerator/Cesium3DTileset-generated.cs:582)

I suppose this happens because it’s trying to destroy the custom material that is actually linked to a project asset (and not a temporary object created if no material is assigned to the tileset).

If I leave the OpaqueMaterial property on the tileset to None, the error does not occur, but obviously, I want to use the custom shader.

I updated my (local) packages to v1.8.0, and this seems to have fixed this issue… I’m not getting the error when using custom materials. So please ignore this one.

But I still have the issue with the height values not being correct (as far as I can tell). So still investigating that one…

I’ve been looking closer at this issue, and it seems that all the values are strange.

For example, the vertices coming from the loaded tile are (very) large numbers. These are the first 10 vertices of some random tile coming in from the Unity mesh:

x y z
693706.30 1033791.00 -6232509.00
1436441.00 591030.30 -6164443.00
409.85 -3651.96 -6355296.00
439.89 623047.60 -6324991.00
1248052.00 -3651.96 -6231827.00
409.85 -3651.96 -6355296.00
1586733.00 2134956.00 -5774282.00
1965603.00 1046561.00 -5954476.00
659585.70 1587470.00 -6120762.00
2896741.00 786249.20 -5607043.00
2400065.00 473444.90 -5867085.00

And these are the resulting longitude, latitude, and height values computed from these vertices:

Longitude Latitude Height
14.88 23.28 3332429.00
19.86 21.63 3038246.00
10.42 18.47 2625713.00
10.42 21.30 3059041.00
18.84 18.84 2625757.00
10.42 18.47 2625713.00
20.44 28.90 4031120.00
23.22 24.13 3341595.00
14.59 25.81 3690489.00
29.82 24.08 3170757.00
26.51 22.01 2957681.00

For reference, this is the function I’m using to convert the vertices of the mesh to long/lat/height:

for (int i = 0; i < verts.Length; i++)
{
    var d3 = double3(verts[i]);
    // Convert vertex to ECEF.
    d3 = Georeference.TransformUnityPositionToEarthCenteredEarthFixed(d3);
    // Convert to Longitude, Latitude, and Height.
    d3 = CesiumWgs84Ellipsoid.EarthCenteredEarthFixedToLongitudeLatitudeHeight(d3);
    // Store Lon, Lat, Height values.
    lonLatHeight[i] = new Vector3((float)d3.x, (float)d3.y, (float)d3.z);
}

I believe that the Longitude and Latitude values could be correct (despite my current view being centered around (10.4, 63.4, 547)), the height is clearly not (if this is indeed supposed to be the height (in meters) above (or below) the WGS84 ellipsoid).

But if I try to visualize the longitude (normalized the range [-180 … 180] and output in the red channel) and latitude (normalized in the range [-90 … 90] and output in the green channel), and zoom out a bit, it looks like this:


Which clearly doesn’t look correct. It should be black at the south pole (-180, -90) and yellow at the north pole (180, 90) and a smooth gradient from 50% green to yellow(ish) around the equator.

For reference, this is the orientation of the globe using the default material:

I’m really not sure if my assumptions are correct about how the mesh vertices are really in Unity’s local space (or another space) (or if I’m flipping lon/lat or lat/lon values again?)

Maybe I need to transform the vertices by the transform of the GameObject?

I’m out of time today… I’ll try this tomorrow.

OMG… this was it! The missing link was to transform the mesh vertices by the transform of the game object:

for (int i = 0; i < verts.Length; i++)
{
    var v3 = localToWorldMatrix.MultiplyPoint3x4(verts[i]);
    var d3 = double3(v3);
    // Convert vertex to ECEF.
    d3 = Georeference.TransformUnityPositionToEarthCenteredEarthFixed(d3);
    // Convert to Longitude, Latitude, and Height.
    d3 = CesiumWgs84Ellipsoid.EarthCenteredEarthFixedToLongitudeLatitudeHeight(d3);
    // Store Lon, Lat, Height values.
    lonLatHeight[i] = new Vector3((float)d3.x, (float)d3.y, (float)d3.z);
}

Finally… elevation ramps in Unity!

4 Likes

it looks very nice

I also added contour lines.

Sorry for the radio silence on my end @jpvanoosten! I was out of office until today, but am glad to see you eventually found a solution.

Thanks for sharing your process with the community – the result looks amazing!! :tada:

1 Like