CesiumGS / cesium

An open-source JavaScript library for world-class 3D globes and maps :earth_americas:
https://cesium.com/cesiumjs/
Apache License 2.0
13.02k stars 3.51k forks source link

Allow overriding the final output of custom shaders #12291

Open UniquePanda opened 2 weeks ago

UniquePanda commented 2 weeks ago

Feature

Context I have some vector data that I'd like to show in a CesiumJS viewer using a streamable data format. The obvious choice is to use 3DTiles. 3DTiles does not have built-in support for vector data, however I am already able to convert my vector data into GLTF tiles. This is working fine for polygons and points. Lines however require some special handling, because you usually don't want to show vector lines only with a fixed width (of e.g. 1 meter). Instead you want to be able to e.g. control their width.

Use Case for this issue As explained in Cesium's own blog post here https://cesium.com/blog/2013/04/22/robust-polyline-rendering-with-webgl/ and as implemented for example here https://github.com/pmndrs/meshline/blob/master/src/MeshLineMaterial.ts for THREE.js, there are some neat vertex shader tricks for rendering lines. I can apply those to a 3DTileset that contains lines from a vector dataset and it works great. However, it only works if I am able to control the final gl_Position. The current way how custom shaders are implemented does not allow for such precise control of the shader output, because the custom shader code is not run at the end of the shader. In my specific case the problem is that the geometryStage will always run after the custom shader code and will transform the output of the custom shader code. Simply cancelling out the calculations of the geometryStage by applying the inverted matrices on the positionMC in the custom shader will lead to massive precision loss and therefore to jagged lines.

Similar use cases may exist. In fact, I have a smaller use case for a fragment shader, too, where I want to prevent an override of the output color because I use a combination of CPU styling and fragment shader styling. This however might be resolvable with some other workarounds I didn't try yet.

Proposed solutions/ideas I have two ideas how this might be possible to achieve:

  1. Allow to set an option on the custom vertex and fragment shader objects that prevents any modification of the output of the custom shader code.
  2. Allow to define a second custom shader function, like overrideShaderOutput(), that will be run at the end of the final shader. This would allow for having some shader code in the normal custom shader and some in the "override" shader to use the advantages of both worlds.

Disclaimer I know that this would open the door for much more developer error than is currently possible. However, as the current way of adding custom shaders wouldn't be removed, I think this solution would still be a nice addition to the framework.

If you'd consider this idea feasible, I could try to come up with a PR for it.

ggetz commented 1 week ago

Thanks for the suggestion @UniquePanda!

What you propose would certainly be one way to solve the issue of handling lines in vector data. However, I wonder if there is a more fundamental question here about how to handle line width display. How is the line data stored in glTF in this case?

UniquePanda commented 1 week ago

Thanks for the repsonse, @ggetz! Sure, obviously the "nicer" solution in my case would be for 3DTiles to support the direct usage of vector data! However, I think this topic is off the table for now. 😄

Another solution would be, to use LINE_STRIPs in the GLTF. This technically works, but only allows for 1px wide lines, because of the limitations of gl.lineWidth(). So currently, when a 3DTiles tileset contains a GLTF with LINE_STRIP primitives those lines are always rendered as 1px wide lines. However, my research in this topic is already some months old now, so maybe I just missed something or something changed by now?

I thought that somewhere I read something about this and concluded that Cesium won't support "better" line rendering from GLTFs, because essentially what you need to do is what I did by myself now:

  1. Make the line a polygon by extruding it and triangulate the shape
  2. Use that polygon together with a shader to change the lines thickness

I might be mistaking about this, because I wasn't able to find any issue or forum thread about this right now. :)

So to answer this question:

How is the line data stored in glTF in this case?

The lines are triangulated and then stored as a polygon (primitive mode TRIANGLES) in the GLTF. However, as this is done in my own preprocessing, I could store them in any other way, too.