Skip to content

Conversation

danielzhong
Copy link

@danielzhong danielzhong commented Aug 30, 2025

Description

This PR implements the proposed GLTF extension EXT_mesh_primitive_edge_visibility, enabling edge rendering for glTF mesh primitives. It adds multi-pass rendering to avoid z-fighting, and implements decoding, creation, and rendering of both hard and silhouette edges:

  • Parsing and handling mesh primitive data of visibility (edge type: HIDDEN, SILHOUETTE, HARD, REPEATED) and silhouette normal.
  • Hard edge & silhouette edge rendering.
  • Multi-pass rendering path (CESIUM_3D_TILE_EDGES) to draw edges before surfaces (CESIUM_3D_TILE).
  • Feature ID/Depth integration to ensure edges display correctly without Z fighting and overlapping.

More details: Edge Visibility

Example
(green edge = hard edge; red edge = silhouette edge):
image

Issue number and link

#12765

Testing plan

How to Run:

  1. Download the file and host: http-server ./ --cors=X-Correlation-Id
  2. Open sandcastle and paste:
const viewer = new Cesium.Viewer("cesiumContainer", {
 
  infoBox: false,
 
  selectionIndicator: false,
 
  shadows: true,
 
  shouldAnimate: true,
 
});
const tileset = await Cesium.Cesium3DTileset.fromUrl(
 
  "http://172.20.20.20:8081/tileset.json"
 
);
 
viewer.scene.primitives.add(tileset);
 
await viewer.zoomTo(tileset);

Make sure change your local host address in the code!

((green edge = hard edge; red edge = silhouette edge):)
image

🔍Review Notes

  • The glTF extension’s material support is not included in this PR. I'm planning to set all edges default to black.
  • Updated getWebGLStub.js to support MRT in coverage tests.
  • This PR is ready to review!

Bug Fix Summary

  • Edge rendering was incorrect because GL_LINES and edge primitives shared the same VAO. Fixed by creating a separate VAO for edge rendering.
  • Added an edge-rendering pipeline stage in modelFS.glsl. The model runtime primitive couldn’t detect the render tag (it runs too early), so I introduced a uniform flag to detect when the pipeline stage is active.
  • MRT couldn’t clear buffers/color attachments. Fixed by using clearCommand in EdgeFramebuffer.js.
  • MRT was failing unit tests. Fixed by updating getWebGLStub.js to support MRT in coverage tests.
  • Silhouette edges bug: in top-down views, the silhouette normal calculation wasn’t accurate enough.

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code
  • Change outline render color back to white
  • Fix issue where edge should not be visible through geometry

@danielzhong
Copy link
Author

danielzhong commented Sep 18, 2025

@ggetz @lilleyse I’ve fixed the edge depth bug that caused geometry can see through edges. The latest screenshot have been updated in the description. Demo link will update ASAP, since not sure why there are some unrelated timeout build errors in Github. This is now ready for the next round of review, thank you!

This reverts commit bc16ca0, reversing
changes made to afe6e41.
@danielzhong danielzhong requested a review from ggetz September 18, 2025 00:59
@danielzhong
Copy link
Author

@ggetz @lilleyse I’ve fixed the edge depth bug that caused geometry can see through edges. The latest screenshot have been updated in the description. Demo link will update ASAP, since not sure why there are some unrelated timeout build errors in Github. This is now ready for the next round of review, thank you!

Demo Link updated as well

Copy link
Contributor

@lilleyse lilleyse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielzhong Excellent job with this PR!

Just a few comments from me.

shaderBuilder.addVarying("vec3", "v_faceNormalBView", "flat");

// Add varying for view space position for perspective-correct silhouette detection
shaderBuilder.addVarying("vec3", "v_positionView", "");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know offhand... does Model already have a varying for this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. Fixed!

* @returns {{edgeMap:Map<string, number[]>, faceNormals:Float32Array, triangleCount:number}}
* @private
*/
function buildTriangleAdjacency(primitive) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this function and others that are doing a lot of math see if you can use Cartesian3 types and Cartesian3 math operations (e.g. Cartesian3.cross). Also try to use scratch variables wherever possible to avoid heap allocations.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified, please review again! Thanks!

Comment on lines 204 to 206
const positions = defined(positionAttribute.typedArray)
? positionAttribute.typedArray
: ModelReader.readAttributeAsTypedArray(positionAttribute);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you foresee any issues if positions are quantized?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, please review.

Comment on lines 819 to 820
const renderState = clone(command.renderState, true);
edgeCommand.renderState = RenderState.fromCache(renderState);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the render state actually modified?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to clean it up. Fixed!

Comment on lines 822 to 826
// Use a very large bounding volume to avoid culling issues
edgeCommand.boundingVolume = new BoundingSphere(
Cartesian3.ZERO,
Number.MAX_VALUE,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have some performance side effects.

createPotentiallyVisibleSet determines the near / far planes from the commands' bounding volumes and ideally we want these to be as tight as possible to avoid needing to render with multiple frustums.

I'm curious about the culling issues you were seeing, because it seems like it should be fine to use the original command's boundingVolume.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines +534 to +541
//
// CESIUM_REDIRECTED_COLOR_OUTPUT: A general-purpose flag to indicate that this shader
// is a derived/modified version created by Cesium's rendering pipeline.
// This flag can be used to avoid color attachment conflicts when shaders
// need different output declarations for different rendering passes.
// For example, MRT (Multiple Render Targets) features can check this flag
// to conditionally declare their output variables only when not conflicting
// with the derived shader's output layout.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit rusty here, but is the problem that the edge visibility code, even though it's not executed in the OIT pass, has output declarations that are incompatible with OIT's output declarations?

If that's the case, this seems like an okay approach, short of other approaches that would require refactoring the model pipeline.

@danielzhong
Copy link
Author

danielzhong commented Sep 19, 2025

image

@lilleyse Not sure why I can't reference or reply. But yes, that’s the root cause. Do you think I should include the refactor in this PR, or would it be better to open a new PR for it later?

@lilleyse
Copy link
Contributor

@danielzhong oh I think that comment is part of this comment thread: #12859 (comment)

I would just open an issue for now.

@danielzhong
Copy link
Author

@lilleyse @ggetz Ready for next round review!

@javagl
Copy link
Contributor

javagl commented Sep 24, 2025

The EdgeVisibility.glb is invalid. The errors are

         {
                "code": "VALUE_NOT_IN_RANGE",
                "message": "Value 1 is out of range.",
                "severity": 0,
                "pointer": "/bufferViews/5/byteStride"
            },
            {
                "code": "VALUE_NOT_IN_RANGE",
                "message": "Value 1 is out of range.",
                "severity": 0,
                "pointer": "/bufferViews/12/byteStride"
            },

CesiumJS does not render anything for this model. (Opening and re-exporting it with gltf.report generates a model that can be rendered with CesiumJS. Whether or not that is related to the validation error remains to be investigated).


The model uses

  "extensionsUsed" : [
    "KHR_mesh_quantization",
    "EXT_mesh_features",
    "EXT_mesh_primitive_edge_visibility",
    "EXT_structural_metadata"
  ],

There should be test model that uses EXT_mesh_primitive_edge_visibility and only EXT_mesh_primitive_edge_visibility - no other extension that might interfere with the baseline test for this extension. Such a model would also be useful, eventually, in the context of KhronosGroup/glTF#2479 , for implementors to test.


I did not peform a thorough review of this PR! It only caught my attention due to #12914 . And when I saw https://github.com/CesiumGS/cesium/pull/12859/files/1c9ba9d2415aa507cce107d91938d10c9e707cd1#diff-20964905fdcbaf8679c56d995e848ed68a3c0f08d868ad38c5d794145b0fbbc6R70, I thought: Wait, that can hardly be right. Edge visibility and feature IDs are totally unrelated. Maybe these defines are the crux here, and maybe there is some if (hasFeatureIds) doThis(); else dont(); that I overlooked and that prevents this from being executed when no feature IDs are present. If this is the case, it should be documented more extensively. And if this is the case, then it should thoroughly explain why it can rely on the featureId_0, and not any other feature ID.
// We know that featureId_0 is present here, because [text]

@danielzhong
Copy link
Author

The EdgeVisibility.glb is invalid. The errors are

         {
                "code": "VALUE_NOT_IN_RANGE",
                "message": "Value 1 is out of range.",
                "severity": 0,
                "pointer": "/bufferViews/5/byteStride"
            },
            {
                "code": "VALUE_NOT_IN_RANGE",
                "message": "Value 1 is out of range.",
                "severity": 0,
                "pointer": "/bufferViews/12/byteStride"
            },

CesiumJS does not render anything for this model. (Opening and re-exporting it with gltf.report generates a model that can be rendered with CesiumJS. Whether or not that is related to the validation error remains to be investigated).

The model uses

  "extensionsUsed" : [
    "KHR_mesh_quantization",
    "EXT_mesh_features",
    "EXT_mesh_primitive_edge_visibility",
    "EXT_structural_metadata"
  ],

There should be test model that uses EXT_mesh_primitive_edge_visibility and only EXT_mesh_primitive_edge_visibility - no other extension that might interfere with the baseline test for this extension. Such a model would also be useful, eventually, in the context of KhronosGroup/glTF#2479 , for implementors to test.

I did not peform a thorough review of this PR! It only caught my attention due to #12914 . And when I saw https://github.com/CesiumGS/cesium/pull/12859/files/1c9ba9d2415aa507cce107d91938d10c9e707cd1#diff-20964905fdcbaf8679c56d995e848ed68a3c0f08d868ad38c5d794145b0fbbc6R70, I thought: Wait, that can hardly be right. Edge visibility and feature IDs are totally unrelated. Maybe these defines are the crux here, and maybe there is some if (hasFeatureIds) doThis(); else dont(); that I overlooked and that prevents this from being executed when no feature IDs are present. If this is the case, it should be documented more extensively. And if this is the case, then it should thoroughly explain why it can rely on the featureId_0, and not any other feature ID. // We know that featureId_0 is present here, because [text]

Thanks for the feedback, it’s fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants