diff --git a/CHANGES.md b/CHANGES.md index 1e6286c8375c..1708c174f139 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Fix JSDoc for SkyBox.show to correctly declare it as a prototype property for TypeScript compatibility. [#13357](https://github.com/CesiumGS/cesium/pull/13357) - Fixed lighting affecting `EquirectangularPanorama`. [#13369](https://github.com/CesiumGS/cesium/pull/13369) +- Fixed a `DeveloperError` thrown when loading 3D tiles containing degenerate (zero-area) triangles with edge visibility data. [#13421](https://github.com/CesiumGS/cesium/pull/13421) ## 1.140 - 2026-04-01 diff --git a/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js b/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js index eae237cec6c8..163a68cc0729 100644 --- a/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js +++ b/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js @@ -5,9 +5,7 @@ import defined from "../../Core/defined.js"; import IndexDatatype from "../../Core/IndexDatatype.js"; import ComponentDatatype from "../../Core/ComponentDatatype.js"; import PrimitiveType from "../../Core/PrimitiveType.js"; -import Cartesian2 from "../../Core/Cartesian2.js"; import Cartesian3 from "../../Core/Cartesian3.js"; -import AttributeCompression from "../../Core/AttributeCompression.js"; import Pass from "../../Renderer/Pass.js"; import ShaderDestination from "../../Renderer/ShaderDestination.js"; import EdgeVisibilityStageFS from "../../Shaders/Model/EdgeVisibilityStageFS.js"; @@ -358,7 +356,22 @@ function buildTriangleAdjacency(primitive) { Cartesian3.subtract(scratchP1, scratchP0, scratchE1); Cartesian3.subtract(scratchP2, scratchP0, scratchE2); Cartesian3.cross(scratchE1, scratchE2, scratchCross); - Cartesian3.normalize(scratchCross, scratchCross); + + // Skip degenerate triangles: a zero or non-finite cross product cannot be + // safely normalized and would produce invalid face normals. + const crossMagnitudeSquared = Cartesian3.magnitudeSquared(scratchCross); + if ( + crossMagnitudeSquared === 0.0 || + !Number.isFinite(crossMagnitudeSquared) + ) { + continue; + } + + Cartesian3.multiplyByScalar( + scratchCross, + 1.0 / Math.sqrt(crossMagnitudeSquared), + scratchCross, + ); faceNormals[base] = scratchCross.x; faceNormals[base + 1] = scratchCross.y; @@ -397,58 +410,34 @@ function generateEdgeFaceNormals( const hasGLBSilhouetteNormals = defined(edgeVisibility) && defined(edgeVisibility.silhouetteNormals); - let silhouetteNormalsUint32 = null; - const scratchDecodedA = new Cartesian3(); - const scratchDecodedB = new Cartesian3(); - if (hasGLBSilhouetteNormals) { - // GLB stores VEC3 BYTE as normalized normal vectors (signed bytes). - // Decode from signed bytes to normalized vectors, then re-encode to 16-bit octahedral format. - const decodeSignedByte = (val) => 2 * ((val + 128) / 255) - 1; + // Decode GLB silhouette normals from signed-byte VEC3 to a flat Float32Array + // (3 floats per normal) to avoid per-normal object allocation and GC pressure. + let silhouetteNormalsFloat = null; - // Re-encode each VEC3 BYTE to 16-bit oct-encoded normal using AttributeCompression - const uint16Normals = new Uint16Array( - edgeVisibility.silhouetteNormals.length, - ); + if (hasGLBSilhouetteNormals) { + const raw = edgeVisibility.silhouetteNormals; + silhouetteNormalsFloat = new Float32Array(raw.length * 3); const scratchNormal = new Cartesian3(); - const scratchEncoded = new Cartesian2(); - for (let i = 0; i < edgeVisibility.silhouetteNormals.length; i++) { - const vec3 = edgeVisibility.silhouetteNormals[i]; + for (let i = 0; i < raw.length; i++) { + const vec3 = raw[i]; + // Signed byte → float: map [-128,127] → [-1,1] via [0,255] + scratchNormal.x = 2 * ((vec3.x + 128) / 255) - 1; + scratchNormal.y = 2 * ((vec3.y + 128) / 255) - 1; + scratchNormal.z = 2 * ((vec3.z + 128) / 255) - 1; - // Decode from signed byte to normal vector - scratchNormal.x = decodeSignedByte(vec3.x); - scratchNormal.y = decodeSignedByte(vec3.y); - scratchNormal.z = decodeSignedByte(vec3.z); - - // Normalize to unit vector - const magnitude = Cartesian3.magnitude(scratchNormal); - if (magnitude > 0) { + if (Cartesian3.magnitudeSquared(scratchNormal) > 0) { Cartesian3.normalize(scratchNormal, scratchNormal); } else { - // Handle zero vector - use default up vector scratchNormal.x = 0; scratchNormal.y = 0; scratchNormal.z = 1; } - // Use Cesium's octahedral encoding (returns 0-255 integers) - AttributeCompression.octEncodeInRange(scratchNormal, 255, scratchEncoded); - - // Convert to 16-bit integer: (y << 8) | x - const byteX = scratchEncoded.x & 0xff; - const byteY = scratchEncoded.y & 0xff; - uint16Normals[i] = (byteY << 8) | byteX; - } - - // Pack pairs into Uint32Array (little-endian: normalA|normalB<<16) - const numPairs = Math.floor(uint16Normals.length / 2); - silhouetteNormalsUint32 = new Uint32Array(numPairs); - - for (let i = 0; i < numPairs; i++) { - const normalA = uint16Normals[i * 2]; - const normalB = uint16Normals[i * 2 + 1]; - silhouetteNormalsUint32[i] = normalA | (normalB << 16); + silhouetteNormalsFloat[i * 3] = scratchNormal.x; + silhouetteNormalsFloat[i * 3 + 1] = scratchNormal.y; + silhouetteNormalsFloat[i * 3 + 2] = scratchNormal.z; } } @@ -466,37 +455,21 @@ function generateEdgeFaceNormals( // Use GLB silhouetteNormals for type=1 (SILHOUETTE) edges if available if ( hasGLBSilhouetteNormals && - silhouetteNormalsUint32 && + silhouetteNormalsFloat && edgeType === 1 && mateVertexIndex >= 0 ) { - // Each OctEncodedNormalPair is stored as one Uint32 value - // Uint32 contains 4 bytes: [byte0, byte1, byte2, byte3] - // normalA = byte0 | (byte1 << 8) - little endian - // normalB = byte2 | (byte3 << 8) - little endian - - if (mateVertexIndex < silhouetteNormalsUint32.length) { - const uint32Value = silhouetteNormalsUint32[mateVertexIndex]; - - // Uint32 contains 4 bytes: [xA, yA, xB, yB] - // Extract and decode using octDecode (rangeMax=255) - AttributeCompression.octDecode( - uint32Value & 0xff, // xA - (uint32Value >> 8) & 0xff, // yA - scratchDecodedA, - ); - AttributeCompression.octDecode( - (uint32Value >> 16) & 0xff, // xB - (uint32Value >> 24) & 0xff, // yB - scratchDecodedB, - ); - - nAx = scratchDecodedA.x; - nAy = scratchDecodedA.y; - nAz = scratchDecodedA.z; - nBx = scratchDecodedB.x; - nBy = scratchDecodedB.y; - nBz = scratchDecodedB.z; + const pairBase = mateVertexIndex * 2; + if ((pairBase + 1) * 3 + 2 < silhouetteNormalsFloat.length) { + const baseA = pairBase * 3; + const baseB = (pairBase + 1) * 3; + + nAx = silhouetteNormalsFloat[baseA]; + nAy = silhouetteNormalsFloat[baseA + 1]; + nAz = silhouetteNormalsFloat[baseA + 2]; + nBx = silhouetteNormalsFloat[baseB]; + nBy = silhouetteNormalsFloat[baseB + 1]; + nBz = silhouetteNormalsFloat[baseB + 2]; usedGLBNormals = true; } diff --git a/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js b/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js index 2ef32f67bde6..2fb1e83916b1 100644 --- a/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js +++ b/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js @@ -563,6 +563,66 @@ describe("Scene/Model/EdgeVisibilityPipelineStage", function () { expect(positionBuffer.sizeInBytes).toBe(12 * 3 * 4); }); + it("does not throw for degenerate (zero-area) triangles", function () { + // A degenerate triangle has two identical vertices, so its cross product is + // the zero vector. Normalizing it used to throw a DeveloperError. + const positions = new Float32Array([ + 0.0, + 0.0, + 0.0, // vertex 0 + 1.0, + 0.0, + 0.0, // vertex 1 + 1.0, + 0.0, + 0.0, // vertex 2 == vertex 1 (degenerate) + ]); + const indices = new Uint16Array([0, 1, 2]); + const visibilityBuffer = new Uint8Array([0b00101010]); // 3 HARD edges + + const primitive = { + attributes: [ + { + semantic: VertexAttributeSemantic.POSITION, + componentDatatype: ComponentDatatype.FLOAT, + count: 3, + typedArray: positions, + buffer: Buffer.createVertexBuffer({ + context: context, + typedArray: positions, + usage: BufferUsage.STATIC_DRAW, + }), + strideInBytes: 12, + offsetInBytes: 0, + }, + ], + indices: { + indexDatatype: IndexDatatype.UNSIGNED_SHORT, + count: 3, + typedArray: indices, + buffer: Buffer.createIndexBuffer({ + context: context, + typedArray: indices, + usage: BufferUsage.STATIC_DRAW, + indexDatatype: IndexDatatype.UNSIGNED_SHORT, + }), + }, + mode: PrimitiveType.TRIANGLES, + edgeVisibility: { visibility: visibilityBuffer }, + }; + + const renderResources = createMockRenderResources(primitive); + const frameState = createMockFrameState(); + + expect(function () { + EdgeVisibilityPipelineStage.process( + renderResources, + primitive, + frameState, + ); + }).not.toThrow(); + }); + it("validates BENTLEY_materials_line_style support", function () { const primitive = createTestPrimitive(); const renderResources = createMockRenderResources(primitive);