Compare commits

...

17 Commits

Author SHA1 Message Date
Daniel Zhong c3a93dd3bf
Merge e5678b111d into ee2b3813b2 2025-11-21 15:47:27 -05:00
jjspace ee2b3813b2
Merge pull request #13038 from CesiumGS/fix/textureatlas-border-internal
deploy / deploy (push) Has been cancelled Details
dev / lint (push) Has been cancelled Details
dev / coverage (push) Has been cancelled Details
dev / release-tests (push) Has been cancelled Details
dev / node-20 (push) Has been cancelled Details
sandcastle-dev / deploy (push) Has been cancelled Details
fix(textureatlas): Apply internal padding between images
2025-11-21 17:14:38 +00:00
Don McCurdy 4e3980cc53 fix(textureatlas): Apply internal padding between images 2025-11-21 11:44:18 -05:00
danielzhong e5678b111d test 2025-10-30 17:41:16 -04:00
danielzhong a2de2ca719 test 2025-10-30 17:29:30 -04:00
danielzhong a72903f0cf test 2025-10-30 16:54:23 -04:00
danielzhong 6d381de00d fix errors 2025-10-30 16:28:11 -04:00
danielzhong 11d6b15eb7 unit tests 2025-10-30 16:01:24 -04:00
Daniel Zhong d4db1ea2d8
Merge branch 'main' into daniel/edge_visibility_material 2025-10-30 15:07:32 -04:00
danielzhong bb9f62f52c test 2025-10-30 14:54:32 -04:00
danielzhong 5d4770e142 test 2025-10-30 10:31:18 -04:00
danielzhong d30ad47d6f upload example glb 2025-10-30 02:41:47 -04:00
danielzhong 072e65f882 upload example glb 2025-10-30 02:23:57 -04:00
Daniel Zhong ca8380fa04
Merge branch 'main' into daniel/edge_visibility_material 2025-10-29 10:22:47 -04:00
Daniel Zhong b935277dd7
Update packages/engine/Source/Scene/GltfLoader.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 10:12:33 -04:00
danielzhong 6bb62280fe update 2025-10-27 00:58:01 -04:00
danielzhong 859a058c60 implmented 2025-10-26 21:58:37 -04:00
12 changed files with 871 additions and 174 deletions

View File

@ -7,7 +7,8 @@
#### Fixes :wrench: #### Fixes :wrench:
- Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585) - Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585)
- Improved scaling of SVGs in billboards [#TODO](https://github.com/CesiumGS/cesium/issues/TODO) - Improved scaling of SVGs in billboards [#13020](https://github.com/CesiumGS/cesium/issues/13020)
- Fixed unexpected outline artifacts around billboards [#13038](https://github.com/CesiumGS/cesium/issues/13038)
#### Additions :tada: #### Additions :tada:
@ -27,6 +28,7 @@
- Added experimental support for loading 3D Tiles as terrain, via `Cesium3DTilesTerrainProvider`. See [the PR](https://github.com/CesiumGS/cesium/pull/12963) for limitations on the types of 3D Tiles that can be used. [#12296](https://github.com/CesiumGS/cesium/issues/12296) - Added experimental support for loading 3D Tiles as terrain, via `Cesium3DTilesTerrainProvider`. See [the PR](https://github.com/CesiumGS/cesium/pull/12963) for limitations on the types of 3D Tiles that can be used. [#12296](https://github.com/CesiumGS/cesium/issues/12296)
- 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) - 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: #### Fixes :wrench:

View File

@ -122,6 +122,8 @@ TexturePacker.prototype._findNode = function (node, { width, height }) {
return node; return node;
} }
const borderPadding = this._borderPadding;
// Vertical split (childNode1 = left half, childNode2 = right half). // Vertical split (childNode1 = left half, childNode2 = right half).
if (widthDifference > heightDifference) { if (widthDifference > heightDifference) {
node.childNode1 = new TextureNode({ node.childNode1 = new TextureNode({
@ -130,12 +132,18 @@ TexturePacker.prototype._findNode = function (node, { width, height }) {
width, width,
height: nodeHeight, height: nodeHeight,
}); });
// Apply padding only along the vertical "cut".
const widthDifferencePadded = widthDifference - borderPadding;
if (widthDifferencePadded > 0) {
node.childNode2 = new TextureNode({ node.childNode2 = new TextureNode({
x: rectangle.x + width, x: rectangle.x + width + borderPadding,
y: rectangle.y, y: rectangle.y,
width: widthDifference, width: widthDifferencePadded,
height: nodeHeight, height: nodeHeight,
}); });
}
return this._findNode(node.childNode1, { width, height }); return this._findNode(node.childNode1, { width, height });
} }
@ -147,12 +155,19 @@ TexturePacker.prototype._findNode = function (node, { width, height }) {
width: nodeWidth, width: nodeWidth,
height, height,
}); });
// Apply padding only along the horizontal "cut".
const heightDifferencePadded = heightDifference - borderPadding;
if (heightDifferencePadded > 0) {
node.childNode2 = new TextureNode({ node.childNode2 = new TextureNode({
x: rectangle.x, x: rectangle.x,
y: rectangle.y + height, y: rectangle.y + height + borderPadding,
width: nodeWidth, width: nodeWidth,
height: heightDifference, height: heightDifferencePadded,
}); });
}
return this._findNode(node.childNode1, { width, height }); return this._findNode(node.childNode1, { width, height });
} }

View File

@ -2002,6 +2002,89 @@ function fetchSpzExtensionFrom(extensions) {
return undefined; 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 * Load resources associated with a mesh primitive for a glTF node
* @param {GltfLoader} loader * @param {GltfLoader} loader
@ -2041,37 +2124,44 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) {
// Edge Visibility // Edge Visibility
const edgeVisibilityExtension = extensions.EXT_mesh_primitive_edge_visibility; const edgeVisibilityExtension = extensions.EXT_mesh_primitive_edge_visibility;
const hasEdgeVisibility = defined(edgeVisibilityExtension); if (defined(edgeVisibilityExtension)) {
if (hasEdgeVisibility) { const edgeVisibility = {};
const visibilityAccessorId = edgeVisibilityExtension.visibility;
if (defined(visibilityAccessorId)) {
const visibilityAccessor = const visibilityAccessor =
loader.gltfJson.accessors[edgeVisibilityExtension.visibility]; loader.gltfJson.accessors[visibilityAccessorId];
if (!defined(visibilityAccessor)) { if (!defined(visibilityAccessor)) {
throw new RuntimeError("Edge visibility accessor not found!"); throw new RuntimeError("Edge visibility accessor not found!");
} }
const visibilityValues = loadAccessor(loader, visibilityAccessor); edgeVisibility.visibility = loadAccessor(loader, visibilityAccessor);
primitive.edgeVisibility = { }
visibility: visibilityValues,
material: edgeVisibilityExtension.material, edgeVisibility.materialColor = getEdgeVisibilityMaterialColor(
}; loader,
edgeVisibilityExtension.material,
);
// Load silhouette normals
if (defined(edgeVisibilityExtension.silhouetteNormals)) { if (defined(edgeVisibilityExtension.silhouetteNormals)) {
const silhouetteNormalsAccessor = const silhouetteNormalsAccessor =
loader.gltfJson.accessors[edgeVisibilityExtension.silhouetteNormals]; loader.gltfJson.accessors[edgeVisibilityExtension.silhouetteNormals];
if (defined(silhouetteNormalsAccessor)) { if (defined(silhouetteNormalsAccessor)) {
const silhouetteNormalsValues = loadAccessor( edgeVisibility.silhouetteNormals = loadAccessor(
loader, loader,
silhouetteNormalsAccessor, silhouetteNormalsAccessor,
); );
primitive.edgeVisibility.silhouetteNormals = silhouetteNormalsValues;
} }
} }
// Load line strings
if (defined(edgeVisibilityExtension.lineStrings)) { if (defined(edgeVisibilityExtension.lineStrings)) {
primitivePlan.edgeVisibility.lineStrings = edgeVisibility.lineStrings = loadEdgeVisibilityLineStrings(
edgeVisibilityExtension.lineStrings; loader,
edgeVisibilityExtension.lineStrings,
edgeVisibilityExtension.material,
);
} }
primitive.edgeVisibility = edgeVisibility;
} }
//support the latest glTF spec and the legacy extension //support the latest glTF spec and the legacy extension

View File

@ -12,6 +12,7 @@ import EdgeVisibilityStageFS from "../../Shaders/Model/EdgeVisibilityStageFS.js"
import ModelUtility from "./ModelUtility.js"; import ModelUtility from "./ModelUtility.js";
import ModelReader from "./ModelReader.js"; import ModelReader from "./ModelReader.js";
import VertexAttributeSemantic from "../VertexAttributeSemantic.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. * 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", "#ifdef HAS_EDGE_FEATURE_ID",
" v_featureId_0 = a_edgeFeatureId;", " v_featureId_0 = a_edgeFeatureId;",
"#endif", "#endif",
"#ifdef HAS_EDGE_COLOR_ATTRIBUTE",
" v_edgeColor = a_edgeColor;",
"#endif",
" // Transform normals from model space to view space", " // Transform normals from model space to view space",
" v_silhouetteNormalView = czm_normal * a_silhouetteNormal;", " v_silhouetteNormalView = czm_normal * a_silhouetteNormal;",
" v_faceNormalAView = czm_normal * a_faceNormalA;", " v_faceNormalAView = czm_normal * a_faceNormalA;",
@ -133,6 +137,26 @@ EdgeVisibilityPipelineStage.process = function (
return; 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). // Generate paired face normals for each unique edge (used to classify silhouette edges in the shader).
const edgeFaceNormals = generateEdgeFaceNormals( const edgeFaceNormals = generateEdgeFaceNormals(
adjacencyData, adjacencyData,
@ -150,6 +174,8 @@ EdgeVisibilityPipelineStage.process = function (
faceNormalALocation, faceNormalALocation,
faceNormalBLocation, faceNormalBLocation,
edgeFeatureIdLocation, edgeFeatureIdLocation,
edgeColorLocation,
vertexColorInfo,
primitive.edgeVisibility, primitive.edgeVisibility,
edgeFaceNormals, edgeFaceNormals,
); );
@ -372,75 +398,82 @@ function generateEdgeFaceNormals(adjacencyData, edgeIndices) {
*/ */
function extractVisibleEdges(primitive) { function extractVisibleEdges(primitive) {
const edgeVisibility = primitive.edgeVisibility; const edgeVisibility = primitive.edgeVisibility;
const visibility = edgeVisibility.visibility; if (!defined(edgeVisibility)) {
const indices = primitive.indices;
if (!defined(visibility) || !defined(indices)) {
return []; return [];
} }
const triangleIndexArray = indices.typedArray; const visibility = edgeVisibility.visibility;
const vertexCount = primitive.attributes[0].count; const indices = primitive.indices;
const lineStrings = edgeVisibility.lineStrings;
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 = hasVisibilityData ? indices.typedArray : undefined;
const edgeIndices = []; const edgeIndices = [];
const edgeData = []; const edgeData = [];
const seenEdgeHashes = new Set(); const seenEdgeHashes = new Set();
let silhouetteEdgeCount = 0; let silhouetteEdgeCount = 0;
const globalColor = edgeVisibility.materialColor;
// Process triangles and extract edges (2 bits per edge) if (hasVisibilityData) {
let edgeIndex = 0; let edgeIndex = 0;
const totalIndices = triangleIndexArray.length; const totalIndices = triangleIndexArray.length;
const visibilityArray = visibility;
for (let i = 0; i + 2 < totalIndices; i += 3) { for (let i = 0; i + 2 < totalIndices; i += 3) {
const v0 = triangleIndexArray[i]; const v0 = triangleIndexArray[i];
const v1 = triangleIndexArray[i + 1]; const v1 = triangleIndexArray[i + 1];
const v2 = triangleIndexArray[i + 2]; const v2 = triangleIndexArray[i + 2];
for (let e = 0; e < 3; e++) { for (let e = 0; e < 3; e++) {
let a, b; let a;
let b;
if (e === 0) { if (e === 0) {
a = v0; a = v0;
b = v1; b = v1;
} else if (e === 1) { } else if (e === 1) {
a = v1; a = v1;
b = v2; b = v2;
} else if (e === 2) { } else {
a = v2; a = v2;
b = v0; b = v0;
} }
const byteIndex = Math.floor(edgeIndex / 4); const byteIndex = Math.floor(edgeIndex / 4);
const bitPairOffset = (edgeIndex % 4) * 2; const bitPairOffset = (edgeIndex % 4) * 2;
edgeIndex++; edgeIndex++;
if (byteIndex >= visibility.length) { if (byteIndex >= visibilityArray.length) {
break; break;
} }
const byte = visibility[byteIndex]; const byte = visibilityArray[byteIndex];
const visibility2Bit = (byte >> bitPairOffset) & 0x3; const visibility2Bit = (byte >> bitPairOffset) & 0x3;
// Only include visible edge types according to EXT_mesh_primitive_edge_visibility spec if (visibility2Bit === 0) {
let shouldIncludeEdge = false; continue;
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 (shouldIncludeEdge) {
const small = Math.min(a, b); const small = Math.min(a, b);
const big = Math.max(a, b); const big = Math.max(a, b);
const hash = small * vertexCount + big; const edgeKey = `${small},${big}`;
if (!seenEdgeHashes.has(hash)) { if (seenEdgeHashes.has(edgeKey)) {
seenEdgeHashes.add(hash); continue;
}
seenEdgeHashes.add(edgeKey);
edgeIndices.push(a, b); edgeIndices.push(a, b);
let mateVertexIndex = -1; let mateVertexIndex = -1;
@ -455,15 +488,170 @@ function extractVisibleEdges(primitive) {
edgeIndex: e, edgeIndex: e,
mateVertexIndex: mateVertexIndex, mateVertexIndex: mateVertexIndex,
currentTriangleVertices: [v0, v1, v2], 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}`;
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,
});
}
}
} }
return { edgeIndices, edgeData, silhouetteEdgeCount }; 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 * 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 * 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} faceNormalALocation Shader attribute location for face normal A
* @param {number} faceNormalBLocation Shader attribute location for face normal B * @param {number} faceNormalBLocation Shader attribute location for face normal B
* @param {number} edgeFeatureIdLocation Shader attribute location for optional edge feature ID * @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 {Object} edgeVisibility Edge visibility extension object (may contain silhouetteNormals[])
* @param {Float32Array} edgeFaceNormals Packed face normals (6 floats per edge) * @param {Float32Array} edgeFaceNormals Packed face normals (6 floats per edge)
* @returns {Object|undefined} Object with {vertexArray, indexBuffer, indexCount} or undefined on failure * @returns {Object|undefined} Object with {vertexArray, indexBuffer, indexCount} or undefined on failure
@ -493,6 +683,8 @@ function createCPULineEdgeGeometry(
faceNormalALocation, faceNormalALocation,
faceNormalBLocation, faceNormalBLocation,
edgeFeatureIdLocation, edgeFeatureIdLocation,
edgeColorLocation,
vertexColorInfo,
edgeVisibility, edgeVisibility,
edgeFaceNormals, edgeFaceNormals,
) { ) {
@ -522,8 +714,62 @@ function createCPULineEdgeGeometry(
const silhouetteNormalArray = new Float32Array(totalVerts * 3); const silhouetteNormalArray = new Float32Array(totalVerts * 3);
const faceNormalAArray = new Float32Array(totalVerts * 3); const faceNormalAArray = new Float32Array(totalVerts * 3);
const faceNormalBArray = 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; 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; const maxSrcVertex = srcPos.length / 3 - 1;
for (let i = 0; i < numEdges; i++) { for (let i = 0; i < numEdges; i++) {
@ -564,6 +810,11 @@ function createCPULineEdgeGeometry(
faceNormalBArray[(normalIdx + 1) * 3] = 0; faceNormalBArray[(normalIdx + 1) * 3] = 0;
faceNormalBArray[(normalIdx + 1) * 3 + 1] = 0; faceNormalBArray[(normalIdx + 1) * 3 + 1] = 0;
faceNormalBArray[(normalIdx + 1) * 3 + 2] = 1; faceNormalBArray[(normalIdx + 1) * 3 + 2] = 1;
if (needsEdgeColorAttribute) {
const baseVertexIndex = i * 2;
setNoColor(baseVertexIndex);
setNoColor(baseVertexIndex + 1);
}
continue; continue;
} }
@ -588,6 +839,21 @@ function createCPULineEdgeGeometry(
edgeTypeArray[i * 2] = t; edgeTypeArray[i * 2] = t;
edgeTypeArray[i * 2 + 1] = 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) // Add silhouette normal for silhouette edges (type 1)
let normalX = 0, let normalX = 0,
normalY = 0, normalY = 0,
@ -671,6 +937,14 @@ function createCPULineEdgeGeometry(
typedArray: faceNormalBArray, typedArray: faceNormalBArray,
usage: BufferUsage.STATIC_DRAW, usage: BufferUsage.STATIC_DRAW,
}); });
let edgeColorBuffer;
if (needsEdgeColorAttribute) {
edgeColorBuffer = Buffer.createVertexBuffer({
context,
typedArray: edgeColorArray,
usage: BufferUsage.STATIC_DRAW,
});
}
// Create sequential indices for line pairs // Create sequential indices for line pairs
const useU32 = totalVerts > 65534; 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 // Get feature ID from original geometry
const primitive = renderResources.runtimePrimitive.primitive; const primitive = renderResources.runtimePrimitive.primitive;
const getFeatureIdForEdge = function () { const getFeatureIdForEdge = function () {
@ -794,6 +1078,7 @@ function createCPULineEdgeGeometry(
indexBuffer, indexBuffer,
indexCount: totalVerts, indexCount: totalVerts,
hasEdgeFeatureIds, hasEdgeFeatureIds,
hasEdgeColors: needsEdgeColorAttribute,
}; };
} }

View File

@ -630,6 +630,14 @@ function Primitive() {
* @private * @private
*/ */
this.modelPrimitiveImagery = undefined; this.modelPrimitiveImagery = undefined;
/**
* Data loaded from the EXT_mesh_primitive_edge_visibility extension.
*
* @type {Object}
* @private
*/
this.edgeVisibility = undefined;
} }
/** /**

View File

@ -16,56 +16,35 @@ void edgeVisibilityStage(inout vec4 color, inout FeatureIds featureIds)
float edgeTypeInt = v_edgeType * 255.0; float edgeTypeInt = v_edgeType * 255.0;
// Color code different edge types if (edgeTypeInt < 0.5) {
vec4 edgeColor = vec4(0.0); discard;
if (edgeTypeInt < 0.5) { // HIDDEN (0)
edgeColor = vec4(0.0, 0.0, 0.0, 0.0); // Transparent for hidden edges
} }
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 normalA = normalize(v_faceNormalAView);
vec3 normalB = normalize(v_faceNormalBView); vec3 normalB = normalize(v_faceNormalBView);
// Calculate view direction using existing eye-space position varying (v_positionEC)
vec3 viewDir = -normalize(v_positionEC); vec3 viewDir = -normalize(v_positionEC);
// Calculate dot products to determine triangle facing
float dotA = dot(normalA, viewDir); float dotA = dot(normalA, viewDir);
float dotB = dot(normalB, viewDir); float dotB = dot(normalB, viewDir);
const float eps = 1e-3; const float eps = 1e-3;
bool frontA = dotA > eps; bool frontA = dotA > eps;
bool backA = dotA < -eps; bool backA = dotA < -eps;
bool frontB = dotB > eps; bool frontB = dotB > eps;
bool backB = dotB < -eps; bool backB = dotB < -eps;
// True silhouette: one triangle front-facing, other back-facing
bool oppositeFacing = (frontA && backB) || (backA && frontB); 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); bool bothNearGrazing = (abs(dotA) <= eps && abs(dotB) <= eps);
if (!(oppositeFacing && !bothNearGrazing)) { if (!(oppositeFacing && !bothNearGrazing)) {
discard; // Not a true silhouette edge discard;
} else {
// True silhouette
edgeColor = vec4(1.0, 0.0, 0.0, 1.0);
} }
} }
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 vec4 finalColor = color;
edgeColor = vec4(1.0, 1.0, 1.0, 1.0); #ifdef HAS_EDGE_COLOR_ATTRIBUTE
color = edgeColor; if (v_edgeColor.a >= 0.0) {
finalColor = v_edgeColor;
}
#endif
color = finalColor;
#if defined(HAS_EDGE_VISIBILITY_MRT) && !defined(CESIUM_REDIRECTED_COLOR_OUTPUT) #if defined(HAS_EDGE_VISIBILITY_MRT) && !defined(CESIUM_REDIRECTED_COLOR_OUTPUT)
// Write edge metadata // Write edge metadata

View File

@ -852,16 +852,16 @@ describe("Scene/TextureAtlas", function () {
.2222222222222222............... .2222222222222222...............
.2222222222222222............... .2222222222222222...............
.2222222222222222............... .2222222222222222...............
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333....
.22222222222222223333333333..... .2222222222222222.3333333333.1..
.22222222222222223333333333..... .2222222222222222.3333333333....
.2222222222222222333333333301... .2222222222222222.3333333333.0..
................................ ................................
`.trim(), `.trim(),
); );
@ -926,9 +926,9 @@ describe("Scene/TextureAtlas", function () {
.2222222222... .2222222222...
.2222222222... .2222222222...
.2222222222... .2222222222...
.2222222222.1.
.2222222222... .2222222222...
.2222222222... .2222222222.0.
.222222222201.
.............. ..............
`.trim(), `.trim(),
); );
@ -976,16 +976,16 @@ describe("Scene/TextureAtlas", function () {
.3333333333333333............... .3333333333333333...............
.3333333333333333............... .3333333333333333...............
.3333333333333333............... .3333333333333333...............
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222....
.33333333333333332222222222..... .3333333333333333.2222222222.1..
.33333333333333332222222222..... .3333333333333333.2222222222....
.3333333333333333222222222201... .3333333333333333.2222222222.0..
................................ ................................
`.trim(), `.trim(),
); );
@ -1337,6 +1337,108 @@ describe("Scene/TextureAtlas", function () {
).contextToRender([0, 255, 0, 255]); ).contextToRender([0, 255, 0, 255]);
}); });
it("adds custom padding with borderWidthInPixels", async function () {
atlas = new TextureAtlas({ borderWidthInPixels: 0 });
let indices = await addImages();
expect(drawAtlas(atlas, indices)).toBe(
`
................
................
................
................
................
................
2222222222......
2222222222......
2222222222......
2222222222......
2222222222......
2222222222......
22222222220.....
22222222220.....
22222222220.....
222222222201....
`.trim(),
);
atlas = new TextureAtlas({ borderWidthInPixels: 2 });
indices = await addImages();
expect(drawAtlas(atlas, indices)).toBe(
`
................................
................................
................................
................................
..2222222222....................
..2222222222....................
..2222222222....................
..2222222222..1.................
..2222222222....................
..2222222222....................
..2222222222..0.................
..2222222222..0.................
..2222222222..0.................
..2222222222..0.................
................................
................................
`.trim(),
);
atlas = new TextureAtlas({ borderWidthInPixels: 5 });
indices = await addImages();
expect(drawAtlas(atlas, indices)).toBe(
`
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
.....2222222222.................
.....2222222222.................
.....2222222222.................
.....2222222222.................
.....2222222222.................
.....2222222222.................
.....2222222222.....0...........
.....2222222222.....0...........
.....2222222222.....0...........
.....2222222222.....0.....1.....
................................
................................
................................
................................
................................
`.trim(),
);
async function addImages() {
const promise = Promise.all([
atlas.addImage(tallGreenGuid, tallGreenImage),
atlas.addImage(blueGuid, blueImage),
atlas.addImage(bigBlueGuid, bigBlueImage),
]);
return pollWhilePromise(promise, () => {
atlas.update(scene.frameState.context);
});
}
});
it("GUID changes when atlas texure is modified", async function () { it("GUID changes when atlas texure is modified", async function () {
atlas = new TextureAtlas(); atlas = new TextureAtlas();

View File

@ -132,6 +132,10 @@ describe(
"./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf"; "./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf";
const edgeVisibilityTestData = const edgeVisibilityTestData =
"./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb"; "./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; let scene;
const gltfLoaders = []; const gltfLoaders = [];
@ -227,14 +231,16 @@ describe(
} }
async function loadModifiedGltfAndTest(gltfPath, options, modifyFunction) { async function loadModifiedGltfAndTest(gltfPath, options, modifyFunction) {
let gltf = await Resource.fetchJson({ const arrayBuffer = await Resource.fetchArrayBuffer({
url: gltfPath, 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( spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue(
Promise.resolve(generateJsonBuffer(gltf).buffer), Promise.resolve(rebuiltGlb),
); );
const gltfLoader = new GltfLoader(getOptions(gltfPath, options)); const gltfLoader = new GltfLoader(getOptions(gltfPath, options));
@ -246,6 +252,111 @@ describe(
return gltfLoader; 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) { function getAttribute(attributes, semantic, setIndex) {
const attributesLength = attributes.length; const attributesLength = attributes.length;
for (let i = 0; i < attributesLength; ++i) { 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 () { it("validates edge visibility data loading", async function () {
const gltfLoader = await loadGltf(edgeVisibilityTestData); const gltfLoader = await loadGltf(edgeVisibilityTestData);
const primitive = gltfLoader.components.scene.nodes[0].primitives[0]; const primitive = gltfLoader.components.scene.nodes[0].primitives[0];

View File

@ -1,4 +1,5 @@
import { import {
Cartesian4,
Buffer, Buffer,
BufferUsage, BufferUsage,
ComponentDatatype, ComponentDatatype,
@ -314,6 +315,36 @@ describe("Scene/Model/EdgeVisibilityPipelineStage", function () {
expect(expectedEdges.size).toBe(3); 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 () { it("sets up uniforms correctly", function () {
const primitive = createTestPrimitive(); const primitive = createTestPrimitive();
const renderResources = createMockRenderResources(primitive); const renderResources = createMockRenderResources(primitive);

View File

@ -2471,6 +2471,10 @@ describe(
}, },
scene, scene,
); );
await pollToPromise(function () {
scene.renderForSpecs();
return model._heightDirty === false;
});
expect(model._heightDirty).toBe(false); expect(model._heightDirty).toBe(false);
const terrainProvider = await CesiumTerrainProvider.fromUrl( const terrainProvider = await CesiumTerrainProvider.fromUrl(
"Data/CesiumTerrainTileJson/QuantizedMeshWithVertexNormals", "Data/CesiumTerrainTileJson/QuantizedMeshWithVertexNormals",