Terrain mesh to GLTF models

Hi, currently I’m exploring ways to generate gltf / obj models from terrain mesh. My goal is to show 3d cut-fill volume calculation, something like the following -

image

This is a follow-up question of this one - Generate GLTF model from raster (DSM) instead of terrain - #3 by atul-sd but an entirely different approach

I realized that I don’t need to DEM to generate GLTF models. Terrain mesh already has all necessary information, if I can load this mesh in some tool and export it as GLTF/OBJ, that’ll solve the problem -

I need to figure out the following things -

  1. How can I get this mesh data (vertices, faces) from the tiles? I’ve gone through this question but still not sure - Terrain mesh to x3d. I’m able to get QuantizedMeshTerrainData from requestTileGeometry method
  2. How can I get mesh data only within a polygon (as shown in the first image), is there any built-in internal method, as I can’t find any method in public API
  3. Once I get the correct mesh data, which library will let me do the import/export? It would be much better if there is any library which directly reads .terrain files
  4. I’ll prefer to do this whole mesh generation and conversion in the backend server. Is there any way to read tile outside the cesium (but just like cesium)

Thanks!

Progress Update 1

I was able to generate the TerrainMesh by using the undocumented method createMesh of QuantizedMeshTerrainData using following code -

const tileCoord = {x:1936452, y: 711072}
const level = 20

viewer.terrainProvider.requestTileGeometry(x, y, level).then((data) => {
    data.createMesh(new GeographicTilingScheme(), tileCoord.x, tileCoord.y, level).then((mesh) => {
        console.log("Terrain mesh:", mesh)
    })
})

Now I’m not sure how to use this information to create a model. I can see some important information like stride, vertices and indices but I’m not sure what to do with it.

I read about what each value means here but I’m still not sure how to make sense of vertices array.

Currently I’m trying to understand how wireframes are generated for tiles in debug mode (like shown in above image) and going through createWireframeVertexArray. I’m not entirely sure whether it’ll help but I’m hopeful.

Attaching mesh data which I pulled for one tile (changed file extension to geojson, json files are not allowed) - terrainMesh.geojson (171.1 KB)

Progress update 2

I tried reading the mesh data as shown here and created a very simple OBJ file from the quantized mesh data.

const MAX_SHORT = 32767;
const tileCoord = {x:1936452, y: 711072}
const level = 20

this.map.terrainProvider.requestTileGeometry(tileCoord.x, tileCoord.y, level)
  .then((terrainData) => {
    console.log("Terrain tile data:", terrainData)
    const rect = new Cesium.GeographicTilingScheme().tileXYToRectangle(tileCoord.x, tileCoord.y, level);

    const data = terrainData as any;
    const minimumHeight = data._minimumHeight;
    const maximumHeight = data._maximumHeight;

    const indices: Uint16Array = data._indices;
    const quantizedVertices: Uint16Array = data._quantizedVertices;
    const vertexCount = quantizedVertices.length / 3;

    const vertices: [number, number, number][] = []
    const faces: [number, number, number][] = [];

    for (let i = 0; i < vertexCount; i++) {
      const rawU = quantizedVertices[i];
      const rawV = quantizedVertices[i + vertexCount];
      const rawH = quantizedVertices[i + vertexCount * 2];

      const u = rawU / MAX_SHORT;
      const v = rawV / MAX_SHORT;

      const longitude = Cesium.Math.lerp(rect.west, rect.east, u);
      const latitude = Cesium.Math.lerp(rect.south, rect.north, v);
      const height = Cesium.Math.lerp(minimumHeight, maximumHeight, rawH / MAX_SHORT);

      const carto = new Cesium.Cartographic(longitude, latitude, height);
      const cartesion = Cesium.Cartographic.toCartesian(carto)
      vertices.push([cartesion.x, cartesion.y, height])
    }

    // form triangle faces from indices
    let i = 0;
    while (i < indices.length) {
      faces.push([indices[i], indices[i + 1], indices[i + 2]])
      i += 3
    }

    console.log("Vertices:", vertices);
    console.log("faces:", faces)
  })

Now when I try loading that OBJ file in the Blender, I get the following error -

Python: Traceback (most recent call last):
  File "/snap/blender/1653/3.0/scripts/addons/io_scene_obj/__init__.py", line 151, in execute
    return import_obj.load(context, **keywords)
  File "/snap/blender/1653/3.0/scripts/addons/io_scene_obj/import_obj.py", line 1264, in load
    for data in split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP):
  File "/snap/blender/1653/3.0/scripts/addons/io_scene_obj/import_obj.py", line 547, in split_mesh
    verts_split.append(verts_loc[vert_idx])  # add the vert to the local verts
IndexError: list index out of range

location: <unknown location>:-1

Can someone please tell me what I’m doing wrong?

PS: Attaching OBJ file which I created - test.zip (22.5 KB)

Progress update 3

I used trimesh python library and generated a glb model, I was able to visualize it here - https://gltf-viewer.donmccurdy.com/ but its doesn’t exactly represent the mesh as it’s shown in Cesium’s wireframe (it also jitters when you rotate model)

vertices = [...] # vertices from tile mesh from progress update 2
faces = [...] # faces from tile mesh from progress update 2

mesh = trimesh.Trimesh(vertices=vertices, faces=faces)

for facet in mesh.facets:
    mesh.visual.face_colors[facet] = trimesh.visual.random_color()

mesh.export('test.glb', 'glb')

This is how it looks in wireframe -
image

test.glb (25.4 KB)

Hi @Kevin_Ring, by any chance, if you are watching this, would really appreciate it if you can nudge me in right direction or point to the right resources. I’ve gone through most of the existing topics. I also tried understanding how wireframe works in debug mode, but couldn’t really understand it.

Regards,
Atul

Hi @atul-sd,

I can’t tell you exact steps for doing this, but I think you’re on the right track. As a guess, the problem might be that the terrain tile is using quantization (see the encoding property of TerrainMesh), but that’s mostly a guess.

You do need to be careful, though. Extracting Cesium World Terrain for offline use is against the terms of use. Accessing it on-the-fly to do some analysis is probably ok, though.

Kevin

Hi @Kevin_Ring ,

Thank you for your reply, I’m pretty new to 3d graphics (and computer graphics in general), can you please elaborate on quantization and how to use the encoding property?

Thanks for the heads up, I’ll be using custom terrain tiled using cesium-terrain-builder so I think that should be okay.

Atul

Progress Update 4

Hi,

So I switched from quantized terrain to heightmap terrain and was able to generate the correct mesh and export it to OBJ and GLTF. Now I have got the following challenges -

  1. The exported model’s scale is not matching with the tile size, what do I have to do to fix it? Also, the model is not aligned correctly (it need to be rotated 90 degrees to the right). I will be generating these models dynamically so doing anything manually is not an option.

  1. I want to find out all the tiles which are within the drawn polygon, I couldn’t find any way to do it so far. Ideally would want to request target tiles from the backend server and do the processing there.

  2. Can I use Cesium’s methods on the server-side, I would like to use requestTileGeometry and other methods to pull tiles in the server and do further steps?

  3. I figured that I’ll be needing high-quality heightmap terrain tiles for generating detailed models, can Cesium Ion on-premise terrain builder generate terrain in heightmap format or just quantized mesh format?

Regards,
Atul

Hi,

Well, I did go down this path some time ago, but through the Cesium API I think it’s a bit of a stretch. I did investigate writing a shader for an area that streams a heightmap back to JS space, as the shader will just traverse the raster cache in the 3D engine, however never got properly started on that (although the terrain wireframe visualisation is through a shader).

CesiumJS is very metadata heavy, and discards a lot of data as it’s streamed into the WebGL layer. You could try to write hooks into the CesiumJS engine for capturing some of this, however I think it will be quite a hit and miss thing. I think you’re on the right path, but keep in mind that the terrain accuracy and details are streamed as needed. It might be worth your time to do a sampleTerrainMostDetailed() once or twice over the area before you request the tileGeometry() to make sure the raster buffers are as good as they can get.

  1. As to your questions around getting the tiles for your area, that’s a bit trickier as zones are streamed into the terrain as needed, and the zones / tiles won’t match your polygon. You simply may have to find your tile, and also request all tiles around it, and then triangulate your polygon over a larger iled area, then only keep what you need.

  2. You could try to find the corners of your polygon, sample the terrain at those spots, find out the tiles, and build a tile request array from that, but I honestly haven’t pursued this in too much detail for quite some time.

  3. Maybe. What terrain are you using? Depending on the size of the terrain file, it may be simpler to stream it and process it outside of the browser, something like building your own API that gets the polygon and the source terrain, and do the processing, sending back a matrix (in whatever format you need).

  4. I’ll leave to the Cesium guys to answer.

Cheers,

Alex

1 Like

Hi @Alexander_Johannesen,

Thank you for your detailed response, I’ll definitely explore the approaches that you have mentioned and come back with further updates.

I’m using height map terrain, I have a custom terrain server so getting tiles on the server is not an issue. Can you please elaborate the point 3? (assume that we have polygon on the server)

EDIT

As you mentioned in point 2, I can generate a grid of points (like the following), I know what level of tiles I need, let’s say L, so all I need is XY of tiles which I can get from positionToTileXY method of GeographicTilingScheme. I can run this method for all the points in the grid and only pick the unique tiles coordinate and request them only -

Is there any better solution than this?

EDIT 2

Dropped grid approach, created a bounding rectangle and found the tile XY for all four corners. Then generated rest of the intersecting tile coordinates using basic math -

const tileCoord = [] // Array of Cartesion2 points (tile coordinates for all corners)

const length = tileCoord.length;

let minimumX = tileCoord[0].x;
let minimumY = tileCoord[0].y;

let maximumX = tileCoord[0].x;
let maximumY = tileCoord[0].y;

for (let i = 1; i < length; i++) {
    const p = tileCoord[i];
    const x = p.x;
    const y = p.y;

    minimumX = Math.min(x, minimumX);
    maximumX = Math.max(x, maximumX);
    minimumY = Math.min(y, minimumY);
    maximumY = Math.max(y, maximumY);
}

const width = maximumX - minimumX + 1;
const height = maximumY - minimumY + 1;

const intersectingTileCoordinates = [];

for (let j = 0; j < width; j++) {
    for (let i = 0; i < height; i++) {
        intersectingTileCoordinates.push([minimumX + i, minimumY + j]);
    }
}

console.log(intersectingTileCoordinates)

Regards,
Atul

Now I’m able to merge meshes generated from individual tiles and generate one single model. Now I want to clip the model as per the polygon drawn by the user on the map. (see below ). The generated model is in the local coordinate system and drawn polygon in having lon/lat. How can I use the polygon to clip the model?