Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
9 changes: 9 additions & 0 deletions Apps/SampleData/models/PrimitiveRestart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# PrimitiveRestart

Sample models demonstrating the [`KHR_mesh_primitive_restart`](https://github.com/KhronosGroup/glTF/pull/2569) glTF extension. Each model contains a single mesh primitive batching multiple topological primitives (line strips, line loops, triangle strips, or triangle fans) separated by inline restart index values.

## License Information

Created by Don McCurdy as test data for the `KHR_mesh_primitive_restart` extension proposal ([KhronosGroup/glTF#2569](https://github.com/KhronosGroup/glTF/pull/2569)).

Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). See also the `KHR_mesh_primitive_restart sample models` entry in [LICENSE.md](../../../../LICENSE.md).
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- Added `Texture.defaultColor` static property to allow customizing the default placeholder texture color. [#13597](https://github.com/CesiumGS/cesium/pull/13597)
- Added support for draping clamped vector tile polylines onto terrain, with screen-space-constant line width and per-feature styling via `Cesium3DTileStyle`. [#13577](https://github.com/CesiumGS/cesium/pull/13577)
- Added support for the [`KHR_mesh_primitive_restart`](https://github.com/KhronosGroup/glTF/pull/2569) glTF extension. [#13634](https://github.com/CesiumGS/cesium/pull/13634)

#### Fixes :wrench:

Expand Down
6 changes: 6 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,12 @@ Creative Commons Attribution 4.0 International
(c) copyright 2019, Dr Edward Alan Lockhart
https://sketchfab.com/3d-models/parc-lead-mine-4759a23abbff454c8c682ff9b02ba111

### KHR_mesh_primitive_restart sample models

Creative Commons Attribution 4.0 International
(c) copyright 2026, Don McCurdy
https://github.com/KhronosGroup/glTF/pull/2569

### GitHub logo

https://github.com/logos
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"asset": {
"version": "2.0"
},
"extensionsUsed": [
"KHR_mesh_primitive_restart",
"BENTLEY_materials_line_style"
],
"extensionsRequired": [
"KHR_mesh_primitive_restart"
],
"accessors": [
{
"name": "Positions",
"bufferView": 0,
"byteOffset": 0,
"componentType": 5126,
"count": 7,
"type": "VEC3",
"min": [0.0, 0.0, 0.0],
"max": [2.0, 5.0, 0.0]
},
{
"name": "Cumulative distances",
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 7,
"type": "SCALAR"
},
{
"name": "Indices with inline restart values",
"bufferView": 2,
"byteOffset": 0,
"componentType": 5123,
"count": 9,
"type": "SCALAR"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 84,
"byteStride": 12,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 84,
"byteLength": 28,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 112,
"byteLength": 18,
"target": 34963
}
],
"buffers": [
{
"byteLength": 130,
"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAPwAAQEAAAAAAAAAAAAAAgEAAAAAAAACAPwAAoEAAAAAAAAAAAPMEtT/zBDVAAAAAAPMEtT8AAAAA8wS1PwAAAQACAP//AwAEAP//BQAGAA=="
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorFactor": [1.0, 0.0, 0.0, 1.0]
},
"extensions": {
"BENTLEY_materials_line_style": {
"width": 3,
"pattern": 61680
}
}
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0,
"BENTLEY_materials_line_style:CUMULATIVE_DISTANCE": 1
},
"indices": 2,
"mode": 3,
"material": 0
}
]
}
],
"nodes": [
{
"mesh": 0
}
],
"scenes": [
{
"nodes": [0]
}
],
"scene": 0
}
6 changes: 5 additions & 1 deletion packages/engine/Source/Scene/Model/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import ModelImagery from "./ModelImagery.js";
* {@link https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing|EXT_mesh_gpu_instancing}
* </li>
* <li>
* {@link https://github.com/KhronosGroup/glTF/pull/2514|EXT_mesh_primitive_restart}
* {@link https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_primitive_restart|EXT_mesh_primitive_restart}
* </li>
* <li>
* {@link https://github.com/KhronosGroup/glTF/pull/2479|EXT_mesh_primitive_edge_visibility}
Expand Down Expand Up @@ -105,6 +105,10 @@ import ModelImagery from "./ModelImagery.js";
* {@link https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit/README.md|KHR_materials_unlit}
* </li>
* <li>
* {@link https://github.com/KhronosGroup/glTF/pull/2569|KHR_mesh_primitive_restart}

@markschlosseratbentley markschlosseratbentley Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since this PR does not include any functional changes to CesiumJS, I did not see a need to include an entry in CHANGES.md, leaving it just to inline documentation here. However, I am open to adding a CHANGES.md entry if someone thinks it's necessary and/or desirable. @ggetz?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree it isn't strictly necessary – but I'd be tempted to include it, as many CesiumJS users probably aren't aware of the extension otherwise and might benefit from knowing that it exists and CesiumJS supports it.

Something like:

- Added support for the [`KHR_mesh_primitive_restart`](https://github.com/KhronosGroup/glTF/pull/2569) glTF extension. [#13634](https://github.com/CesiumGS/cesium/pull/13634)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback; I think this makes sense. Added.

* (requires a WebGL 2 context; behavior on WebGL 1 is undefined)
* </li>
* <li>
* {@link https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization|KHR_mesh_quantization}
* </li>
* <li>
Expand Down
43 changes: 43 additions & 0 deletions packages/engine/Specs/Scene/GltfLoaderSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ describe(
"./Data/Models/glTF-2.0/StyledPoints/points-r5-g8-b14-y10.gltf";
const meshPrimitiveRestartTestData =
"./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf";
const meshPrimitiveRestartKhrTestData =
"./Data/Models/glTF-2.0/MeshPrimitiveRestartKHR/glTF-Embedded/MeshPrimitiveRestartKHR.gltf";
const edgeVisibilityTestData =
"./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb";
const edgeVisibilityMaterialTestData =
Expand Down Expand Up @@ -4516,6 +4518,47 @@ describe(
expect(loadedPrimitives.length).toBe(8);
});

it("loads model with KHR_mesh_primitive_restart extension", async function () {
const gltfLoader = await loadGltf(meshPrimitiveRestartKhrTestData, {
loadAttributesAsTypedArray: true,
});
const components = gltfLoader.components;
const primitives = components.nodes[0].primitives;

// Three line strips batched into a single LINE_STRIP primitive.
expect(primitives.length).toBe(1);
const primitive = primitives[0];
expect(primitive.primitiveType).toBe(PrimitiveType.LINE_STRIP);

// The inline 0xFFFF restart values must survive loading untouched.
const indices = primitive.indices;
expect(indices.count).toBe(9);
expect(indices.indexDatatype).toBe(IndexDatatype.UNSIGNED_SHORT);
expect(Array.from(indices.typedArray)).toEqual([
0, 1, 2, 0xffff, 3, 4, 0xffff, 5, 6,
]);
});

it("loads model with KHR_mesh_primitive_restart extension and line styling", async function () {
const gltfLoader = await loadGltf(meshPrimitiveRestartKhrTestData);
const components = gltfLoader.components;
const primitive = components.nodes[0].primitives[0];

// Restart values reference no vertex, so they coexist with per-vertex
// line styling attributes such as CUMULATIVE_DISTANCE.
const cumulativeDistanceAttribute = getAttribute(
primitive.attributes,
VertexAttributeSemantic.CUMULATIVE_DISTANCE,
);
expect(cumulativeDistanceAttribute).toBeDefined();
expect(cumulativeDistanceAttribute.count).toBe(7);

const material = primitive.material;
expect(material.lineStyle).toBeDefined();
expect(material.lineStyle.width).toBe(3);
expect(material.lineStyle.pattern).toBe(61680); // 0xF0F0
});

it("loads model with EXT_mesh_primitive_edge_visibility extension", async function () {
const gltfLoader = await loadGltf(edgeVisibilityTestData);
const components = gltfLoader.components;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
127 changes: 127 additions & 0 deletions packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";

// KHR_mesh_primitive_restart allows many line strips, line loops, triangle
// strips, or triangle fans to be batched into a single glTF primitive. The
// primitive's indices accessor may contain inline "restart" values (the
// maximum value for the accessor's component type (0xFF / 0xFFFF /
// 0xFFFFFFFF) to begin a new primitive.
//
// Requires WebGL 2 (PRIMITIVE_RESTART_FIXED_INDEX). Use the context dropdown
// to force a WebGL 1 context, where results vary by browser/GPU backend:
// nothing, corrupted geometry, or even correct rendering.
//
// The sample models below are from the KHR_mesh_primitive_restart proposal:
// https://github.com/KhronosGroup/glTF/pull/2569
// Each contains a single mesh primitive with multiple topologies separated
// by inline restart values.

let viewer;

function createViewer(requestWebgl1) {
if (Cesium.defined(viewer)) {
viewer.destroy();
}
viewer = new Cesium.Viewer("cesiumContainer", {
contextOptions: { requestWebgl1: requestWebgl1 },
});
}

createViewer(false);

const models = {
"Hilbert Curve (LINE_STRIP, 32-bit indices)": {
url: "../../SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I touched base with @donmccurdy on the inclusion of these test assets in this dev Cesium Sandcastle and he seemed to think it was fine to include them. However, we were not sure how Cesium-included assets are licensed in general. Anyone have any insights?

Context: these test assets were created by @donmccurdy and were provided as test data for this Khronos spec PR: KhronosGroup/glTF#2569

Copy link
Copy Markdown
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 the answer, and am not a lawyer, but would think that unless there is specific license information for a model, the "root" license (Apache 2) would apply.

The main question might therefore be: Under which license would Don like to publish them?

(The point is: I think that there could be some README.md or LICENSE.txt in that folder with whatever license should apply. At least that's done for some of the 'Specs' models, e.g. https://github.com/CesiumGS/cesium/blob/0d0c35fad1cc05ed0560f0d1ec6a9197baaef84e/Specs/Data/Models/glTF-2.0/AnimatedTriangle/README.md )

And now for something completely different: I don't see a difference between the WebGL 1 and 2 versions in terms of primitive restart. (There is a difference, in terms of (anti)aliasing, so I think that they are indeed different contexts, but the geometry looks the same for me...)

Cesium Restart Sample

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@javagl Interesting, thanks for the webgl1 + 2 observation. I tested on Firefox on macOS. What browser/OS are you using?

@donmccurdy donmccurdy Jul 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The main question might therefore be: Under which license would Don like to publish them?

I'm comfortable contributing these under any license conventional for non-code assets in CesiumJS. I might suggest CC BY if there's no prior convention.

If there are no objections, I can just add that license to these files as XMP data (gltf-transform xmp in.glb out.glb), no need for sidecar files.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@markschlosseratbentley This was on Windows 11 with some AMD Radeon (Laptop) card (on Chrome, if that might matter). Of course, it's nothing that affects this PR. It might even be an AMD driver bug 🤪 and things unexpectedly working is a nice "problem" to have.

@donmccurdy No strong preference (details are up to you anyhow) but... 1. for that license information to even be accessible, one needs a library/loader/viewer that supports the extension, and 2. one has to explicitly and dedicatedly look for it, exactly there (which isn't self-evident - I cannt remember having seen license infos embedded in XMP).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small addendum: I haven't looked at the details of the models, but for the other models, there is the expected difference (i.e. the rendering is "broken" on WebGL 1). Only the "hilbert curve" is working. (Are the other ones using non-32-bit indices by any chance? That might be a path to an explanation...)

@markschlosseratbentley markschlosseratbentley Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe the Hilbert curve is the only UNSIGNED_INT model and others are 16-bit, which indeed could explain things. After some research, on ANGLE/D3D11 (working theory):

  • D3D11 strip-cut appears to be always on (0xFFFF/0xFFFFFFFF per index format, no obvious disable API). Found some docs here.
  • WebGL 1 has no primitive restart, so 0xFFFF must act as a normal vertex index, but D3D11 would cut on it. ANGLE therefore rewrites 16-bit buffers containing 0xFFFF as 32-bit, casting each 0xFFFF to 0x0000FFFF (see ANGLE UsePrimitiveRestartWorkaround code here). No cut fires; the value is fetched as vertex 65535 (out of range), resulting in broken rendering...
  • UNSIGNED_INT is excluded from that workaround so 0xFFFFFFFF hits the always-on cut and restart coincidentally works.

Based on this, WebGL 1 behavior is implementation-defined / undefined: macOS Firefox/Chrome render nothing, but Safari renders correctly (Metal probably behaves similar to D3D11 in this regard...).

I will update this PR's docs to clarify webgl1 behavior is "implementation-defined / undefined."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's quite some archeology there 🙂 Given that WebGL 1 support in CesiumJS is ... maybe not completely dropped, but quickly phasing out, and everything that is not running on Windows 3.1 and Mosaic supports WebGL 2 anyhow, it's just a matter of setting the expectations right for people with really old setups.

@donmccurdy donmccurdy Jul 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's also .asset.extras.license, which might be more widely-used than the XMP extension (Sketchfab exports used extras, in particular). But no objection to a separate license file, in that case I'd suggest this one:

https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/LICENSES/CC-BY-4.0.txt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @donmccurdy and @javagl. Based on this licensing discussion, I additionally followed what I found to be an existing repo convention: added a CC-BY 4.0 entry to the root LICENSE.md (matching other Apps/SampleData models like "Perc Lead Mine"). I did this because that LICENSE.md file is clearly distributed and accessible in a release zip of CesiumJS. Providing license alongside distribution for CC-BY 4.0 is required.

I also placed a README.md in the model folder, which hasn't always been done it seems, but makes sense for clarity, I think.

@donmccurdy I credited you in both places, so LMK if you have any issues with this.

heading: 0.0,
pitch: -45.0,
},
"Line Strips": {
url: "../../SampleData/models/PrimitiveRestart/primitive-restart-line-strip.glb",
heading: -90.0,
pitch: 0.0,
},
"Line Loops": {
url: "../../SampleData/models/PrimitiveRestart/primitive-restart-line-loop.glb",
heading: -90.0,
pitch: 0.0,
},
"Triangle Strips": {
url: "../../SampleData/models/PrimitiveRestart/primitive-restart-triangle-strip.glb",
heading: -90.0,
pitch: 0.0,
},
"Triangle Fans": {
url: "../../SampleData/models/PrimitiveRestart/primitive-restart-triangle-fan.glb",
heading: -90.0,
pitch: 0.0,
},
};

const origin = Cesium.Cartesian3.fromDegrees(-75.152408, 39.946975, 50.0);
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
origin,
new Cesium.HeadingPitchRoll(0.0, 0.0, 0.0),
);

let model;
let currentEntry;

async function loadModel(entry) {
currentEntry = entry;
if (Cesium.defined(model)) {
viewer.scene.primitives.remove(model);
model = undefined;
}

try {
model = viewer.scene.primitives.add(
await Cesium.Model.fromGltfAsync({
url: entry.url,
modelMatrix: modelMatrix,
}),
);

model.readyEvent.addEventListener(() => {
const camera = viewer.camera;
const center = model.boundingSphere.center;
const r = 3.0 * Math.max(model.boundingSphere.radius, 1.0);
camera.lookAt(
center,
new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(entry.heading),
Cesium.Math.toRadians(entry.pitch),
r,
),
);
camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
});
} catch (error) {
window.alert(`Error loading model: ${error}`);
}
}

Sandcastle.addToolbarMenu(
Object.entries(models).map(([text, entry]) => ({
text: text,
onselect: () => loadModel(entry),
})),
);

Sandcastle.addToolbarMenu([
{
text: "WebGL 2 (restart supported)",
onselect: () => {
createViewer(false);
loadModel(currentEntry);
},
},
{
text: "WebGL 1 (restart unsupported; behavior varies)",
onselect: () => {
createViewer(true);
loadModel(currentEntry);
},
},
]);

await loadModel(Object.values(models)[0]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
title: KHR_mesh_primitive_restart - Dev
description: Load the KHR_mesh_primitive_restart sample models from the Khronos extension proposal (KhronosGroup/glTF#2569). Each model batches multiple line strips, line loops, triangle strips, or triangle fans into a single glTF primitive using inline restart index values.
labels:
- Development
development: true