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