Self made 3D tiles terrain, precision problem at edges problem

Hi.
I’m developing a custom 3d tiles terrain-only generation workflow.

(In case the topic is too complicated to read and understand, I’m open to any direct suggestions about handling vertex positions at earth scale, like using double typed calculation instead of single, or using CESIUM_RTC etc. Something obvious for you may be something new for me)

I have a problem at the connected edges of the tiles I generated. Until now, no matter I’ve tried I’m not able to make any vertices at the edges ‘equal*’ wrt its closest counterpart in the other tile. In my latest attempts, the error is between 10-30 cm. See my latest enhancement, the difference between the tiles relative to default cube size, which is great but not sufficient at the same time :slight_smile:

When I visually analyse Cesium World Terrain, the error is much more less, almost not visible in any edges between tiles in same LOD. So, there should be a solution to my problem…


I was initially using ‘translate’ of glTF meshes to translate the vertices to earth surface. However, it only support float/single operations. Hence, I switched to using CESIUM_RTC extension, instead of using ‘translate’ property. The result is better, as seen in the image.

Skipping how I generated the tiles from heightmaps and textures, my latest relevant step in my workflow is to bring vertices at the connected edges together for the same LOD levels :

  • Import 2 connected gltf tiles into the Blender
  • Take the difference of CESIUM_RTC fields and translate the 2nd tile wrt this difference. I’m still close to origin, so less floating point errors
  • Now, the tiles are connected as expected, as if they are on the earth surface.
  • For each edge vertex in mesh1, I compare the vertex position against mesh 2’s related vertex, and make them equal.
  • Not touching CESIUM_RTC during the process
  • Before saving the result, I translate mesh2 to its initial position than save the gltf meshes.

Shortly, I’m temporarly translating the 2nd tile, making snap operation then translating back the 2nd tile to its initial position.

During the snapping process I explained above, I report the distances between the vertices and the result is satisfactory enoughs (units are in meters):

Dist= 0.000061 | Mesh1 ([ 400.17355 9198.144 -5480.696 ]) ↔ Mesh2 ([ 400.1735 9198.144 -5480.696 ])
Dist= 0.000183 | Mesh1 ([ 387.7054 9219.322 -5479.6772]) ↔ Mesh2 ([ 387.70523 9219.322 -5479.6772 ])
Dist= 0.000214 | Mesh1 ([ 375.70593 9240.812 -5478.2256 ]) ↔ Mesh2 ([ 375.70572 9240.812 -5478.2256 ])
Dist= 0.000153 | Mesh1 ([ 363.25732 9262.004 -5477.189 ]) ↔ Mesh2 ([ 363.25748 9262.004 -5477.189 ])
Dist= 0.000183 | Mesh1 ([ 349.0721 9282.043 -5477.756 ]) ↔ Mesh2 ([ 349.07193 9282.043 -5477.756 ])
Dist= 0.000183 | Mesh1 ([ 335.50082 9302.489 -5477.756 ]) ↔ Mesh2 ([ 335.50064 9302.489 -5477.756 ])

However, when I render the tileset in Unreal Engine or Blender, I see those ~30 cm openings between the tiles, and I’m almost out of ideas…

  • Is there another field like CESIUM_RTC, that can be used to support 64bit operations during the process. Maybe ‘matrix’ field in glTF or RTC field in 3D Tiles?
  • My meshes have ‘rotation’ applied (by blender), could be a problem
  • I may need to update ‘CESIUM_RTC’ value after the snapping process I described above
  • Any other suggestions to check?

I’m attaching my 3dtiles, also the blog post about the precision problem:

Precisions, Precisions | DME Component Libraries for .NET 2024 r3

tdtiles_yy.zip (17.3 MB)

There are several building blocks and processing steps coming together here: The source data. The conversion. The representation within the glTF. The rendering engine. And at each point, precision might be lost in one way or another. So it might be necessary to iterate on some of these points in order to find a suitable solution.

I had a short look at the tileset, but only within a CesiumJS sandcastle (I’d have to allocate some time to check this in the latest Unreal version). From that inspection, it looks like there is a tiny gap between the tiles, but it’s hard to estimate the actual size.


An aside: It looks like the bounding regions in the tileset do not perfectly match the data - see the bounding volume where the mouse is in this screenshot:

This should not affect the precision issue. But it may lead to unexpected behavior (like tiles suddenly disappearing), and should be fixed eventually.


In any case: Preferably, all computations should happen in double precision. The conversion to float should only be the absolutely last and final step, when writing the data into the glTF accessors (because for the GPU, the values have to be single-precision float).

But this does raise the question about how to handle “large values”. This is what the “Precisions, Precisions” blog post that you already linked to referred to. When you have “positions” around a point like 4217057.8, 2789986.8, 3874670.5 (like in your data), then these values should not be stored direcly as float values, because that will lead to precision issues (distorted geometry, and jittering).


You mentioned CESIUM_RTC several times. While this can work in some cases, I’d usually recommend to not use this extension. It is an extension for glTF 1.0 (!), and most engines will not be able to handle this extension when it appears in a glTF 2.0 asset. (The fact that Cesium engines support this even in glTF 2.0 is … an attempt to handle “legacy” data, but it’s nothing that should be used when creating new data).

The proper way to handle the concept of “relative to center” rendering in glTF is to add the “RTC center” as a translation in a node in the the glTF. You mentioned

I was initially using ‘translate’ of glTF meshes to translate the vertices to earth surface. However, it only support float/single operations.

And it’s not clear what this refers to: When you really translate the POSITION data, then it’s true that this will only store single-precision values (and cause errors). But using a glTF node.translation should support double precision.

Note: Some rendering engines also cannot cope with that :grimacing: But that’s rather an issue of these engines, and should not affect the design of the data itself.


The process that you described with the bullet point list sounds… a bit complicated, and involves CESIUM_RTC, but like it might work. You conclusion there was

However, when I render the tileset in Unreal Engine or Blender, …

Note that Blender does not support CESIUM_RTC - so within Blender, the tiles will be displayed near the origin. Also, I don’t know for sure how well Blender handles the case where large positions are using “relative to center” rendering, even when it is stored as a node.translation in the glTF. (I’d have to check this, but for now, wanted to mention that Blender might not be the ultimate reference here).


Is there another field like CESIUM_RTC, that can be used to support 64bit operations during the process. Maybe ‘matrix’ field in glTF or RTC field in 3D Tiles?

Broadly speaking, there are different ways of “placing geometry data somewhere on the globe”:

  • Storing the actual position in the glTF POSITION data. This is not recommended, due to the precision issues
  • Storing the POSITION data “relative to center” and store the “RTC center” itself as…
    • a glTF node.translation (or node.matrix)
    • or as a tile.transform in the tileset JSON

The latter is a bit more flexible, because you’d basically have the glTF itself “at the origin” and in its canonical representation (so that every glTF viewer can load and view it), but more specific recommendations may depend on details of the source data, goals, and processing pipeline.

Now… some of this may be obvious. And in fact, it hardly touches the main point that makes things more tricky in your case, namely, the goal to (perfectly) align two (otherwise independent) glTFs. But maybe we can iterate on that where necessary.

1 Like

I stripped other children tiles by hand, that should be why bounding box a little larger

Yea I just practised single precision problem recently. Now being extremely careful not to involve any single precision operations, except last stage.

So I incorrectly evaluated the statements about CESIUM_RTC. I’d been using node.translation and it was the time I realized single/double precision problems, hence switched to CESIUM_RTC. Now you mentioned that, I should go back and play with node.translation with my single/double experience. Appearently, node.translation was not the problem…

I tested it by taking difference of 2 CESIUM_RTC values and translating one of the tiles as the difference. It happened around the origin, you’re right that Blender doesn’t support CESIUM_RTC.

I’ll try node.translation/node.matrix first, this time carefully handling single/double issue.
Thanks

Blender’s internals definitely effecting the precision. Maybe not for vertex positions, but transformation related fields like matrix, rotate, translate, etc…

I have a C++ code that generates glTF mesh which uses double operations. That is my ideal reference source for transformation values, then I have a blender script that modifies the vertex positions, locally.

I modified the exported glTF’s ‘translate’, ‘rotation’ and ‘matrix’ fields by using the reference values present in my C++ generated mesh, re-writing float values in doubles, without changing the binary buffers. (The reference cube is for reference, 1x1x1 meter)

The result is promising, this is the best result I got so far, the error is ~6-8cm.

Still struggling… I’ve made hell a lot tests, tried different things but…

Lately, I started to think the problem is related to node.mesh.translation (or node.matrix) and the errors it introduced. I’ve compared two meshes with different translations, but sharing same world positions. Before saving the being modified glTF, I logged the local vertex positions (First part below) just before exporting/saving it to a .gltf file and compared it with the saved one(second part below). Local vertex positions are identical so they are not changed internally by blender’s internals. When world positions are compared, I see a ~0.258 offset to differences between related vertices’s world positions.

The format is:
VID1<->VID2, Distance between World Positions, V1_LocalPos, V2_LocalPos

Vertex 318260↔ 318260 Dist=0.000031 - [ 426.99807739 5480.98632812 9157.0390625 ] - [ 8663.99804688  5480.98632812 -3542.9609375 ]
Vertex 318261↔ 318261 Dist=0.000092 - [ 413.24911499 5481.15087891 9177.36816406] - [ 8650.24902344  5481.15087891 -3522.63183594]
Vertex 318262↔ 318262 Dist=0.000275 - [ 400.17355347 5480.69384766 9198.14355469] - [ 8637.17382812  5480.69384766 -3501.85644531]
Vertex 318263↔ 318263 Dist=0.000336 - [ 387.70541382 5479.67529297 9219.32226562] - [ 8624.70507812  5479.67529297 -3480.67773438]
Vertex 318264↔ 318264 Dist=0.000122 - [ 375.70593262 5478.22363281 9240.81152344] - [ 8612.70605469  5478.22363281 -3459.18847656]
Vertex 318265↔ 318265 Dist=0.000488 - [ 363.25732422 5477.18701172 9262.00390625] - [ 8600.2578125   5477.18701172 -3437.99609375]
Vertex 318266↔ 318266 Dist=0.000153 - [ 349.07211304 5477.75390625 9282.04296875] - [ 8586.07226562  5477.75390625 -3417.95703125]
Vertex 318267↔ 318267 Dist=0.000153 - [ 335.50082397 5477.75390625 9302.48925781] - [ 8572.50097656  5477.75390625 -3397.51074219]
Vertex 318260↔ 318260 Dist=0.248298 - [ 426.99807739 5480.98632812 9157.0390625 ] - [ 8663.99804688  5480.98632812 -3542.9609375 ]
Vertex 318261↔ 318261 Dist=0.248298 - [ 413.24911499 5481.15087891 9177.36816406] - [ 8650.24902344  5481.15087891 -3522.63183594]
Vertex 318262↔ 318262 Dist=0.248298 - [ 400.17355347 5480.69384766 9198.14355469] - [ 8637.17382812  5480.69384766 -3501.85644531]
Vertex 318263↔ 318263 Dist=0.248298 - [ 387.70541382 5479.67529297 9219.32226562] - [ 8624.70507812  5479.67529297 -3480.67773438]
Vertex 318264↔ 318264 Dist=0.248298 - [ 375.70593262 5478.22363281 9240.81152344] - [ 8612.70605469  5478.22363281 -3459.18847656]
Vertex 318265↔ 318265 Dist=0.248299 - [ 363.25732422 5477.18701172 9262.00390625] - [ 8600.2578125   5477.18701172 -3437.99609375]
Vertex 318266↔ 318266 Dist=0.248298 - [ 349.07211304 5477.75390625 9282.04296875] - [ 8586.07226562  5477.75390625 -3417.95703125]
Vertex 318267↔ 318267 Dist=0.248298 - [ 335.50082397 5477.75390625 9302.48925781] - [ 8572.50097656  5477.75390625 -3397.51074219]

I can only make distance in world positions as is when I use the identical translation value between the source and modified mesh. If values are different, I can’t make them closer and it seems the distance almost has an offset value. (like 0.248 in this case)

Maybe I should use same translation value for neighbouring tiles…
:pleading_face:

I have this structure involving matrix and translation for my tiles:

"nodes":[
		{
			"mesh":0,
			"name":"Mesh_0.001",
			"translation":
			[ 4200249.74042291,2793613.93061088, 3880785.3615384   ]
		},
		{
			"children":[
				0
			],
			"matrix":[
				1,
				0,
				0,
				0,
				0,
				0,
				-1,
				0,
				0,
				1,
				0,
				0,
				0,
				0,
				0,
				1
			],
			"name":"Node_0.001"
		}
	],

If tiles cover large areas, local vertex positions may start having precision issues.

For:8000.123535

float:  value                  = 8.000124e+03 ( 8000.123535)
float:  nearest above          = 8.000124e+03 ( 8000.124023)
float:  nearest below          = 8.000123e+03 ( 8000.123047)

For -12000.1234567:

float:  value                  = 1.200012e+04 ( 12000.123047)
float:  nearest above          = 1.200012e+04 ( 12000.124023)
float:  nearest below          = 1.200012e+04 ( 12000.122070)

(used this tool: http://www.ehopkinson.com/cgi-bin/floatprecision_htmlout?float=-12000.1234567)

For -12000.1234567 case, the error between below and above roundings are around 0.002 level, which corresponds to millimeter and very small for my case.

If translation is applied as summation, then I don’t expect this millimeter error to increase/expand, as the process is simply summation of mesh.translation(Center) + local vertex value (double + float). (or, is it?)

I have blender involved in my workflow, but I’m manually handling the node.translation value to be saved as ‘double’. So my node.translation value is certainly double.

The image simply shows how I’m doing my calculations:

(removed some part)

Btw, I’m visually investigating World Terrain and clips downloaded from Cesium Ion, by using wireframes and suspend update feature of tileset actors and I frequently see the same problem especially in low LOD’s (tiles that cover bigger areas). In high detail areas, the edges are almost always exactly connected.

To emphasize my disclaimer:

There are several building blocks and processing steps coming together here: The source data. The conversion. The representation within the glTF. The rendering engine. And at each point, precision might be lost in one way or another. So it might be necessary to iterate on some of these points in order to find a suitable solution.


I’m not sure whether I fully understood all details of what you have been describing in the last posts. But iff I understood this correctly, then you are doing certain computations (for aligning the vertex positions) within Blender. (I’m not a Blender expert - is this some sort of “script” that can be run in Blender itself? Or some sort of ~“Python-based ‘Plugin’ for Blender” that you created just for this purpose?)

Looking at the node.translation of [ 4200249.74042291,2793613.93061088, 3880785.3615384 ] in the JSON that you posted, it might very well be that this large translation causes trouble here. (Maybe some of this could even be caused for double when something like a cancellation is happening somewhere - some guesses are involved here…)

Specifically, you mentioned

… the process is simply summation of mesh.translation(Center) + local vertex value (double + float) . (or, is it?)

I don’t know for sure. But it might be that Blender is doing some computation or storage here in single-precision float.

Even at the risk that this appears to be a bit of trial-and-error (with the goal of not really avoiding but just minimizing the error…):

You could try to not store the node.translation in the glTF at all. Instead, you could apply this translation as part of the tile.transform within the tileset JSON.

This may have two advantages:

  • The resulting glTF could be in its canonical form: At the origin, with y-up. (I tried to illustrate this advantage in an otherwise unrelated thread
  • More important here: All the computations that you are doing there do no longer involve these “huge” numbers. They can all happen in this “local, small glTF”. (And the glTF is only moved to the right position eventually, with the 3D Tiles transform)