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/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityLineString.glb b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityLineString.glb new file mode 100644 index 000000000000..a089101d26b3 Binary files /dev/null and b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityLineString.glb differ diff --git a/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityMaterial.glb b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityMaterial.glb new file mode 100644 index 000000000000..e2896cbebad4 Binary files /dev/null and b/Specs/Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityMaterial.glb differ diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index 81509d9c1a88..0fa8fe7a51e3 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 line strings 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..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,6 +113,9 @@ EdgeVisibilityPipelineStage.process = function ( "#ifdef HAS_EDGE_FEATURE_ID", " v_featureId_0 = a_edgeFeatureId;", "#endif", + "#ifdef HAS_EDGE_COLOR_ATTRIBUTE", + " 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 +137,26 @@ EdgeVisibilityPipelineStage.process = function ( return; } + const runtimePrimitive = renderResources.runtimePrimitive.primitive; + const vertexColorInfo = collectVertexColors(runtimePrimitive); + const hasEdgeColorOverride = edgeResult.edgeData.some(function (edge) { + return defined(edge.color); + }); + + 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_ATTRIBUTE", + 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 +174,8 @@ EdgeVisibilityPipelineStage.process = function ( faceNormalALocation, faceNormalBLocation, edgeFeatureIdLocation, + edgeColorLocation, + vertexColorInfo, primitive.edgeVisibility, edgeFaceNormals, ); @@ -372,91 +398,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; + + const attributes = primitive.attributes; + const vertexCount = + defined(attributes) && attributes.length > 0 ? attributes[0].count : 0; - if (!defined(visibility) || !defined(indices)) { + 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(edgeKey)) { + continue; + } + seenEdgeHashes.add(edgeKey); + edgeIndices.push(a, b); - if (!seenEdgeHashes.has(hash)) { - seenEdgeHashes.add(hash); - 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; + } - edgeData.push({ - edgeType: visibility2Bit, - triangleIndex: Math.floor(i / 3), - edgeIndex: e, - mateVertexIndex: mateVertexIndex, - currentTriangleVertices: [v0, v1, v2], - }); + 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}`; + + 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, + }); } } } @@ -464,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 @@ -478,6 +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 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 @@ -493,6 +683,8 @@ function createCPULineEdgeGeometry( faceNormalALocation, faceNormalBLocation, edgeFeatureIdLocation, + edgeColorLocation, + vertexColorInfo, edgeVisibility, edgeFaceNormals, ) { @@ -522,8 +714,62 @@ 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; + 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++) { @@ -564,6 +810,11 @@ function createCPULineEdgeGeometry( faceNormalBArray[(normalIdx + 1) * 3] = 0; faceNormalBArray[(normalIdx + 1) * 3 + 1] = 0; faceNormalBArray[(normalIdx + 1) * 3 + 2] = 1; + if (needsEdgeColorAttribute) { + const baseVertexIndex = i * 2; + setNoColor(baseVertexIndex); + setNoColor(baseVertexIndex + 1); + } continue; } @@ -588,6 +839,21 @@ function createCPULineEdgeGeometry( edgeTypeArray[i * 2] = t; edgeTypeArray[i * 2 + 1] = t; + if (needsEdgeColorAttribute) { + const color = edgeData[i].color; + 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) let normalX = 0, normalY = 0, @@ -671,6 +937,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 +1001,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 +1078,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..539d98bd642b 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_ATTRIBUTE + 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 diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index 41ac57e6330d..de62a4f34267 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -132,6 +132,10 @@ describe( "./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf"; const edgeVisibilityTestData = "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb"; + const edgeVisibilityMaterialTestData = + "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityMaterial.glb"; + const edgeVisibilityLineStringTestData = + "./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibilityLineString.glb"; let scene; const gltfLoaders = []; @@ -227,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)); @@ -246,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) { @@ -4374,6 +4485,76 @@ describe( } }); + it("loads edge visibility material color override", async function () { + 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).toEqualEpsilon( + new Cartesian4(0.2, 0.4, 0.6, 0.8), + CesiumMath.EPSILON7, + ); + }); + + it("loads edge visibility line strings", async function () { + 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).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 () { 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); 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",