From 44e5be8ce3c9b672edd78ff250b1e40ea87edd3d Mon Sep 17 00:00:00 2001 From: Mark Schlosser <47000437+markschlosseratbentley@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:39:59 -0400 Subject: [PATCH 1/4] Clarify documentation for KHR_mesh_primitive_restart, add unit tests, and add a Sandcastle for visual verification of support. --- .../PrimitiveRestartLineStrip.glb | Bin 0 -> 11472 bytes .../primitive-restart-line-loop.glb | Bin 0 -> 2052 bytes .../primitive-restart-line-strip.glb | Bin 0 -> 2052 bytes .../primitive-restart-triangle-fan.glb | Bin 0 -> 2180 bytes .../primitive-restart-triangle-strip.glb | Bin 0 -> 2144 bytes .../MeshPrimitiveRestartKHR.gltf | 106 +++++++++++++++ packages/engine/Source/Scene/Model/Model.js | 6 +- packages/engine/Specs/Scene/GltfLoaderSpec.js | 43 ++++++ .../khr-mesh-primitive-restart-dev/index.html | 6 + .../khr-mesh-primitive-restart-dev/main.js | 126 ++++++++++++++++++ .../sandcastle.yaml | 5 + 11 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 Apps/SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb create mode 100644 Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-loop.glb create mode 100644 Apps/SampleData/models/PrimitiveRestart/primitive-restart-line-strip.glb create mode 100644 Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-fan.glb create mode 100644 Apps/SampleData/models/PrimitiveRestart/primitive-restart-triangle-strip.glb create mode 100644 Specs/Data/Models/glTF-2.0/MeshPrimitiveRestartKHR/glTF-Embedded/MeshPrimitiveRestartKHR.gltf create mode 100644 packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/index.html create mode 100644 packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js create mode 100644 packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/sandcastle.yaml diff --git a/Apps/SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb b/Apps/SampleData/models/PrimitiveRestart/PrimitiveRestartLineStrip.glb new file mode 100644 index 0000000000000000000000000000000000000000..0cac041db84b26acf5b8c9a8382a8b706879333f GIT binary patch literal 11472 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69aac5e33f666ed691c32a709efbeb56ff4a02bb GIT binary patch literal 2052 zcmZ`%O>Y}j6g{*7T4*WX-ydQ44wJ+lk0%~O;wGV`Zd1ET(r%(EjO}M)-*ca zd78>jPhO|4IxA=RKb*KYgu#cr>)7SJ10MEa&7&c`C)2=LiBacprl zom}k&ZJ(3W?@LAztqDaJ`W-*^H`LLKY0p|O=Hr%es=8EOE>|n$67hGsYyj=AX{C97 z(C{f|y^2ryT!;PaoyhQLMo2d00V85(w3;)Zo@c<6vB@o&{{ zh85G1_~yYMhJXHAOY#0oU!^^_?tCXP&;I?-b%~ktFL7DoyBBWEXss_Ue>S7F2DjSN zTI=qGv4PeaI9CQ*i?PsQ)?w2vV)0-o0BWFC%``3{^Od&>|x{D*Alabmk!^V<{pKAVyt=w zjMc2f>>+VVoOs9`0Pm}{*axk}{%9@sO=~GEwN#JfRI?H*&ZI6qOtg4kt;IfQE%rxi zv2R*SVX38hB&V8{SaC|Ma!7BMcu3w#tg<9A`$KObG5bbulwx|jw1?hQVxFb9mRMz5 zVwFRQRc0hsSyDYRV@W;9UW&Dr%4#~RwV0Fgmei9RN~|&?vC5LfDq|9>?4?+1agW}M zXSEh{YAxUa?7?1);6Xfuhp`X$?(N4TcoYY45Rc&y9>)_@dlFCKX*`2x@f@DV3wV)g zFX3gpf>-exM)5ifyg{`$@fP03J9roGVGQrXq}n(pFo_JZu#kg|Jk=abp#T>}e1H!z kjTx$ya2T_g!#qC15gf(GR6B+;j^hLta1s?%af;shUpx$bbpQYW literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..18b242dfb8562b2824d0e63c47fc3da80d95f65d GIT binary patch literal 2052 zcmZ`%O>Y}j6g{*7T4*WX-ydQ44wJ+lk0%~O;wGV`Zd1ET(r%(EjO}M%jpCNk5`hT4xEmTpqeZRb%b^!U&2HEk-LNO@iHu)k*E@v|n$67hGsYyi2gX{C97 z(C{f|kumr#~Hu28C(Y8<MNo(Ey{z^$}{nGrhq_yTgIbG6PSAU-@ zX)VTDi+i*d&uT5^)LMy$>|o;I=Hv?96JQ^1|8Y%X_ONm7Yl+#zONZ}FbC1G5F;+bT z#%fk#_K-LwPCVoefbXld*axk}{%9@sO=~GEwN#JfRI?H*&ZI6qOtkpET8n+qTI`S3 zV&Alu!ct51NKQ2?vEr0i<&fSi@sPZgSY=6K_J`g=V)l*RD8=-4X%D@r#5_xHEwReB z#43jptISBOvZQ)s#*%uHy%cLLmDO}sYcVI~EvY9tlvrg(VwEL{RmLP%*-Nq3;vRi3 zp4D2+skMLyum^iFf(P*s9>zZ0ySE>Y;87gFK|F>-cpOhq?MXa^r|}G)#dCNbFW^P0 zy@Z$X3SPx)7{%)_@CMc1#9Me9@8Dg$hcUbllWOCbz$7xr!a@!<@>Fv$g#ug@@c}-> lG-jw)!ePu}4)gd3M{pD$Q|%baIF1uoz)4h4#VLC0e*r;eeRco< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9188277079075b1d1279f25fb167aac025e25f46 GIT binary patch literal 2180 zcmZ`%O>Y}j6g{*B!bd6PyL_5QTcDj7f7M@Ylcu31X_L6vZU9wPG;yAtk?e7p8HX0j z2njY_@B>((c2#x5FQ|jWiY>o^6+&W->Em=D6ymQaJ@67FVn-}*1_;w2D z?guV4YAZwKhM|sBb*MVp*MS@L165TWl1?`R*ALtMpqJV%qzmbca@4jCLa*-?``#c?MF>RpUUQnOb&c%SIIc4=iZ>IY&xIM zRp?hJ6wAeOxl(k9$`(tRY^hi&=gOr@vEXE`lis7o*QWOgH=y|p{mqy z{brO?kNEum-f`)Y_B+w0s%A4~ zJ6t0QybVe)n{!m;1|9w|`9h_b<6$3SHmOehD8hf0=SRtGb}kAcQRhlUzVEq_4m`IT zE4J2yWgWTQuGgyf2c1oyzA`Pz$Wi)6q+KB;2s9(CbL zBEuG~aLmem#;DDO@tI72HPjolPJ6zC0C0YB1>gL1Vh8w|{y=Z)^v;*REgS6I)jPNUNE`h7$?p>U%kWO38UJuEsb&7> zgWo6mA8*}Fd<3(3e(p>sjeb_`YD$j73AW<6X65@npU9Vjscm zFB+P8R%&VHS@EPd&Pp$OdxB}^L_=96wakkr>!la_2xfoL#5r@yan48l1$sZgIggLu zF_?1}%z282b8a6#bCmU#r}qO^Z?N?-m~*CDgX5f84R8+P$@xex&P_1qEE>*P`dZ$2 zTD@TQG1&SW9OoSO4PdV{Tx!PehkyU+BU;+!degE=4i zE(UXMf;mso*nKt{=4m$)J?%!~Bbaj*jooLX;T*)%?xEp1H|fiHipK7<)bgGudg;YJ zg4tg*cAt%&a}ZC?M|yE?f;mso*nKv&%!?=Mr5F1MW`EHDk6<76;{YDTV|W}>cmfX| zJc+09G!Eh!9Ky3WjOXw?u`l37yo8tW3Xb4ayoT5D2C;AAEu`=^6yCv69K&(EOYD1i zA181U4yKXD2go2xYz}!8a0*3~P(}q+oF?`R&SD0$IEOi$$2>m71!6DaBP?JMmv9+N ZSjGx!#9qP2sH1@W326o!A20AXLlzR1Ha#%4w%%a(vxwv%8djuj-?K&4X7SZ>KfNh32ea*}ch zRK*=H!6j9>$`b^=1lM^5t|_XLb9x&WB)S-#{<{1Bx{pS^e*3}+0M}0gYbSw=t;Xh& z6Q-%ioZ6Ao6R}9bY?wGTr$p1&CQ z-h8=Ib$ucwBA&nCS1Nw6u;6;vXn925tFvX-lf&Inw=0tENZfF06t;US6OC?H-o+cc z_AIT{<&EX$Lr)Xrr&*+cV|o66^GKi1tGlZ4|J`cqhKPIFzEktPMSZwdmPC8h9lzu{ zS(x;==|Q<#De+++5ti?^c~$D+@$JFbo=u;N8evzL7AibH2s4pHVPCB{*i9~rEbRBA zPIEZw?Z@;B(hD$l9dR=gG5^3kb}p?ockIrcQQVLCJsFQ>qDc3Jj!U9JltqVn7-n?Q z-BHHFBj-w^wccKDY*L>ne=y_@%;yJ^?Tg~Qh~6S<&G;H;$FxdC56Hqh38o!-Jrykk^q-$^pXD)gQ!8Fv%DJkU-?;mW<)+p-mQ#+bf%=p)_%m~- zIF*Hb)UAto?qzPg%#F8x%v-cI=Np&K(Oy{B%Jskg%)PKK=DC--aw%@SurB8LePxbx z=^X8ySl6%9qOGYv*2O&cGFL9ejdx;Q%yX~IkuII1z4;CEY{kr&zfH|E#;dsT0>;JM z)N1{0>C!pcJF)(|zpHs=oh%s_b5m0vs8%k;jd!wST+B_a)~j5dV|;bx0q!-<*qT_~ zxR{$-aph9ncu_Yl=BCy;(xr2>w{Bhc8e6s|t#vWaz08f5x$)Mmi`VB~nWMcjM|&sM z_4gmY*_!%eUCeVYbLCRpcqi7yJom~R>C!pc`xs8(Bu?RRJb@>18c*T={ipE^p2c%` z9xvcUyo8tW3ei__2Cw0Dyn#3I7T(4?c$esVcpnAKz`+Oj5Fg=Vd_we7e1;-Ehl^Rv z;R|^1iIxzcjCoYBfJIbM! *
  • - * {@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; on WebGL 1, restart indices are not interpreted and affected primitives will render incorrectly) + *
  • + *
  • * {@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..9d54ddcf22a2 --- /dev/null +++ b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js @@ -0,0 +1,126 @@ +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 and observe lack of rendering support. +// +// 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)", + 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 From 61be2bc3155067f85c571fe61b9e6c4fed211efd Mon Sep 17 00:00:00 2001 From: Mark Schlosser <47000437+markschlosseratbentley@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:21:20 -0400 Subject: [PATCH 2/4] clarify undefined behavior of KHR_mesh_primitive_restart on webgl1 --- packages/engine/Source/Scene/Model/Model.js | 2 +- .../gallery/khr-mesh-primitive-restart-dev/main.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/engine/Source/Scene/Model/Model.js b/packages/engine/Source/Scene/Model/Model.js index 52b4f70cf9c6..33b3dea6bd84 100644 --- a/packages/engine/Source/Scene/Model/Model.js +++ b/packages/engine/Source/Scene/Model/Model.js @@ -106,7 +106,7 @@ import ModelImagery from "./ModelImagery.js"; *
  • *
  • * {@link https://github.com/KhronosGroup/glTF/pull/2569|KHR_mesh_primitive_restart} - * (requires a WebGL 2 context; on WebGL 1, restart indices are not interpreted and affected primitives will render incorrectly) + * (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/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js index 9d54ddcf22a2..a50d3b144fd9 100644 --- a/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js +++ b/packages/sandcastle/gallery/khr-mesh-primitive-restart-dev/main.js @@ -8,7 +8,8 @@ import Sandcastle from "Sandcastle"; // 0xFFFFFFFF) to begin a new primitive. // // Requires WebGL 2 (PRIMITIVE_RESTART_FIXED_INDEX). Use the context dropdown -// to force a WebGL 1 context and observe lack of rendering support. +// 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 @@ -115,7 +116,7 @@ Sandcastle.addToolbarMenu([ }, }, { - text: "WebGL 1 (restart unsupported)", + text: "WebGL 1 (restart unsupported; behavior varies)", onselect: () => { createViewer(true); loadModel(currentEntry); From cac5b8fd18917e95481dcf944d8067628a694611 Mon Sep 17 00:00:00 2001 From: Mark Schlosser <47000437+markschlosseratbentley@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:36:31 -0400 Subject: [PATCH 3/4] KHR_mesh_primitive_restart CHANGES.md entry --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) 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: From 283dc26d0369ddb6444414f98ea438504283fe01 Mon Sep 17 00:00:00 2001 From: Mark Schlosser <47000437+markschlosseratbentley@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:20:08 -0400 Subject: [PATCH 4/4] KHR_mesh_primitive_restart model licensing --- Apps/SampleData/models/PrimitiveRestart/README.md | 9 +++++++++ LICENSE.md | 6 ++++++ 2 files changed, 15 insertions(+) create mode 100644 Apps/SampleData/models/PrimitiveRestart/README.md 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/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