Skip to content

Support for BENTLEY_materials_line_style glTF Extension#13110

Open
danielzhong wants to merge 47 commits intomainfrom
DanielZ/BENTLEY_materials_line_style
Open

Support for BENTLEY_materials_line_style glTF Extension#13110
danielzhong wants to merge 47 commits intomainfrom
DanielZ/BENTLEY_materials_line_style

Conversation

@danielzhong
Copy link
Contributor

@danielzhong danielzhong commented Dec 30, 2025

Description

  1. This PR implements support for the BENTLEY_materials_line_style gltfmaterial extension in CesiumJS. This extension enables CAD-style line visualization with variable width and dash patterns. The proposed specification can be found here.
  2. This PR fixed silhouette normal decode issues: EXT_Edge_Visibility Bug in Cesium JS iTwin/itwinjs-core#8879
    in this commit: fe13f48
  3. This PR updates the EXT_mesh_primitive_edge_visibility gltf extension by switching from gl_lines to tessellated quads rendered as gl_triangles, enabling line-width support.
  4. This PR supersedes the previous PR (#12859). All changes have been moved here and mainly focus on completing the material and line string support for EXT_mesh_primitive_edge_visibility
  5. UPDATE (1/29): Issue: https://github.com/iTwin/itwinjs-backlog/issues/1812. Added cumulative-distance in line patterns gltf extension (no screen‑space calc), chose frustum‑based pixelsPerWorld over czm_metersPerPixel to match iTwin implementation.

Overview (BENTLEY_materials_line_style)

width: 5, pattern: 61680
Snipaste_2025-12-30_11-30-46
Snipaste_2026-01-01_03-02-11

The BENTLEY_materials_line_style extension allows glTF materials to specify:

width: Line thickness in screen pixels
pattern: A 16-bit repeating on/off dash pattern (each bit = 1 screen pixel)
This PR allows CesiumJS to process and apply the above extension when loading glTF files. This means lines and edges will be able to have customizable width and pattern properties specified and respected in CesiumJS when loaded via glTF.

Implementation Details:
Because variable line width is required (and many graphics APIs including WebGL do not support gl_line primitives with width > 1), this PR refactors the EXT_mesh_primitive_edge_visibility implementation to use quad-based rendering instead of the previous gl_line approach. Each line segment is tessellated into a quad (two triangles) that is dynamically expanded perpendicular to the edge direction based on the material's width property.

The 16-bit dash pattern is applied in the fragment shader by testing individual bits against the screen-space position along the line, providing pixel-perfect pattern rendering that remains stable under camera movement.

Changes:
Added support for BENTLEY_materials_line_style in GltfLoader.js and MaterialPipelineStage.js
Refactored edge rendering from line primitives to quad-based geometry in EdgeVisibilityPipelineStage.js
Implemented vertex shader expansion logic for variable-width lines in EdgeVisibilityStageVS.glsl
Implemented fragment shader pattern matching using bit extraction in EdgeVisibilityStageFS.glsl
Added u_lineWidth and u_linePattern uniforms to the edge rendering pipeline

To test these changes locally:

  • Clone this branch.
  • Build CesiumJS and Sandcastle.
  • Run the dev server locally.
  • Host your glb/gltf test files using: http-server ./ --cors=X-Correlation-Id
  • Navigate to Sandcastle and open the gltf:
    line_style.zip
    line&point_style.zip
const viewer = new Cesium.Viewer("cesiumContainer", {
  infoBox: false,
  selectionIndicator: false,
  shouldAnimate: true,
});

function createModel(url, height) {
  viewer.entities.removeAll();

  const position = Cesium.Cartesian3.fromDegrees(
    -123.0744619,
    44.0503706,
    height
  );

  const heading = Cesium.Math.toRadians(100.0);
  const pitch = 0.0;
  const roll = 0.0;
  const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
  const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);

  const entity = viewer.entities.add({
    name: url,
    position: position,
    orientation: orientation,
    model: {
      uri: url,
      minimumPixelSize: 128,
      maximumScale: 20000,
    },
  });

  viewer.trackedEntity = entity;
}

createModel("http://172.20.20.20:8282/3-1-0.glb", 100.0);
  • Or npm run build-sandcastle and in sandcastle search for the Styled glTF Lines example.

Note:

This PR may include changes from other currently open PRs, this PR was built on top of the following PRs:
EXT_mesh_primitive_edge_visibility's material and line string:
(This PR supersedes the previous PR (#12859). All changes have been moved here and mainly focus on completing the material and line string support for EXT_mesh_primitive_edge_visibility. Please refer to the links for more details.)

For line patterns & cumulative distance:
If the demo does not show density changes while zooming, it’s usually because the camera frustum width isn’t changing (e.g., a trackedEntity or fixed frustum). Once frustum.width updates, the pattern density changes as expected.

For example:

// ------------------------------
// 1) Viewer
// ------------------------------
const viewer = new Cesium.Viewer("cesiumContainer", {
  infoBox: false,
  selectionIndicator: false,
  shadows: false,
  shouldAnimate: true,
});
 
const scene = viewer.scene;
const camera = scene.camera;
const canvas = scene.canvas;
 
// ------------------------------
// 2) Orthographic camera
// ------------------------------
const ortho = new Cesium.OrthographicFrustum();
ortho.near = 0.1;
ortho.far = 1.0e9;
ortho.width = 400.0;
ortho.aspectRatio = canvas.clientWidth / canvas.clientHeight;
 
//  frustum
camera.frustum = ortho;
 
function updateOrthoAspect() {
  if (camera.frustum instanceof Cesium.OrthographicFrustum) {
    camera.frustum.aspectRatio = canvas.clientWidth / canvas.clientHeight;
  }
}
window.addEventListener("resize", updateOrthoAspect);
 
// ------------------------------
// 3) Wheel zoom => change frustum.width only
// ------------------------------
scene.screenSpaceCameraController.enableZoom = false;
 
function clamp(v, min, max) {
  return Math.max(min, Math.min(max, v));
}
 
canvas.addEventListener(
  "wheel",
  (e) => {
    if (!(camera.frustum instanceof Cesium.OrthographicFrustum)) return;
    e.preventDefault();
 
    const frustum = camera.frustum;
 
    const zoomFactor = Math.pow(1.0015, e.deltaY);
    frustum.width *= zoomFactor;
    frustum.width = clamp(frustum.width, 0.01, 1.0e9);
 
    frustum.aspectRatio = canvas.clientWidth / canvas.clientHeight;

  },
  { passive: false }
);
 
// ------------------------------
// 4) Load model (no trackedEntity)
// ------------------------------
function createModel(url, height) {
  viewer.entities.removeAll();
 
  const position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, height);
 
  const heading = Cesium.Math.toRadians(135.0);
  const pitch = 0.0;
  const roll = 0.0;
 
  const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
  const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
 
  const entity = viewer.entities.add({
    name: url,
    position,
    orientation,
    model: {
      uri: url,
      minimumPixelSize: 128,
      maximumScale: 20000,
    },
  });
 
  // viewer.trackedEntity = entity;
 
  viewer.zoomTo(entity).then(() => {
    camera.frustum = ortho;
    updateOrthoAspect();
  });
 
  scene.requestRender();
}
 
createModel("[http://172.20.20.20:8081/9-1-0.glb"](http://172.20.20.20:8081/9-1-0.glb%22), 100.0);

Issue number and link

#12889

Testing plan

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

@danielzhong danielzhong requested a review from ggetz December 30, 2025 18:02
@github-actions
Copy link

Thank you for the pull request, @danielzhong!

✅ We can confirm we have a CLA on file for you.

@markschlosseratbentley markschlosseratbentley requested review from lilleyse and removed request for lilleyse February 3, 2026 14:37
@markschlosseratbentley
Copy link
Contributor

@lilleyse This has made a bunch of progress, so would you mind taking another pass on it?

cc @danielzhong

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements support for the proposed BENTLEY_materials_line_style glTF extension, enabling CAD-style line visualization with customizable width (in screen pixels) and 16-bit dash patterns. The implementation refactors the existing EXT_mesh_primitive_edge_visibility extension from line primitives to quad-based rendering to support variable line widths, as WebGL doesn't support line widths greater than 1. Additionally, the PR addresses silhouette normal decode issues and extends edge visibility loading to honor material colors and line-string overrides.

Changes:

  • Added support for the BENTLEY_materials_line_style glTF extension with width and pattern properties for lines and edges
  • Refactored EXT_mesh_primitive_edge_visibility from gl_lines to tessellated quads (4 vertices, 2 triangles per edge) to enable variable line width rendering
  • Extended edge visibility to support material color overrides and line strings with cumulative distance attributes for stable pattern scaling

Reviewed changes

Copilot reviewed 20 out of 23 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/sandcastle/gallery/styled-gltf-lines-dev/* New Sandcastle example demonstrating styled glTF lines with custom width and dash patterns
packages/engine/Specs/Scene/Model/*.js Added comprehensive unit tests for line style extension, edge visibility quad rendering, and material color overrides
packages/engine/Source/Shaders/Model/*.glsl Implemented vertex and fragment shader stages for line style rendering, edge expansion, and pattern application
packages/engine/Source/Scene/VertexAttributeSemantic.js Added CUMULATIVE_DISTANCE semantic for line pattern distance tracking
packages/engine/Source/Scene/ModelComponents.js Added lineWidth, linePattern, and edgeVisibility properties to Material and Primitive components
packages/engine/Source/Scene/Model/*.js Implemented quad-based edge geometry generation, material pipeline integration, and edge color attribute handling
packages/engine/Source/Scene/GltfLoader.js Added loading logic for BENTLEY_materials_line_style extension and edge visibility line strings
Specs/Data/Models/glTF-2.0/StyledLines/*.gltf Test data files demonstrating the line style extension
CHANGES.md Updated changelog with new features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@markschlosseratbentley
Copy link
Contributor

@jjhembd fyi

@jjhembd
Copy link
Contributor

jjhembd commented Feb 11, 2026

@danielzhong thanks for this PR! It is a fairly big change, so it will take me some time to finish the review. I should have some feedback early next week. Let me know if you need anything sooner.

@javagl
Copy link
Contributor

javagl commented Feb 12, 2026

Admittedly, I'm a bit surprised that debugWireframe still seems to work...

const viewer = new Cesium.Viewer("cesiumContainer", {
  infoBox: false,
  selectionIndicator: false,
  shouldAnimate: true,
});

async function createModel(url, height) {
  const transform = Cesium.Transforms.eastNorthUpToFixedFrame(
    Cesium.Cartesian3.fromDegrees(-75.152408, 39.946975, height)
  );
  const model = await Cesium.Model.fromGltfAsync({
    url: url,
    modelMatrix: transform,

    // TEST debugWireframe
    enableDebugWireframe: true,
    debugWireframe: true
  });
  viewer.scene.primitives.add(model);
  model.readyEvent.addEventListener(() => {
    viewer.scene.camera.flyToBoundingSphere(
      model.boundingSphere, {
      duration: 0
    });
  });
}

createModel("http://localhost:8003/line.point_style/7-1-0.glb", 20.0);

I haven't looked at the implementation here, but know that debugWireframe does some index juggling, and that could easily have affected the implementation of anything that is related to edge rendering (or relies on indices in any way).

A detail:
The test glTFs both have some validation errors of the form
VALUE_NOT_IN_RANGE | Value 1 is out of range. | /bufferViews/5/byteStride

@danielzhong
Copy link
Contributor Author

Admittedly, I'm a bit surprised that debugWireframe still seems to work...

const viewer = new Cesium.Viewer("cesiumContainer", {
  infoBox: false,
  selectionIndicator: false,
  shouldAnimate: true,
});

async function createModel(url, height) {
  const transform = Cesium.Transforms.eastNorthUpToFixedFrame(
    Cesium.Cartesian3.fromDegrees(-75.152408, 39.946975, height)
  );
  const model = await Cesium.Model.fromGltfAsync({
    url: url,
    modelMatrix: transform,

    // TEST debugWireframe
    enableDebugWireframe: true,
    debugWireframe: true
  });
  viewer.scene.primitives.add(model);
  model.readyEvent.addEventListener(() => {
    viewer.scene.camera.flyToBoundingSphere(
      model.boundingSphere, {
      duration: 0
    });
  });
}

createModel("http://localhost:8003/line.point_style/7-1-0.glb", 20.0);

I haven't looked at the implementation here, but know that debugWireframe does some index juggling, and that could easily have affected the implementation of anything that is related to edge rendering (or relies on indices in any way).

A detail: The test glTFs both have some validation errors of the form VALUE_NOT_IN_RANGE | Value 1 is out of range. | /bufferViews/5/byteStride

@javagl Could you let me know which tool you used for validation and which files show this error? I ran them through https://github.khronos.org/glTF-Validator/, and they are reported as valid, I don’t see any errors at the moment.

@javagl
Copy link
Contributor

javagl commented Feb 12, 2026

Could you let me know which tool you used for validation and which files show this error?

Apologies for the false alert here: I ran the test with the files that had been attached in the first comment (line_style.zip and line&point_style.zip), and they still contained the errors.

But I now checked the ones that are part of this PR, and they do not cause any errors 👍

@@ -0,0 +1,36 @@
<!doctype html>
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @danielzhong, I am looking at this now and plan to give more feedback tomorrow.

One initial comment: This Sandcastle HTML doesn't follow the pattern of the new Sandcastle files. Can we replace this with a more minimal file like the others? Most of this boilerplate should be taken care of by the sandcastle build step.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

@jjhembd
Copy link
Contributor

jjhembd commented Feb 25, 2026

Hi @danielzhong, I am looking through this in a little more depth. Can you help me with a couple big-picture questions here?

Change to existing edge rendering examples

The PR description mentions this:

This PR updates the EXT_mesh_primitive_edge_visibility gltf extension by switching from gl_lines to tessellated quads rendered as gl_triangles, enabling line-width support.

The testing plan doesn't mention how to verify that old examples using EXT_mesh_primitive_edge_visibility are still working. What examples can I look at, to confirm we didn't break anything for users of the old extension?

Interaction between defines

Intuitively, I would expect a hierarchy something like this:

#if (defined(EDGES_VISIBLE))
    // Process attributes & uniforms related to edge rendering
    // ...
    #if (defined(VARIABLE_EDGE_WIDTH))
        // Process attributes & uniforms related to variable edge width
    #endif
    #if (defined(EDGE_LINE_PATTERN))
        // Process attributes & uniforms related to line patterns like dashes etc
    #endif
#endif

But inside ModelFS I see this:

 #if defined(HAS_LINE_PATTERN) && !defined(HAS_EDGE_VISIBILITY)
    const float maskLength = 16.0;
    // More line pattern processing

Why would we bother with a line pattern if the edges are not visible?

I assume HAS_EDGE_VISIBILITY is used for a completely separate edge rendering path. If that is true, perhaps the define should be renamed to something like USE_EDGE_VISIBILITY_EXTENSION, to make these ifdefs more readable. Even then, are we assuming the EDGE_VISIBILITY extension would override a line pattern?

@danielzhong
Copy link
Contributor Author

danielzhong commented Feb 25, 2026

Hi @danielzhong, I am looking through this in a little more depth. Can you help me with a couple big-picture questions here?

Change to existing edge rendering examples

The PR description mentions this:

This PR updates the EXT_mesh_primitive_edge_visibility gltf extension by switching from gl_lines to tessellated quads rendered as gl_triangles, enabling line-width support.

The testing plan doesn't mention how to verify that old examples using EXT_mesh_primitive_edge_visibility are still working. What examples can I look at, to confirm we didn't break anything for users of the old extension?

Interaction between defines

Intuitively, I would expect a hierarchy something like this:

#if (defined(EDGES_VISIBLE))
    // Process attributes & uniforms related to edge rendering
    // ...
    #if (defined(VARIABLE_EDGE_WIDTH))
        // Process attributes & uniforms related to variable edge width
    #endif
    #if (defined(EDGE_LINE_PATTERN))
        // Process attributes & uniforms related to line patterns like dashes etc
    #endif
#endif

But inside ModelFS I see this:

 #if defined(HAS_LINE_PATTERN) && !defined(HAS_EDGE_VISIBILITY)
    const float maskLength = 16.0;
    // More line pattern processing

Why would we bother with a line pattern if the edges are not visible?

I assume HAS_EDGE_VISIBILITY is used for a completely separate edge rendering path. If that is true, perhaps the define should be renamed to something like USE_EDGE_VISIBILITY_EXTENSION, to make these ifdefs more readable. Even then, are we assuming the EDGE_VISIBILITY extension would override a line pattern?

Hi, thanks for taking the time to review this.

  1. There is an existing EdgeVisibility test file under:
    \Specs\Data\Models\glTF-2.0\EdgeVisibility\glTF-Binary

This test file has not been changed, and the previous unit tests were not modified either. I’ve verified that it still renders correctly. In addition, I’ve included another demo for edge visibility in the PR description.

There is no distinction between "old" and "new" EXT_mesh_primitive_edge_visibility glTF files, the file format is unchanged. The same .glb works with both the old GL_LINES path and the new quad-based path (Required to support the line style extension, including line width and line pattern features.) The existing EdgeVisibility.glb test file and its specs in GltfLoaderSpec.js already serve as the backward compatibility test.

  1. BENTLEY_materials_line_style is a material extension that applies independently to any line primitive. A plain GL_LINES mesh with no EXT_mesh_primitive_edge_visibility can still have a dash pattern (Example) - that case is handled in ModelFS.glsl.
  • No Edge Visibility -> pattern run in ModelFS.glsl
  • Has Edge Visibility -> Run in EdgeVisibilityStageFS.glsl

The !defined(HAS_EDGE_VISIBILITY) guard simply prevents double-processing when both extensions are present, since edge visibility's own fragment stage handles the pattern internally.

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.

6 participants