diff --git a/Apps/SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb b/Apps/SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb
new file mode 100644
index 000000000000..0cac041db84b
Binary files /dev/null and b/Apps/SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb differ
diff --git a/Apps/SampleData/models/PrimitiveRestart/README.md b/Apps/SampleData/models/PrimitiveRestart/README.md
new file mode 100644
index 000000000000..008f0e3b0055
--- /dev/null
+++ b/Apps/SampleData/models/PrimitiveRestart/README.md
@@ -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).
diff --git a/Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-loop.glb b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-loop.glb
new file mode 100644
index 000000000000..69aac5e33f66
Binary files /dev/null and b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-loop.glb differ
diff --git a/Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-strip.glb b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-strip.glb
new file mode 100644
index 000000000000..18b242dfb856
Binary files /dev/null and b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-strip.glb differ
diff --git a/Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-fan.glb b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-fan.glb
new file mode 100644
index 000000000000..918827707907
Binary files /dev/null and b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-fan.glb differ
diff --git a/Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-strip.glb b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-strip.glb
new file mode 100644
index 000000000000..3ea28a4c93b8
Binary files /dev/null and b/Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-strip.glb differ
diff --git a/CHANGES.md b/CHANGES.md
index 1de2345c26db..416ee7b31768 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -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:
diff --git a/LICENSE.md b/LICENSE.md
index abfcfb8b0bfe..8e426a08213c 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -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
diff --git a/Specs/Data/Models/glTF-2.0/MeshPrimitiveRestartKHR/glTF-Embedded/MeshPrimitiveRestartKHR.gltf b/Specs/Data/Models/glTF-2.0/MeshPrimitiveRestartKHR/glTF-Embedded/MeshPrimitiveRestartKHR.gltf
new file mode 100644
index 000000000000..81998d58e8e6
--- /dev/null
+++ b/Specs/Data/Models/glTF-2.0/MeshPrimitiveRestartKHR/glTF-Embedded/MeshPrimitiveRestartKHR.gltf
@@ -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
+}
diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js
index a5d860a32ca6..33b3dea6bd84 100644
--- a/packages/engine/Source/Scene/Model/Model.js
+++ b/packages/engine/Source/Scene/Model/Model.js
@@ -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}
*
*
- * {@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}
*
*
* {@link https://github.com/KhronosGroup/glTF/pull/2479|EXT_mesh_primitive_edge_visibility}
@@ -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}
*
*
+ * {@link https://github.com/KhronosGroup/glTF/pull/2569|KHR_mesh_primitive_restart}
+ * (requires a WebGL 2 context; behavior on WebGL 1 is undefined)
+ *
+ *
* {@link https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization|KHR_mesh_quantization}
*
*
diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js
index eeb885ec12a6..9bb9e89c0a26 100644
--- a/packages/engine/Specs/Scene/GltfLoaderSpec.js
+++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js
@@ -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 =
@@ -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;
diff --git a/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/index.html b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/index.html
new file mode 100644
index 000000000000..f30fef07b9d4
--- /dev/null
+++ b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/index.html
@@ -0,0 +1,6 @@
+
+
+Loading...
+
diff --git a/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js
new file mode 100644
index 000000000000..a50d3b144fd9
--- /dev/null
+++ b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js
@@ -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",
+ 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]);
diff --git a/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/sandcastle.yaml b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/sandcastle.yaml
new file mode 100644
index 000000000000..3d13a277d65f
--- /dev/null
+++ b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/sandcastle.yaml
@@ -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