From 859a058c60d6362cb2cb19ed8e155abf9facb30f Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:58:37 -0400 Subject: [PATCH 01/12] implmented --- packages/engine/Source/Scene/GltfLoader.js | 124 ++++++-- .../Model/EdgeVisibilityPipelineStage.js | 274 ++++++++++++++---- .../engine/Source/Scene/ModelComponents.js | 8 + .../Shaders/Model/EdgeVisibilityStageFS.glsl | 53 +--- 4 files changed, 341 insertions(+), 118 deletions(-) diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index 81509d9c1a88..f386b8cc7366 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -2007,6 +2007,89 @@ function fetchSpzExtensionFrom(extensions) { return undefined; } +function getEdgeVisibilityMaterialColor(loader, materialIndex) { + if (!defined(materialIndex)) { + return undefined; + } + + const materials = loader.gltfJson.materials; + if ( + !defined(materials) || + materialIndex < 0 || + materialIndex >= materials.length + ) { + return undefined; + } + + const material = materials[materialIndex]; + if (!defined(material)) { + return undefined; + } + + const metallicRoughness = + material.pbrMetallicRoughness ?? Frozen.EMPTY_OBJECT; + const color = fromArray(Cartesian4, metallicRoughness.baseColorFactor); + + if (defined(color)) { + return color; + } + + return new Cartesian4(1.0, 1.0, 1.0, 1.0); +} + +function getLineStringPrimitiveRestartValue(componentType) { + switch (componentType) { + case ComponentDatatype.UNSIGNED_BYTE: + return 255; + case ComponentDatatype.UNSIGNED_SHORT: + return 65535; + case ComponentDatatype.UNSIGNED_INT: + return 4294967295; + default: + throw new RuntimeError( + "EXT_mesh_primitive_edge_visibility lineStrings indices must use unsigned scalar component types.", + ); + } +} + +function loadEdgeVisibilityLineStrings( + loader, + lineStringDefinitions, + defaultMaterialIndex, +) { + if (!defined(lineStringDefinitions) || lineStringDefinitions.length === 0) { + return undefined; + } + + const result = new Array(lineStringDefinitions.length); + for (let i = 0; i < lineStringDefinitions.length; i++) { + const definition = lineStringDefinitions[i] ?? Frozen.EMPTY_OBJECT; + const accessorId = definition.indices; + const accessor = loader.gltfJson.accessors[accessorId]; + + if (!defined(accessor)) { + throw new RuntimeError("Edge visibility line string accessor not found!"); + } + + const indices = loadAccessor(loader, accessor); + const restartIndex = getLineStringPrimitiveRestartValue( + accessor.componentType, + ); + const materialIndex = defined(definition.material) + ? definition.material + : defaultMaterialIndex; + + result[i] = { + indices: indices, + restartIndex: restartIndex, + componentType: accessor.componentType, + materialColor: getEdgeVisibilityMaterialColor(loader, materialIndex), + }; + } + + return result; +} + /** * Load resources associated with a mesh primitive for a glTF node * @param {GltfLoader} loader @@ -2046,37 +2129,44 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { // Edge Visibility const edgeVisibilityExtension = extensions.EXT_mesh_primitive_edge_visibility; - const hasEdgeVisibility = defined(edgeVisibilityExtension); - if (hasEdgeVisibility) { - const visibilityAccessor = - loader.gltfJson.accessors[edgeVisibilityExtension.visibility]; - if (!defined(visibilityAccessor)) { - throw new RuntimeError("Edge visibility accessor not found!"); + if (defined(edgeVisibilityExtension)) { + const edgeVisibility = {}; + + const visibilityAccessorId = edgeVisibilityExtension.visibility; + if (defined(visibilityAccessorId)) { + const visibilityAccessor = + loader.gltfJson.accessors[visibilityAccessorId]; + if (!defined(visibilityAccessor)) { + throw new RuntimeError("Edge visibility accessor not found!"); + } + edgeVisibility.visibility = loadAccessor(loader, visibilityAccessor); } - const visibilityValues = loadAccessor(loader, visibilityAccessor); - primitive.edgeVisibility = { - visibility: visibilityValues, - material: edgeVisibilityExtension.material, - }; - // Load silhouette normals + edgeVisibility.materialColor = getEdgeVisibilityMaterialColor( + loader, + edgeVisibilityExtension.material, + ); + if (defined(edgeVisibilityExtension.silhouetteNormals)) { const silhouetteNormalsAccessor = loader.gltfJson.accessors[edgeVisibilityExtension.silhouetteNormals]; if (defined(silhouetteNormalsAccessor)) { - const silhouetteNormalsValues = loadAccessor( + edgeVisibility.silhouetteNormals = loadAccessor( loader, silhouetteNormalsAccessor, ); - primitive.edgeVisibility.silhouetteNormals = silhouetteNormalsValues; } } - // Load line strings if (defined(edgeVisibilityExtension.lineStrings)) { - primitivePlan.edgeVisibility.lineStrings = - edgeVisibilityExtension.lineStrings; + edgeVisibility.lineStrings = loadEdgeVisibilityLineStrings( + loader, + edgeVisibilityExtension.lineStrings, + edgeVisibilityExtension.material, + ); } + + primitive.edgeVisibility = edgeVisibility; } //support the latest glTF spec and the legacy extension diff --git a/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js b/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js index 768a04f7c6f4..418e3b9922ab 100644 --- a/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js +++ b/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js @@ -112,6 +112,9 @@ EdgeVisibilityPipelineStage.process = function ( "#ifdef HAS_EDGE_FEATURE_ID", " v_featureId_0 = a_edgeFeatureId;", "#endif", + "#ifdef HAS_EDGE_COLOR_OVERRIDE", + " v_edgeColor = a_edgeColor;", + "#endif", " // Transform normals from model space to view space", " v_silhouetteNormalView = czm_normal * a_silhouetteNormal;", " v_faceNormalAView = czm_normal * a_faceNormalA;", @@ -133,6 +136,21 @@ EdgeVisibilityPipelineStage.process = function ( return; } + let edgeColorLocation; + const hasEdgeColorOverride = edgeResult.edgeData.some(function (edge) { + return defined(edge.color); + }); + + if (hasEdgeColorOverride) { + edgeColorLocation = shaderBuilder.addAttribute("vec4", "a_edgeColor"); + shaderBuilder.addVarying("vec4", "v_edgeColor", "flat"); + shaderBuilder.addDefine( + "HAS_EDGE_COLOR_OVERRIDE", + undefined, + ShaderDestination.BOTH, + ); + } + // Generate paired face normals for each unique edge (used to classify silhouette edges in the shader). const edgeFaceNormals = generateEdgeFaceNormals( adjacencyData, @@ -150,6 +168,7 @@ EdgeVisibilityPipelineStage.process = function ( faceNormalALocation, faceNormalBLocation, edgeFeatureIdLocation, + edgeColorLocation, primitive.edgeVisibility, edgeFaceNormals, ); @@ -372,91 +391,165 @@ function generateEdgeFaceNormals(adjacencyData, edgeIndices) { */ function extractVisibleEdges(primitive) { const edgeVisibility = primitive.edgeVisibility; + if (!defined(edgeVisibility)) { + return []; + } + const visibility = edgeVisibility.visibility; const indices = primitive.indices; + const lineStrings = edgeVisibility.lineStrings; - if (!defined(visibility) || !defined(indices)) { + const attributes = primitive.attributes; + const vertexCount = + defined(attributes) && attributes.length > 0 ? attributes[0].count : 0; + + const hasVisibilityData = + defined(visibility) && + defined(indices) && + defined(indices.typedArray) && + indices.typedArray.length > 0; + const hasLineStrings = defined(lineStrings) && lineStrings.length > 0; + + if (!hasVisibilityData && !hasLineStrings) { return []; } - const triangleIndexArray = indices.typedArray; - const vertexCount = primitive.attributes[0].count; + const triangleIndexArray = hasVisibilityData ? indices.typedArray : undefined; const edgeIndices = []; const edgeData = []; const seenEdgeHashes = new Set(); let silhouetteEdgeCount = 0; + const globalColor = edgeVisibility.materialColor; + + if (hasVisibilityData) { + let edgeIndex = 0; + const totalIndices = triangleIndexArray.length; + const visibilityArray = visibility; + + for (let i = 0; i + 2 < totalIndices; i += 3) { + const v0 = triangleIndexArray[i]; + const v1 = triangleIndexArray[i + 1]; + const v2 = triangleIndexArray[i + 2]; + for (let e = 0; e < 3; e++) { + let a; + let b; + if (e === 0) { + a = v0; + b = v1; + } else if (e === 1) { + a = v1; + b = v2; + } else { + a = v2; + b = v0; + } - // Process triangles and extract edges (2 bits per edge) - let edgeIndex = 0; - const totalIndices = triangleIndexArray.length; - - for (let i = 0; i + 2 < totalIndices; i += 3) { - const v0 = triangleIndexArray[i]; - const v1 = triangleIndexArray[i + 1]; - const v2 = triangleIndexArray[i + 2]; - for (let e = 0; e < 3; e++) { - let a, b; - if (e === 0) { - a = v0; - b = v1; - } else if (e === 1) { - a = v1; - b = v2; - } else if (e === 2) { - a = v2; - b = v0; - } - const byteIndex = Math.floor(edgeIndex / 4); - const bitPairOffset = (edgeIndex % 4) * 2; - edgeIndex++; + const byteIndex = Math.floor(edgeIndex / 4); + const bitPairOffset = (edgeIndex % 4) * 2; + edgeIndex++; - if (byteIndex >= visibility.length) { - break; - } + if (byteIndex >= visibilityArray.length) { + break; + } - const byte = visibility[byteIndex]; - const visibility2Bit = (byte >> bitPairOffset) & 0x3; + const byte = visibilityArray[byteIndex]; + const visibility2Bit = (byte >> bitPairOffset) & 0x3; - // Only include visible edge types according to EXT_mesh_primitive_edge_visibility spec - let shouldIncludeEdge = false; - switch (visibility2Bit) { - case 0: // HIDDEN - never draw - shouldIncludeEdge = false; - break; - case 1: // SILHOUETTE - conditionally visible (front-facing vs back-facing) - shouldIncludeEdge = true; - break; - case 2: // HARD - always draw (primary encoding) - shouldIncludeEdge = true; - break; - case 3: // REPEATED - always draw (secondary encoding of a hard edge already encoded as 2) - shouldIncludeEdge = true; - break; - } + if (visibility2Bit === 0) { + continue; + } - if (shouldIncludeEdge) { const small = Math.min(a, b); const big = Math.max(a, b); - const hash = small * vertexCount + big; + const edgeKey = `${small},${big}`; - if (!seenEdgeHashes.has(hash)) { - seenEdgeHashes.add(hash); - edgeIndices.push(a, b); + if (seenEdgeHashes.has(edgeKey)) { + continue; + } + seenEdgeHashes.add(edgeKey); + edgeIndices.push(a, b); - let mateVertexIndex = -1; - if (visibility2Bit === 1) { - mateVertexIndex = silhouetteEdgeCount; - silhouetteEdgeCount++; - } + let mateVertexIndex = -1; + if (visibility2Bit === 1) { + mateVertexIndex = silhouetteEdgeCount; + silhouetteEdgeCount++; + } + + edgeData.push({ + edgeType: visibility2Bit, + triangleIndex: Math.floor(i / 3), + edgeIndex: e, + mateVertexIndex: mateVertexIndex, + currentTriangleVertices: [v0, v1, v2], + color: globalColor, + }); + } + } + } + + if (hasLineStrings) { + for (let i = 0; i < lineStrings.length; i++) { + const lineString = lineStrings[i]; + if (!defined(lineString) || !defined(lineString.indices)) { + continue; + } + + const indicesArray = lineString.indices; + if (!defined(indicesArray) || indicesArray.length < 2) { + continue; + } + + const restartValue = lineString.restartIndex; + const lineColor = defined(lineString.materialColor) + ? lineString.materialColor + : globalColor; + + let previous; + for (let j = 0; j < indicesArray.length; j++) { + const currentIndex = indicesArray[j]; + if (defined(restartValue) && currentIndex === restartValue) { + previous = undefined; + continue; + } + + if (!defined(previous)) { + previous = currentIndex; + continue; + } + + const a = previous; + const b = currentIndex; + previous = currentIndex; + + if (a === b) { + continue; + } + + if ( + vertexCount > 0 && + (a < 0 || a >= vertexCount || b < 0 || b >= vertexCount) + ) { + continue; + } + + const small = Math.min(a, b); + const big = Math.max(a, b); + const edgeKey = `${small},${big}`; - edgeData.push({ - edgeType: visibility2Bit, - triangleIndex: Math.floor(i / 3), - edgeIndex: e, - mateVertexIndex: mateVertexIndex, - currentTriangleVertices: [v0, v1, v2], - }); + if (seenEdgeHashes.has(edgeKey)) { + continue; } + + seenEdgeHashes.add(edgeKey); + edgeIndices.push(a, b); + edgeData.push({ + edgeType: 2, + triangleIndex: -1, + edgeIndex: -1, + mateVertexIndex: -1, + currentTriangleVertices: undefined, + color: defined(lineColor) ? lineColor : undefined, + }); } } } @@ -478,6 +571,7 @@ function extractVisibleEdges(primitive) { * @param {number} faceNormalALocation Shader attribute location for face normal A * @param {number} faceNormalBLocation Shader attribute location for face normal B * @param {number} edgeFeatureIdLocation Shader attribute location for optional edge feature ID + * @param {number} edgeColorLocation Shader attribute location for optional edge color override * @param {Object} edgeVisibility Edge visibility extension object (may contain silhouetteNormals[]) * @param {Float32Array} edgeFaceNormals Packed face normals (6 floats per edge) * @returns {Object|undefined} Object with {vertexArray, indexBuffer, indexCount} or undefined on failure @@ -493,6 +587,7 @@ function createCPULineEdgeGeometry( faceNormalALocation, faceNormalBLocation, edgeFeatureIdLocation, + edgeColorLocation, edgeVisibility, edgeFaceNormals, ) { @@ -522,6 +617,10 @@ function createCPULineEdgeGeometry( const silhouetteNormalArray = new Float32Array(totalVerts * 3); const faceNormalAArray = new Float32Array(totalVerts * 3); const faceNormalBArray = new Float32Array(totalVerts * 3); + const needsEdgeColorAttribute = defined(edgeColorLocation); + const edgeColorArray = needsEdgeColorAttribute + ? new Float32Array(totalVerts * 4) + : undefined; let p = 0; const maxSrcVertex = srcPos.length / 3 - 1; @@ -564,6 +663,16 @@ function createCPULineEdgeGeometry( faceNormalBArray[(normalIdx + 1) * 3] = 0; faceNormalBArray[(normalIdx + 1) * 3 + 1] = 0; faceNormalBArray[(normalIdx + 1) * 3 + 2] = 1; + if (needsEdgeColorAttribute) { + const colorBase = i * 8; + for (let k = 0; k < 2; k++) { + const base = colorBase + k * 4; + edgeColorArray[base] = 0; + edgeColorArray[base + 1] = 0; + edgeColorArray[base + 2] = 0; + edgeColorArray[base + 3] = -1.0; + } + } continue; } @@ -588,6 +697,24 @@ function createCPULineEdgeGeometry( edgeTypeArray[i * 2] = t; edgeTypeArray[i * 2 + 1] = t; + if (needsEdgeColorAttribute) { + const color = edgeData[i].color; + const hasOverride = defined(color); + const r = hasOverride ? color.x : 0.0; + const g = hasOverride ? color.y : 0.0; + const b = hasOverride ? color.z : 0.0; + const a = hasOverride ? color.w : -1.0; + const colorBase = i * 8; + edgeColorArray[colorBase] = r; + edgeColorArray[colorBase + 1] = g; + edgeColorArray[colorBase + 2] = b; + edgeColorArray[colorBase + 3] = a; + edgeColorArray[colorBase + 4] = r; + edgeColorArray[colorBase + 5] = g; + edgeColorArray[colorBase + 6] = b; + edgeColorArray[colorBase + 7] = a; + } + // Add silhouette normal for silhouette edges (type 1) let normalX = 0, normalY = 0, @@ -671,6 +798,14 @@ function createCPULineEdgeGeometry( typedArray: faceNormalBArray, usage: BufferUsage.STATIC_DRAW, }); + let edgeColorBuffer; + if (needsEdgeColorAttribute) { + edgeColorBuffer = Buffer.createVertexBuffer({ + context, + typedArray: edgeColorArray, + usage: BufferUsage.STATIC_DRAW, + }); + } // Create sequential indices for line pairs const useU32 = totalVerts > 65534; @@ -727,6 +862,16 @@ function createCPULineEdgeGeometry( }, ]; + if (needsEdgeColorAttribute) { + attributes.push({ + index: edgeColorLocation, + vertexBuffer: edgeColorBuffer, + componentsPerAttribute: 4, + componentDatatype: ComponentDatatype.FLOAT, + normalize: false, + }); + } + // Get feature ID from original geometry const primitive = renderResources.runtimePrimitive.primitive; const getFeatureIdForEdge = function () { @@ -794,6 +939,7 @@ function createCPULineEdgeGeometry( indexBuffer, indexCount: totalVerts, hasEdgeFeatureIds, + hasEdgeColors: needsEdgeColorAttribute, }; } diff --git a/packages/engine/Source/Scene/ModelComponents.js b/packages/engine/Source/Scene/ModelComponents.js index 71d7fb088076..446628909938 100644 --- a/packages/engine/Source/Scene/ModelComponents.js +++ b/packages/engine/Source/Scene/ModelComponents.js @@ -630,6 +630,14 @@ function Primitive() { * @private */ this.modelPrimitiveImagery = undefined; + + /** + * Data loaded from the EXT_mesh_primitive_edge_visibility extension. + * + * @type {Object} + * @private + */ + this.edgeVisibility = undefined; } /** diff --git a/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl b/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl index 99081026c07f..543f4335c17d 100644 --- a/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl @@ -13,59 +13,38 @@ void edgeVisibilityStage(inout vec4 color, inout FeatureIds featureIds) if (!u_isEdgePass) { return; } - + float edgeTypeInt = v_edgeType * 255.0; - - // Color code different edge types - vec4 edgeColor = vec4(0.0); - - if (edgeTypeInt < 0.5) { // HIDDEN (0) - edgeColor = vec4(0.0, 0.0, 0.0, 0.0); // Transparent for hidden edges + + if (edgeTypeInt < 0.5) { + discard; } - else if (edgeTypeInt > 0.5 && edgeTypeInt < 1.5) { // SILHOUETTE (1) - Conditional visibility - // Proper silhouette detection using face normals + + if (edgeTypeInt > 0.5 && edgeTypeInt < 1.5) { // silhouette candidate vec3 normalA = normalize(v_faceNormalAView); vec3 normalB = normalize(v_faceNormalBView); - - // Calculate view direction using existing eye-space position varying (v_positionEC) vec3 viewDir = -normalize(v_positionEC); - - // Calculate dot products to determine triangle facing float dotA = dot(normalA, viewDir); float dotB = dot(normalB, viewDir); - const float eps = 1e-3; bool frontA = dotA > eps; - bool backA = dotA < -eps; + bool backA = dotA < -eps; bool frontB = dotB > eps; - bool backB = dotB < -eps; - - // True silhouette: one triangle front-facing, other back-facing + bool backB = dotB < -eps; bool oppositeFacing = (frontA && backB) || (backA && frontB); - - // Exclude edges where both triangles are nearly grazing (perpendicular to view) - // This handles the top-view cylinder case where both normals are ~horizontal bool bothNearGrazing = (abs(dotA) <= eps && abs(dotB) <= eps); - if (!(oppositeFacing && !bothNearGrazing)) { - discard; // Not a true silhouette edge - } else { - // True silhouette - edgeColor = vec4(1.0, 0.0, 0.0, 1.0); + discard; } } - else if (edgeTypeInt > 1.5 && edgeTypeInt < 2.5) { // HARD (2) - BRIGHT GREEN - edgeColor = vec4(0.0, 1.0, 0.0, 1.0); // Extra bright green - } - else if (edgeTypeInt > 2.5 && edgeTypeInt < 3.5) { // REPEATED (3) - edgeColor = vec4(0.0, 0.0, 1.0, 1.0); - } else { - edgeColor = vec4(0.0, 0.0, 0.0, 0.0); - } - // Temporary color: white - edgeColor = vec4(1.0, 1.0, 1.0, 1.0); - color = edgeColor; + vec4 finalColor = color; +#ifdef HAS_EDGE_COLOR_OVERRIDE + if (v_edgeColor.a >= 0.0) { + finalColor = v_edgeColor; + } +#endif + color = finalColor; #if defined(HAS_EDGE_VISIBILITY_MRT) && !defined(CESIUM_REDIRECTED_COLOR_OUTPUT) // Write edge metadata From 6bb62280fe32f8049d7f6e77704d85bba49dae12 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Mon, 27 Oct 2025 00:58:01 -0400 Subject: [PATCH 02/12] update --- .../Model/EdgeVisibilityPipelineStage.js | 193 +++++++++++++++--- .../Shaders/Model/EdgeVisibilityStageFS.glsl | 2 +- 2 files changed, 167 insertions(+), 28 deletions(-) diff --git a/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js b/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js index 418e3b9922ab..d5a63499af9c 100644 --- a/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js +++ b/packages/engine/Source/Scene/Model/EdgeVisibilityPipelineStage.js @@ -12,6 +12,7 @@ import EdgeVisibilityStageFS from "../../Shaders/Model/EdgeVisibilityStageFS.js" import ModelUtility from "./ModelUtility.js"; import ModelReader from "./ModelReader.js"; import VertexAttributeSemantic from "../VertexAttributeSemantic.js"; +import AttributeType from "../AttributeType.js"; /** * Builds derived line geometry for model edges using EXT_mesh_primitive_edge_visibility data. @@ -112,7 +113,7 @@ EdgeVisibilityPipelineStage.process = function ( "#ifdef HAS_EDGE_FEATURE_ID", " v_featureId_0 = a_edgeFeatureId;", "#endif", - "#ifdef HAS_EDGE_COLOR_OVERRIDE", + "#ifdef HAS_EDGE_COLOR_ATTRIBUTE", " v_edgeColor = a_edgeColor;", "#endif", " // Transform normals from model space to view space", @@ -136,16 +137,21 @@ EdgeVisibilityPipelineStage.process = function ( return; } - let edgeColorLocation; + const runtimePrimitive = renderResources.runtimePrimitive.primitive; + const vertexColorInfo = collectVertexColors(runtimePrimitive); const hasEdgeColorOverride = edgeResult.edgeData.some(function (edge) { return defined(edge.color); }); - if (hasEdgeColorOverride) { + const needsEdgeColorAttribute = + hasEdgeColorOverride || defined(vertexColorInfo); + + let edgeColorLocation; + if (needsEdgeColorAttribute) { edgeColorLocation = shaderBuilder.addAttribute("vec4", "a_edgeColor"); shaderBuilder.addVarying("vec4", "v_edgeColor", "flat"); shaderBuilder.addDefine( - "HAS_EDGE_COLOR_OVERRIDE", + "HAS_EDGE_COLOR_ATTRIBUTE", undefined, ShaderDestination.BOTH, ); @@ -169,6 +175,7 @@ EdgeVisibilityPipelineStage.process = function ( faceNormalBLocation, edgeFeatureIdLocation, edgeColorLocation, + vertexColorInfo, primitive.edgeVisibility, edgeFaceNormals, ); @@ -557,6 +564,94 @@ function extractVisibleEdges(primitive) { return { edgeIndices, edgeData, silhouetteEdgeCount }; } +function collectVertexColors(runtimePrimitive) { + if (!defined(runtimePrimitive)) { + return undefined; + } + + const colorAttribute = ModelUtility.getAttributeBySemantic( + runtimePrimitive, + VertexAttributeSemantic.COLOR, + ); + if (!defined(colorAttribute)) { + return undefined; + } + + const components = AttributeType.getNumberOfComponents(colorAttribute.type); + if (components !== 3 && components !== 4) { + return undefined; + } + + let colorData = colorAttribute.typedArray; + if (!defined(colorData)) { + colorData = ModelReader.readAttributeAsTypedArray(colorAttribute); + } + if (!defined(colorData)) { + return undefined; + } + const count = colorAttribute.count; + if (!defined(count) || count === 0) { + return undefined; + } + + if (colorData.length < count * components) { + return undefined; + } + + const isFloatArray = + colorData instanceof Float32Array || colorData instanceof Float64Array; + const isUint8Array = colorData instanceof Uint8Array; + const isUint16Array = colorData instanceof Uint16Array; + const isInt8Array = colorData instanceof Int8Array; + const isInt16Array = colorData instanceof Int16Array; + + if ( + !isFloatArray && + !isUint8Array && + !isUint16Array && + !isInt8Array && + !isInt16Array + ) { + return undefined; + } + + const colors = new Float32Array(count * 4); + + const convertComponent = function (value) { + let converted; + if (isFloatArray) { + converted = value; + } else if (isUint8Array) { + converted = value / 255.0; + } else if (isUint16Array) { + converted = value / 65535.0; + } else if (isInt8Array) { + converted = (value + 128.0) / 255.0; + } else { + converted = (value + 32768.0) / 65535.0; + } + return Math.min(Math.max(converted, 0.0), 1.0); + }; + + for (let i = 0; i < count; i++) { + const srcBase = i * components; + const destBase = i * 4; + colors[destBase] = convertComponent(colorData[srcBase]); + colors[destBase + 1] = convertComponent(colorData[srcBase + 1]); + colors[destBase + 2] = convertComponent(colorData[srcBase + 2]); + if (components === 4) { + colors[destBase + 3] = convertComponent(colorData[srcBase + 3]); + } else { + colors[destBase + 3] = 1.0; + } + } + + return { + colors: colors, + count: count, + }; +} + /** * Create a derived line list geometry representing edges. A new vertex domain is used so we can pack * per-edge attributes (silhouette normal, face normal pair, edge type, optional feature ID) without @@ -571,7 +666,8 @@ function extractVisibleEdges(primitive) { * @param {number} faceNormalALocation Shader attribute location for face normal A * @param {number} faceNormalBLocation Shader attribute location for face normal B * @param {number} edgeFeatureIdLocation Shader attribute location for optional edge feature ID - * @param {number} edgeColorLocation Shader attribute location for optional edge color override + * @param {number} edgeColorLocation Shader attribute location for optional edge color data + * @param {{colors:Float32Array,count:number}} vertexColorInfo Packed per-vertex colors (optional) * @param {Object} edgeVisibility Edge visibility extension object (may contain silhouetteNormals[]) * @param {Float32Array} edgeFaceNormals Packed face normals (6 floats per edge) * @returns {Object|undefined} Object with {vertexArray, indexBuffer, indexCount} or undefined on failure @@ -588,6 +684,7 @@ function createCPULineEdgeGeometry( faceNormalBLocation, edgeFeatureIdLocation, edgeColorLocation, + vertexColorInfo, edgeVisibility, edgeFaceNormals, ) { @@ -621,8 +718,58 @@ function createCPULineEdgeGeometry( const edgeColorArray = needsEdgeColorAttribute ? new Float32Array(totalVerts * 4) : undefined; + const vertexColors = defined(vertexColorInfo) + ? vertexColorInfo.colors + : undefined; + const vertexColorCount = defined(vertexColorInfo) ? vertexColorInfo.count : 0; let p = 0; + function setNoColor(destVertexIndex) { + if (!needsEdgeColorAttribute) { + return; + } + const destOffset = destVertexIndex * 4; + edgeColorArray[destOffset] = 0.0; + edgeColorArray[destOffset + 1] = 0.0; + edgeColorArray[destOffset + 2] = 0.0; + edgeColorArray[destOffset + 3] = -1.0; + } + + function setColorFromOverride(destVertexIndex, color) { + if (!needsEdgeColorAttribute) { + return; + } + const destOffset = destVertexIndex * 4; + const r = defined(color.x) ? color.x : color[0]; + const g = defined(color.y) ? color.y : color[1]; + const b = defined(color.z) ? color.z : color[2]; + const a = defined(color.w) ? color.w : defined(color[3]) ? color[3] : 1.0; + edgeColorArray[destOffset] = r; + edgeColorArray[destOffset + 1] = g; + edgeColorArray[destOffset + 2] = b; + edgeColorArray[destOffset + 3] = a; + } + + function assignVertexColor(destVertexIndex, sourceVertexIndex) { + if (!needsEdgeColorAttribute) { + return; + } + if ( + !defined(vertexColors) || + sourceVertexIndex < 0 || + sourceVertexIndex >= vertexColorCount + ) { + setNoColor(destVertexIndex); + return; + } + const srcOffset = sourceVertexIndex * 4; + const destOffset = destVertexIndex * 4; + edgeColorArray[destOffset] = vertexColors[srcOffset]; + edgeColorArray[destOffset + 1] = vertexColors[srcOffset + 1]; + edgeColorArray[destOffset + 2] = vertexColors[srcOffset + 2]; + edgeColorArray[destOffset + 3] = vertexColors[srcOffset + 3]; + } + const maxSrcVertex = srcPos.length / 3 - 1; for (let i = 0; i < numEdges; i++) { @@ -664,14 +811,9 @@ function createCPULineEdgeGeometry( faceNormalBArray[(normalIdx + 1) * 3 + 1] = 0; faceNormalBArray[(normalIdx + 1) * 3 + 2] = 1; if (needsEdgeColorAttribute) { - const colorBase = i * 8; - for (let k = 0; k < 2; k++) { - const base = colorBase + k * 4; - edgeColorArray[base] = 0; - edgeColorArray[base + 1] = 0; - edgeColorArray[base + 2] = 0; - edgeColorArray[base + 3] = -1.0; - } + const baseVertexIndex = i * 2; + setNoColor(baseVertexIndex); + setNoColor(baseVertexIndex + 1); } continue; } @@ -699,20 +841,17 @@ function createCPULineEdgeGeometry( if (needsEdgeColorAttribute) { const color = edgeData[i].color; - const hasOverride = defined(color); - const r = hasOverride ? color.x : 0.0; - const g = hasOverride ? color.y : 0.0; - const b = hasOverride ? color.z : 0.0; - const a = hasOverride ? color.w : -1.0; - const colorBase = i * 8; - edgeColorArray[colorBase] = r; - edgeColorArray[colorBase + 1] = g; - edgeColorArray[colorBase + 2] = b; - edgeColorArray[colorBase + 3] = a; - edgeColorArray[colorBase + 4] = r; - edgeColorArray[colorBase + 5] = g; - edgeColorArray[colorBase + 6] = b; - edgeColorArray[colorBase + 7] = a; + const baseVertexIndex = i * 2; + if (defined(color)) { + setColorFromOverride(baseVertexIndex, color); + setColorFromOverride(baseVertexIndex + 1, color); + } else if (defined(vertexColors)) { + assignVertexColor(baseVertexIndex, a); + assignVertexColor(baseVertexIndex + 1, b); + } else { + setNoColor(baseVertexIndex); + setNoColor(baseVertexIndex + 1); + } } // Add silhouette normal for silhouette edges (type 1) diff --git a/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl b/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl index 543f4335c17d..539d98bd642b 100644 --- a/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/EdgeVisibilityStageFS.glsl @@ -39,7 +39,7 @@ void edgeVisibilityStage(inout vec4 color, inout FeatureIds featureIds) } vec4 finalColor = color; -#ifdef HAS_EDGE_COLOR_OVERRIDE +#ifdef HAS_EDGE_COLOR_ATTRIBUTE if (v_edgeColor.a >= 0.0) { finalColor = v_edgeColor; } From b935277dd70408f81f3645b1e0b81a54f51dfe82 Mon Sep 17 00:00:00 2001 From: Daniel Zhong <32878167+danielzhong@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:12:33 -0400 Subject: [PATCH 03/12] Update packages/engine/Source/Scene/GltfLoader.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/engine/Source/Scene/GltfLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index f386b8cc7366..0fa8fe7a51e3 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -2047,7 +2047,7 @@ function getLineStringPrimitiveRestartValue(componentType) { return 4294967295; default: throw new RuntimeError( - "EXT_mesh_primitive_edge_visibility lineStrings indices must use unsigned scalar component types.", + "EXT_mesh_primitive_edge_visibility line strings indices must use unsigned scalar component types.", ); } } From 072e65f88292aa505536b0d2283d7b275bc1be3b Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:23:57 -0400 Subject: [PATCH 04/12] upload example glb --- .../glTF-Binary/EdgeVisibility.glb | Bin 8144 -> 0 bytes .../glTF-Binary/EdgeVisibility1.glb | Bin 0 -> 2196 bytes .../glTF-Binary/EdgeVisibility2.glb | Bin 0 -> 11740 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb create mode 100644 Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility1.glb create mode 100644 Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility2.glb diff --git a/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb deleted file mode 100644 index 4cd0855473b9ce543aad4972f6048e65940a7c0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8144 zcmeI0dvH|c8OA?O!c75DM8OM&^@4IXVXxWU#AFSS9Kc*K3E>h#*lbRc)nqs9ZUP}B z5Fp4^t`aX?)Q%mgol>Wxe;8$_{L^XeAGTJf(`l`(?NDdzw6&w1>Wtg>Ip@45XOox_ zJDuq`gqO^ZXV3SY@A=;6JG*Radu^E{gn0WdAwHNc#HyOA%3fP2kx-Mif?iv<8c#%G zQCoq{mFKY8ZLMlljfax4I4Np#tD4&#Nkp0=?U7_pZYOW-p+u> zlR4Zq!El$yMOT0G@g8@+KXZJ(hXm{DC%V1Y;mpq*?sSbZKjCnvd(?23=W2$#eWQj4 zGM7Hl#(UlVQO^BWvF~1w&y{KXsz!UgqZaPEnoalOUe=zESvX}k6b`G2M2yp|r4LEg z(i@I-bjG4;G+EozsZ!pZE~niVj&(&Tv3h(p-Bz=#q`ah>PGL0ELD$(L(dGzE+vuQE z$C1`2)y9vWluR0vI`e6}lKP}|rOVtjWk-Y#dA-N&b^7hDe4o={clms7CoR1rw2fY& zL+JL~-2sOyV0Zf+jsU%4ETc1)pfeqdjHBX+cnCW z2b-l$71@qQQ8M18n(Ge7+GFuX$2d2k4vt;6dlBJFLlE;X4{D>XCYwOrfP(JdcYGtT1pE9MkA$E3(N-f~B}M=xh)D6@BC=c7|K zc3UTyPtT;!@AKGQejgo*%t*QeKCcsz%nz8ajE>~kMbJi!UBt*fWtjR}@fmvyIy@8J z0}qJtdtpfrO{*Qgq#BQe+Vyw5&Zc;UnhdqKN5a*yuGY3FJyY;5-C>Ajp|JMW?C7)G znqyr}?P^V=nRBmAM{mNaF&DoT>;DWTlkrGXR}!!G+E!Q9tgKyGRcR}rovEy^tEZvFe618k9+{YTwmkOA0IT#&k^*@v)jfbo*s*ReR#}e zE|k|sZ5vrL<;+}Y;42qO!TWC)>U7YW8nCg^ShKOOv@7ulo|uk2hr{9V2mDS?zQg7B zy8~XY%kD7$`fm-jCp2}HHXiFpgy{pC-ceqXLtCKs0xrtQ$@$e> zA(pPJ6k?JzDND-Ama->Hlcz{irb<(%NzjvqD+0yJA zr5kUOZki*_xmmh-t~B=+>6TliTjxph=1cQ$lWx0Ry8RC6jyt707f1{4lJ3fpau!Mp zZIbP7>F#@^d+wF)y-&Jtk+kT3>3$(SuvlJfm+iT7Zl0XCL|)>M9ZuQll3i}u?U6lR z*_$ut`(&SA_6OuZfm~217d|LISR@w}%f&%C_>lZiiCnT&Ub;+Pwp?CbDwmeYWs0n< zkXNjfSFVy*JuE+5E|*ux6_s*jm0Y!2Uj2yt$OCe9O>K4Unwq+`HEV0@)~&Bwzjob* z`gQf|H*9R!&``f|Q{%?QhE1D8n?jA7o5GvJp{8cFNewr*w9;qQmbOSsq_yqQ$J!o? zJlfvzXvbsiQF{OAh<0v?Zi#iq6P<}I@nl!LE0OHpn%vsey{)IaXY01@z1w+kar+fyehBJhcDNfrE#S z96WO9@DoQ5A3gHKvExr1KYHxM$zvyvpE&j8i6>8CXU;x*=G^%+=g*#d?)h`ipMS1;&2wv>7k;}U4qc~+@BX?>%*j*4doy*OwNw!; zLgx;LBHo&!{KPyJKpKhZq9KGu19ogzN|v(7)PQRsgh(G>D=gSp} zc0G$~+=K(r1bj}8JX6T#~=*-YL zGtil#bAF&RL+30(XNJzXg3b(`GX|X*I_C{KGjz@#bY|$BL+H%VIg`+tp>sZ=GehUB zLT84~xrNRQoihxb89L_~Ix}?6Hgsm_oO9^R&^hzanW1z3p)*70vjCkLI-d*Z%+UFa zKxc-|=LI@5bUr)KnW6JJg3b(`&lGfK=zPAQGehUI2Avr?pF8Nx(D@8PXNJz_5jrz; zKAX^)q4PO~&J3N;EOch*=FhH_iQ*dTd-3hshv|C<3i0>zFLws7@wa;)d_!%irVrjN z{>%KAhYzmlPTAKUDxMWQpTX8^Uh?#(7JvAUBJbLxg;sv|w>wjBES&u28w&@At^Bu8 zZ)*3El1o3GJydArpF}%TzyJBgOZ&QB9Jcb%U}I|NadmTf1I1zlrBttR{q+sl{daPD>d~*ZR*uYKN_|&dA$Tb@M~rAeF^^I-OA+kP56Xg zE0fnE-RI!2mC0){e8R7l$!jru!mpLdYcYIkel1L1gYXHzRwl1O_=I08lh+`8!mpLd zYY;x+*UIEI2%ko;O(w5F_%!-!GIcQ%pGJR8Mwa2z=&#AhH9Q;rH5plkXQRI+Bg?vHt-mHC%kXUU z*JNZFo{j#Rj4Z>m(O;91Wq3CFYcjG7&qjYuMwa2(=&#A?Yd_a^;QwF;cIQm{#+AKX zWIyzcN{9n?v1eYIejhIa3}kEdNqXRu^x*q~>hN1>s?)CvsuwP#sa`pQx_n$vy>kF{ zWlx&wiviS3triXnD$n6GRq1I#Rrf-gD*Os6dRb6)zk}LwB~8`;g;q1u{WH_s7YeF` zCruS86;us%X{yRFswgU`?A@q&JJM9y{isj0dVjm1dMlBpdOaejE;OX6&QzkvBYppw zl|4CY@)Y{u`M>yVCT4$DD)t!PgMpij@116Ur_AIT7t6#Of6%|%QEpc<_^k1L+GYAa N$>53N`Muqe_zwdVT~Po4 diff --git a/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility1.glb b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility1.glb new file mode 100644 index 0000000000000000000000000000000000000000..a089101d26b3e91ee906d4fb7b81487af5adc21d GIT binary patch literal 2196 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 EU&J`u=>Px# literal 0 HcmV?d00001 From d30ad47d6f182422f26974e0ecefd8054cf5a945 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:41:47 -0400 Subject: [PATCH 05/12] upload example glb --- .../{EdgeVisibility1.glb => EdgeVisibility.glb} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/{EdgeVisibility1.glb => EdgeVisibility.glb} (100%) diff --git a/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility1.glb b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb similarity index 100% rename from Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility1.glb rename to Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb From 5d4770e142f30172001c82f7286ed0d94299a438 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:31:18 -0400 Subject: [PATCH 06/12] test --- packages/engine/Specs/Scene/GltfLoaderSpec.js | 2 +- .../engine/Specs/Scene/Model/EdgeVisibilityRenderingSpec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index 41ac57e6330d..05d5147cffce 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -131,7 +131,7 @@ describe( const meshPrimitiveRestartTestData = "./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf"; const edgeVisibilityTestData = - "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb"; + "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility2.glb"; let scene; const gltfLoaders = []; diff --git a/packages/engine/Specs/Scene/Model/EdgeVisibilityRenderingSpec.js b/packages/engine/Specs/Scene/Model/EdgeVisibilityRenderingSpec.js index c7563b63f502..b9a0f1f46a89 100644 --- a/packages/engine/Specs/Scene/Model/EdgeVisibilityRenderingSpec.js +++ b/packages/engine/Specs/Scene/Model/EdgeVisibilityRenderingSpec.js @@ -6,7 +6,7 @@ import pollToPromise from "../../../../../Specs/pollToPromise.js"; describe("Scene/Model/EdgeVisibilityRendering", function () { let scene; const edgeVisibilityTestData = - "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb"; + "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility2.glb"; beforeAll(function () { scene = createScene(); From bb9f62f52c3af2fc689a8a14c9a6531522bac111 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:54:32 -0400 Subject: [PATCH 07/12] test --- packages/engine/Specs/Scene/Model/ModelSpec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js index 9d05e9c7d4ea..fea127f34867 100644 --- a/packages/engine/Specs/Scene/Model/ModelSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelSpec.js @@ -2471,6 +2471,10 @@ describe( }, scene, ); + await pollToPromise(function () { + scene.renderForSpecs(); + return model._heightDirty === false; + }); expect(model._heightDirty).toBe(false); const terrainProvider = await CesiumTerrainProvider.fromUrl( "Data/CesiumTerrainTileJson/QuantizedMeshWithVertexNormals", From 11d6b15eb7f2669f774a510a9325c5be1f66f3d9 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:01:24 -0400 Subject: [PATCH 08/12] unit tests --- CHANGES.md | 1 + packages/engine/Specs/Scene/GltfLoaderSpec.js | 69 +++++++++++++++++++ ...EdgeVisibilityPipelineStageDecodingSpec.js | 31 +++++++++ 3 files changed, 101 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7be7c68fe703..9892f6f2fb56 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ #### Additions :tada: - Added support for [EXT_mesh_primitive_edge_visibility](https://github.com/KhronosGroup/glTF/pull/2479) glTF extension. [#12765](https://github.com/CesiumGS/cesium/issues/12765) +- Extended edge visibility loading to honor material colors and line-string overrides from EXT_mesh_primitive_edge_visibility. #### Fixes :wrench: diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index 05d5147cffce..9cf684954b03 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -4374,6 +4374,75 @@ describe( } }); + it("loads edge visibility material color and line strings", async function () { + function modifyGltf(gltf) { + const primitive = gltf.meshes[0].primitives[0]; + const edgeVisibility = + primitive.extensions.EXT_mesh_primitive_edge_visibility; + + edgeVisibility.material = 0; + const globalColor = [0.2, 0.4, 0.6, 0.8]; + const overrideColor = [0.9, 0.1, 0.2, 1.0]; + + const pbr = + gltf.materials[0].pbrMetallicRoughness ?? + (gltf.materials[0].pbrMetallicRoughness = {}); + pbr.baseColorFactor = globalColor.slice(); + + gltf.materials.push({ + pbrMetallicRoughness: { + baseColorFactor: overrideColor.slice(), + }, + }); + + const overrideMaterialIndex = gltf.materials.length - 1; + + edgeVisibility.lineStrings = [ + { + indices: primitive.indices, + }, + { + indices: primitive.indices, + material: overrideMaterialIndex, + }, + ]; + + return gltf; + } + + const gltfLoader = await loadModifiedGltfAndTest( + edgeVisibilityTestData, + undefined, + modifyGltf, + ); + const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; + + const edgeVisibility = primitive.edgeVisibility; + expect(edgeVisibility).toBeDefined(); + expect(edgeVisibility.materialColor).toEqualEpsilon( + new Cartesian4(0.2, 0.4, 0.6, 0.8), + CesiumMath.EPSILON7, + ); + + const lineStrings = edgeVisibility.lineStrings; + expect(lineStrings).toBeDefined(); + expect(lineStrings.length).toBe(2); + expect(lineStrings[0].indices.length).toBeGreaterThan(0); + expect(lineStrings[0].restartIndex).toBe(255); + expect( + Cartesian4.equals( + lineStrings[0].materialColor, + edgeVisibility.materialColor, + ), + ).toBe(true); + expect( + Cartesian4.equals( + lineStrings[1].materialColor, + new Cartesian4(0.9, 0.1, 0.2, 1.0), + ), + ).toBe(true); + }); + it("validates edge visibility data loading", async function () { const gltfLoader = await loadGltf(edgeVisibilityTestData); const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; diff --git a/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js b/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js index 482664be9df3..72cc2acae4b8 100644 --- a/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js +++ b/packages/engine/Specs/Scene/Model/EdgeVisibilityPipelineStageDecodingSpec.js @@ -1,4 +1,5 @@ import { + Cartesian4, Buffer, BufferUsage, ComponentDatatype, @@ -314,6 +315,36 @@ describe("Scene/Model/EdgeVisibilityPipelineStage", function () { expect(expectedEdges.size).toBe(3); }); + it("generates edge color attribute for material overrides and line strings", function () { + const primitive = createTestPrimitive(); + primitive.edgeVisibility.materialColor = new Cartesian4(0.2, 0.3, 0.4, 1.0); + primitive.edgeVisibility.lineStrings = [ + { + indices: new Uint16Array([0, 1, 65535, 1, 3]), + restartIndex: 65535, + materialColor: new Cartesian4(0.9, 0.1, 0.2, 1.0), + }, + ]; + + const renderResources = createMockRenderResources(primitive); + const frameState = createMockFrameState(); + + EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState); + + expect(renderResources.edgeGeometry).toBeDefined(); + + const attributeLocations = renderResources.shaderBuilder.attributeLocations; + expect(attributeLocations.a_edgeColor).toBeDefined(); + + const vertexDefines = + renderResources.shaderBuilder._vertexShaderParts.defineLines; + expect(vertexDefines).toContain("HAS_EDGE_COLOR_ATTRIBUTE"); + + const attributes = + renderResources.edgeGeometry.vertexArray._attributes ?? []; + expect(attributes.length).toBeGreaterThan(5); + }); + it("sets up uniforms correctly", function () { const primitive = createTestPrimitive(); const renderResources = createMockRenderResources(primitive); From 6d381de00dbe85d40988a801bc6a85ece714d89b Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:28:11 -0400 Subject: [PATCH 09/12] fix errors --- packages/engine/Specs/Scene/GltfLoaderSpec.js | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index 9cf684954b03..b3ac136b7b3a 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -227,9 +227,7 @@ describe( } async function loadModifiedGltfAndTest(gltfPath, options, modifyFunction) { - let gltf = await Resource.fetchJson({ - url: gltfPath, - }); + let gltf = await loadGlbAsJson(gltfPath); gltf = modifyFunction(gltf); @@ -246,6 +244,40 @@ describe( return gltfLoader; } + async function loadGlbAsJson(gltfPath) { + const arrayBuffer = await Resource.fetchArrayBuffer({ + url: gltfPath, + }); + + const dataView = new DataView(arrayBuffer); + if (dataView.byteLength >= 4) { + const magic = dataView.getUint32(0, true); + if (magic !== 0x46546c67) { + const text = new TextDecoder().decode(new Uint8Array(arrayBuffer)); + return JSON.parse(text); + } + } + + let offset = 12; + const textDecoder = new TextDecoder(); + while (offset < arrayBuffer.byteLength) { + const chunkLength = dataView.getUint32(offset, true); + offset += 4; + const chunkType = dataView.getUint32(offset, true); + offset += 4; + + if (chunkType === 0x4e4f534a) { + const chunk = new Uint8Array(arrayBuffer, offset, chunkLength); + const jsonText = textDecoder.decode(chunk); + return JSON.parse(jsonText); + } + + offset += chunkLength; + } + + return undefined; + } + function getAttribute(attributes, semantic, setIndex) { const attributesLength = attributes.length; for (let i = 0; i < attributesLength; ++i) { @@ -4429,18 +4461,14 @@ describe( expect(lineStrings.length).toBe(2); expect(lineStrings[0].indices.length).toBeGreaterThan(0); expect(lineStrings[0].restartIndex).toBe(255); - expect( - Cartesian4.equals( - lineStrings[0].materialColor, - edgeVisibility.materialColor, - ), - ).toBe(true); - expect( - Cartesian4.equals( - lineStrings[1].materialColor, - new Cartesian4(0.9, 0.1, 0.2, 1.0), - ), - ).toBe(true); + expect(lineStrings[0].materialColor).toEqualEpsilon( + edgeVisibility.materialColor, + CesiumMath.EPSILON7, + ); + expect(lineStrings[1].materialColor).toEqualEpsilon( + new Cartesian4(0.9, 0.1, 0.2, 1.0), + CesiumMath.EPSILON7, + ); }); it("validates edge visibility data loading", async function () { From a72903f0cfbb5d5f1b745dd96d55a317e0394b5c Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:54:23 -0400 Subject: [PATCH 10/12] test --- packages/engine/Specs/Scene/GltfLoaderSpec.js | 105 +----------------- 1 file changed, 4 insertions(+), 101 deletions(-) diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index b3ac136b7b3a..41ac57e6330d 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -131,7 +131,7 @@ describe( const meshPrimitiveRestartTestData = "./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf"; const edgeVisibilityTestData = - "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility2.glb"; + "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb"; let scene; const gltfLoaders = []; @@ -227,7 +227,9 @@ describe( } async function loadModifiedGltfAndTest(gltfPath, options, modifyFunction) { - let gltf = await loadGlbAsJson(gltfPath); + let gltf = await Resource.fetchJson({ + url: gltfPath, + }); gltf = modifyFunction(gltf); @@ -244,40 +246,6 @@ describe( return gltfLoader; } - async function loadGlbAsJson(gltfPath) { - const arrayBuffer = await Resource.fetchArrayBuffer({ - url: gltfPath, - }); - - const dataView = new DataView(arrayBuffer); - if (dataView.byteLength >= 4) { - const magic = dataView.getUint32(0, true); - if (magic !== 0x46546c67) { - const text = new TextDecoder().decode(new Uint8Array(arrayBuffer)); - return JSON.parse(text); - } - } - - let offset = 12; - const textDecoder = new TextDecoder(); - while (offset < arrayBuffer.byteLength) { - const chunkLength = dataView.getUint32(offset, true); - offset += 4; - const chunkType = dataView.getUint32(offset, true); - offset += 4; - - if (chunkType === 0x4e4f534a) { - const chunk = new Uint8Array(arrayBuffer, offset, chunkLength); - const jsonText = textDecoder.decode(chunk); - return JSON.parse(jsonText); - } - - offset += chunkLength; - } - - return undefined; - } - function getAttribute(attributes, semantic, setIndex) { const attributesLength = attributes.length; for (let i = 0; i < attributesLength; ++i) { @@ -4406,71 +4374,6 @@ describe( } }); - it("loads edge visibility material color and line strings", async function () { - function modifyGltf(gltf) { - const primitive = gltf.meshes[0].primitives[0]; - const edgeVisibility = - primitive.extensions.EXT_mesh_primitive_edge_visibility; - - edgeVisibility.material = 0; - const globalColor = [0.2, 0.4, 0.6, 0.8]; - const overrideColor = [0.9, 0.1, 0.2, 1.0]; - - const pbr = - gltf.materials[0].pbrMetallicRoughness ?? - (gltf.materials[0].pbrMetallicRoughness = {}); - pbr.baseColorFactor = globalColor.slice(); - - gltf.materials.push({ - pbrMetallicRoughness: { - baseColorFactor: overrideColor.slice(), - }, - }); - - const overrideMaterialIndex = gltf.materials.length - 1; - - edgeVisibility.lineStrings = [ - { - indices: primitive.indices, - }, - { - indices: primitive.indices, - material: overrideMaterialIndex, - }, - ]; - - return gltf; - } - - const gltfLoader = await loadModifiedGltfAndTest( - edgeVisibilityTestData, - undefined, - modifyGltf, - ); - const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; - - const edgeVisibility = primitive.edgeVisibility; - expect(edgeVisibility).toBeDefined(); - expect(edgeVisibility.materialColor).toEqualEpsilon( - new Cartesian4(0.2, 0.4, 0.6, 0.8), - CesiumMath.EPSILON7, - ); - - const lineStrings = edgeVisibility.lineStrings; - expect(lineStrings).toBeDefined(); - expect(lineStrings.length).toBe(2); - expect(lineStrings[0].indices.length).toBeGreaterThan(0); - expect(lineStrings[0].restartIndex).toBe(255); - expect(lineStrings[0].materialColor).toEqualEpsilon( - edgeVisibility.materialColor, - CesiumMath.EPSILON7, - ); - expect(lineStrings[1].materialColor).toEqualEpsilon( - new Cartesian4(0.9, 0.1, 0.2, 1.0), - CesiumMath.EPSILON7, - ); - }); - it("validates edge visibility data loading", async function () { const gltfLoader = await loadGltf(edgeVisibilityTestData); const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; From a2de2ca719dda9bd25eeac9fb9fbf4a3e1d17dd9 Mon Sep 17 00:00:00 2001 From: danielzhong <32878167+danielzhong@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:29:30 -0400 Subject: [PATCH 11/12] test --- .../glTF-Binary/EdgeVisibility.glb | Bin 2196 -> 8144 bytes .../glTF-Binary/EdgeVisibilityLineString.glb | Bin 0 -> 2196 bytes ...bility2.glb => EdgeVisibilityMaterial.glb} | Bin packages/engine/Specs/Scene/GltfLoaderSpec.js | 36 ++++++++++++++++++ .../Model/EdgeVisibilityRenderingSpec.js | 2 +- 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityLineString.glb rename Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/{EdgeVisibility2.glb => EdgeVisibilityMaterial.glb} (100%) diff --git a/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb index a089101d26b3e91ee906d4fb7b81487af5adc21d..4cd0855473b9ce543aad4972f6048e65940a7c0b 100644 GIT binary patch literal 8144 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 N$>53N`Muqe_zwdVT~Po4 literal 2196 zcmcgtO>YuG7@qRca?`|`)SJ%1i(4|g><5sTTAS{Qpe0k^b}4Q_w)P?fGCS`(&pglb&b-_0)$|xb=yeLAA4!Dn zSIea_Ha(9roFC&ZwW(_|$HjTvh1AWOYuaANar@Uta#~7@SimFddX{5DhLk3FB4D%C zqMqlt9?sXtm>mx&#Hu^n#qA2Dx19dKv8l~!en1f=kxve7*f1yH=CISD?!HAI<2-@K zV@AuJj!%b7gp0w6*+@K53IXo#?qvSTP7Sl`n@1o@&dRDLC`3^dA*1C;=0L!G3mmA6 znxsjBs1l6`8BtVah=dh^)MbkPjX(t0yd3#_IT2`*{NK%yBF(v=oH;Ns=S(Srvl-}L zJZJ%tBDK5h5a%<1#Z0%$uS7=9sTuCflBGPCS9rEoogU&UbFDU91R0*gVE!3g29}#c zla%SiDVhW){4Cl`_9gucnBYniYuG7@qRca?`|`)SJ%1i(4|g><5sTTAS{Qpe0k^b}4Q_w)P?fGCS`(&pglb&b-_0)$|xb=yeLAA4!Dn zSIea_Ha(9roFC&ZwW(_|$HjTvh1AWOYuaANar@Uta#~7@SimFddX{5DhLk3FB4D%C zqMqlt9?sXtm>mx&#Hu^n#qA2Dx19dKv8l~!en1f=kxve7*f1yH=CISD?!HAI<2-@K zV@AuJj!%b7gp0w6*+@K53IXo#?qvSTP7Sl`n@1o@&dRDLC`3^dA*1C;=0L!G3mmA6 znxsjBs1l6`8BtVah=dh^)MbkPjX(t0yd3#_IT2`*{NK%yBF(v=oH;Ns=S(Srvl-}L zJZJ%tBDK5h5a%<1#Z0%$uS7=9sTuCflBGPCS9rEoogU&UbFDU91R0*gVE!3g29}#c zla%SiDVhW){4Cl`_9gucnBYni Date: Thu, 30 Oct 2025 17:41:16 -0400 Subject: [PATCH 12/12] test --- packages/engine/Specs/Scene/GltfLoaderSpec.js | 185 ++++++++++++++++-- 1 file changed, 165 insertions(+), 20 deletions(-) diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index 809b22a73dbf..de62a4f34267 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -231,14 +231,16 @@ describe( } async function loadModifiedGltfAndTest(gltfPath, options, modifyFunction) { - let gltf = await Resource.fetchJson({ + const arrayBuffer = await Resource.fetchArrayBuffer({ url: gltfPath, }); - gltf = modifyFunction(gltf); + const gltfData = parseGlb(arrayBuffer); + const modifiedGltf = modifyFunction(gltfData.gltf) ?? gltfData.gltf; + const rebuiltGlb = createGlbBuffer(modifiedGltf, gltfData.binaryChunk); spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue( - Promise.resolve(generateJsonBuffer(gltf).buffer), + Promise.resolve(rebuiltGlb), ); const gltfLoader = new GltfLoader(getOptions(gltfPath, options)); @@ -250,6 +252,111 @@ describe( return gltfLoader; } + function parseGlb(arrayBuffer) { + const dataView = new DataView(arrayBuffer); + if (dataView.byteLength < 12) { + const jsonText = new TextDecoder().decode(new Uint8Array(arrayBuffer)); + return { gltf: JSON.parse(jsonText), binaryChunk: undefined }; + } + + const magic = dataView.getUint32(0, true); + if (magic !== 0x46546c67) { + const jsonText = new TextDecoder().decode(new Uint8Array(arrayBuffer)); + return { gltf: JSON.parse(jsonText), binaryChunk: undefined }; + } + + let offset = 12; + let jsonObject; + let binaryChunk; + const textDecoder = new TextDecoder(); + + while (offset < arrayBuffer.byteLength) { + const chunkLength = dataView.getUint32(offset, true); + offset += 4; + const chunkType = dataView.getUint32(offset, true); + offset += 4; + + const chunkData = new Uint8Array(arrayBuffer, offset, chunkLength); + if (chunkType === 0x4e4f534a) { + jsonObject = JSON.parse(textDecoder.decode(chunkData)); + } else if (chunkType === 0x004e4942) { + binaryChunk = chunkData.slice(); + } + + offset += chunkLength; + } + + if (!jsonObject) { + throw new RuntimeError("GLB JSON chunk not found."); + } + + if (binaryChunk && jsonObject.buffers && jsonObject.buffers.length > 0) { + jsonObject.buffers[0].byteLength = binaryChunk.length; + delete jsonObject.buffers[0].uri; + } + + return { gltf: jsonObject, binaryChunk: binaryChunk }; + } + + function createGlbBuffer(gltf, binaryChunk) { + const textEncoder = new TextEncoder(); + const jsonBuffer = textEncoder.encode(JSON.stringify(gltf)); + const jsonPadding = (4 - (jsonBuffer.byteLength % 4)) % 4; + const paddedJson = new Uint8Array(jsonBuffer.byteLength + jsonPadding); + paddedJson.set(jsonBuffer); + if (jsonPadding > 0) { + paddedJson.fill(0x20, jsonBuffer.byteLength); + } + + let paddedBinary; + if (binaryChunk && binaryChunk.length > 0) { + const binPadding = (4 - (binaryChunk.length % 4)) % 4; + paddedBinary = new Uint8Array(binaryChunk.length + binPadding); + paddedBinary.set(binaryChunk); + if (binPadding > 0) { + paddedBinary.fill(0, binaryChunk.length); + } + } + + const hasBinaryChunk = !!paddedBinary; + const totalLength = + 12 + + 8 + + paddedJson.byteLength + + (hasBinaryChunk ? 8 + paddedBinary.byteLength : 0); + + const glbBuffer = new ArrayBuffer(totalLength); + const dataView = new DataView(glbBuffer); + let offset = 0; + + dataView.setUint32(offset, 0x46546c67, true); + offset += 4; + dataView.setUint32(offset, 2, true); + offset += 4; + dataView.setUint32(offset, totalLength, true); + offset += 4; + + dataView.setUint32(offset, paddedJson.byteLength, true); + offset += 4; + dataView.setUint32(offset, 0x4e4f534a, true); + offset += 4; + new Uint8Array(glbBuffer, offset, paddedJson.byteLength).set(paddedJson); + offset += paddedJson.byteLength; + + if (hasBinaryChunk) { + dataView.setUint32(offset, paddedBinary.byteLength, true); + offset += 4; + dataView.setUint32(offset, 0x004e4942, true); + offset += 4; + new Uint8Array(glbBuffer, offset, paddedBinary.byteLength).set( + paddedBinary, + ); + offset += paddedBinary.byteLength; + } + + return glbBuffer; + } + function getAttribute(attributes, semantic, setIndex) { const attributesLength = attributes.length; for (let i = 0; i < attributesLength; ++i) { @@ -4379,35 +4486,73 @@ describe( }); it("loads edge visibility material color override", async function () { - const gltfLoader = await loadGltf(edgeVisibilityMaterialTestData); - const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; + const gltfLoader = await loadModifiedGltfAndTest( + edgeVisibilityMaterialTestData, + undefined, + function (gltf) { + const primitive = gltf.meshes[0].primitives[0]; + const extension = + primitive.extensions.EXT_mesh_primitive_edge_visibility; + extension.material = 0; + + const material = gltf.materials[0]; + const pbr = + material.pbrMetallicRoughness ?? + (material.pbrMetallicRoughness = {}); + pbr.baseColorFactor = [0.2, 0.4, 0.6, 0.8]; + return gltf; + }, + ); + + const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; const edgeVisibility = primitive.edgeVisibility; expect(edgeVisibility).toBeDefined(); - expect(edgeVisibility.materialColor).toBeDefined(); - - const materialColor = edgeVisibility.materialColor; - expect(materialColor.x).toBeDefined(); - expect(materialColor.y).toBeDefined(); - expect(materialColor.z).toBeDefined(); - expect(materialColor.w).toBeDefined(); + expect(edgeVisibility.materialColor).toEqualEpsilon( + new Cartesian4(0.2, 0.4, 0.6, 0.8), + CesiumMath.EPSILON7, + ); }); it("loads edge visibility line strings", async function () { - const gltfLoader = await loadGltf(edgeVisibilityLineStringTestData); - const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; + const gltfLoader = await loadModifiedGltfAndTest( + edgeVisibilityLineStringTestData, + undefined, + function (gltf) { + const primitive = gltf.meshes[0].primitives[0]; + primitive.extensions = primitive.extensions ?? Object.create(null); + primitive.extensions.EXT_mesh_primitive_edge_visibility = { + lineStrings: [ + { + indices: gltf.meshes[0].primitives[1].indices, + material: 0, + }, + ], + }; + + const material = gltf.materials[0]; + const pbr = + material.pbrMetallicRoughness ?? + (material.pbrMetallicRoughness = {}); + pbr.baseColorFactor = [1.0, 0.5, 0.0, 1.0]; + + return gltf; + }, + ); + const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; const edgeVisibility = primitive.edgeVisibility; expect(edgeVisibility).toBeDefined(); expect(edgeVisibility.lineStrings).toBeDefined(); const lineStrings = edgeVisibility.lineStrings; - expect(lineStrings.length).toBeGreaterThan(0); - - const firstLineString = lineStrings[0]; - expect(firstLineString.indices).toBeDefined(); - expect(firstLineString.indices.length).toBeGreaterThan(0); - expect(firstLineString.restartIndex).toBeDefined(); + expect(lineStrings.length).toBe(1); + expect(lineStrings[0].indices.length).toBeGreaterThan(0); + expect(lineStrings[0].restartIndex).toBeDefined(); + expect(lineStrings[0].materialColor).toEqualEpsilon( + new Cartesian4(1.0, 0.5, 0.0, 1.0), + CesiumMath.EPSILON7, + ); }); it("validates edge visibility data loading", async function () {