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:
- 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:
@ -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 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:

View File

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

View File

@ -2002,6 +2002,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
@ -2041,37 +2124,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!");
}
const visibilityValues = loadAccessor(loader, visibilityAccessor);
primitive.edgeVisibility = {
visibility: visibilityValues,
material: edgeVisibilityExtension.material,
};
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);
}
edgeVisibility.materialColor = getEdgeVisibilityMaterialColor(
loader,
edgeVisibilityExtension.material,
);
// Load silhouette normals
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

View File

@ -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;
const visibility = edgeVisibility.visibility;
const indices = primitive.indices;
if (!defined(visibility) || !defined(indices)) {
if (!defined(edgeVisibility)) {
return [];
}
const triangleIndexArray = indices.typedArray;
const vertexCount = primitive.attributes[0].count;
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;
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 edgeData = [];
const seenEdgeHashes = new Set();
let silhouetteEdgeCount = 0;
const globalColor = edgeVisibility.materialColor;
// Process triangles and extract edges (2 bits per edge)
let edgeIndex = 0;
const totalIndices = triangleIndexArray.length;
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, 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++;
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;
}
if (byteIndex >= visibility.length) {
break;
}
const byteIndex = Math.floor(edgeIndex / 4);
const bitPairOffset = (edgeIndex % 4) * 2;
edgeIndex++;
const byte = visibility[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;
if (byteIndex >= visibilityArray.length) {
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;
}
}
const byte = visibilityArray[byteIndex];
const visibility2Bit = (byte >> bitPairOffset) & 0x3;
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);
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],
});
if (seenEdgeHashes.has(edgeKey)) {
continue;
}
seenEdgeHashes.add(edgeKey);
edgeIndices.push(a, b);
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}`;
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,
};
}

View File

@ -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;
}
/**

View File

@ -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
}
else if (edgeTypeInt > 0.5 && edgeTypeInt < 1.5) { // SILHOUETTE (1) - Conditional visibility
// Proper silhouette detection using face normals
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 frontB = dotB > eps;
bool backB = dotB < -eps;
// True silhouette: one triangle front-facing, other back-facing
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);
}
}
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);
if (edgeTypeInt < 0.5) {
discard;
}
// Temporary color: white
edgeColor = vec4(1.0, 1.0, 1.0, 1.0);
color = edgeColor;
if (edgeTypeInt > 0.5 && edgeTypeInt < 1.5) { // silhouette candidate
vec3 normalA = normalize(v_faceNormalAView);
vec3 normalB = normalize(v_faceNormalBView);
vec3 viewDir = -normalize(v_positionEC);
float dotA = dot(normalA, viewDir);
float dotB = dot(normalB, viewDir);
const float eps = 1e-3;
bool frontA = dotA > eps;
bool backA = dotA < -eps;
bool frontB = dotB > eps;
bool backB = dotB < -eps;
bool oppositeFacing = (frontA && backB) || (backA && frontB);
bool bothNearGrazing = (abs(dotA) <= eps && abs(dotB) <= eps);
if (!(oppositeFacing && !bothNearGrazing)) {
discard;
}
}
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

View File

@ -852,16 +852,16 @@ describe("Scene/TextureAtlas", function () {
.2222222222222222...............
.2222222222222222...............
.2222222222222222...............
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.22222222222222223333333333.....
.2222222222222222333333333301...
.2222222222222222.3333333333....
.2222222222222222.3333333333....
.2222222222222222.3333333333....
.2222222222222222.3333333333....
.2222222222222222.3333333333....
.2222222222222222.3333333333....
.2222222222222222.3333333333....
.2222222222222222.3333333333.1..
.2222222222222222.3333333333....
.2222222222222222.3333333333.0..
................................
`.trim(),
);
@ -926,9 +926,9 @@ describe("Scene/TextureAtlas", function () {
.2222222222...
.2222222222...
.2222222222...
.2222222222.1.
.2222222222...
.2222222222...
.222222222201.
.2222222222.0.
..............
`.trim(),
);
@ -976,16 +976,16 @@ describe("Scene/TextureAtlas", function () {
.3333333333333333...............
.3333333333333333...............
.3333333333333333...............
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.33333333333333332222222222.....
.3333333333333333222222222201...
.3333333333333333.2222222222....
.3333333333333333.2222222222....
.3333333333333333.2222222222....
.3333333333333333.2222222222....
.3333333333333333.2222222222....
.3333333333333333.2222222222....
.3333333333333333.2222222222....
.3333333333333333.2222222222.1..
.3333333333333333.2222222222....
.3333333333333333.2222222222.0..
................................
`.trim(),
);
@ -1337,6 +1337,108 @@ describe("Scene/TextureAtlas", function () {
).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 () {
atlas = new TextureAtlas();

View File

@ -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];

View File

@ -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);

View File

@ -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",