mirror of https://github.com/CesiumGS/cesium.git
Merge branch 'main' into alark/async-picking
This commit is contained in:
commit
4439df1658
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: install npm packages
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: npm install
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: npm install
|
||||
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: npm install
|
||||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: npm install
|
||||
|
|
@ -67,7 +67,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 20
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: npm install
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: npm install
|
||||
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: npm install
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install node 22
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: npm install
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Change Log
|
||||
|
||||
## 1.135 - 2025-11-01
|
||||
## 1.135 - 2025-11-03
|
||||
|
||||
### @cesium/engine
|
||||
|
||||
|
|
@ -9,6 +9,10 @@
|
|||
- `scene.drillPick` now uses a breadth-first search strategy instead of depth-first. This may change which entities are picked when
|
||||
using large values of `width` and `height` when providing a `limit`, prioritizing entities closer to the camera.
|
||||
|
||||
#### 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)
|
||||
|
||||
#### Fixes :wrench:
|
||||
|
||||
- Fixed parsing content bounding volumes contained in 3D Tiles 1.1 subtree files. [#12972](https://github.com/CesiumGS/cesium/pull/12972)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
|
|||
- [Paul Connelly](https://github.com/pmconne)
|
||||
- [Jason Crow](https://github.com/jason-crow)
|
||||
- [Erin Ingram](https://github.com/eringram)
|
||||
- [Daniel Zhong](https://github.com/danielzhong)
|
||||
- [Mark Schlosser](https://github.com/markschlosseratbentley)
|
||||
- [Adam Larkeryd](https://github.com/alarkbentley)
|
||||
- [Flightradar24 AB](https://www.flightradar24.com)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -154,6 +154,11 @@ const instancedArraysStub = {
|
|||
vertexAttribDivisorANGLE: noop,
|
||||
};
|
||||
|
||||
// WEBGL_draw_buffers
|
||||
const drawBuffersStub = {
|
||||
drawBuffersWEBGL: noop,
|
||||
};
|
||||
|
||||
function noop() {}
|
||||
|
||||
function createStub() {
|
||||
|
|
@ -208,6 +213,11 @@ function getExtensionStub(name) {
|
|||
return {};
|
||||
}
|
||||
|
||||
// EdgeFramebuffer and other MRT features require draw buffers
|
||||
if (name === "WEBGL_draw_buffers") {
|
||||
return drawBuffersStub;
|
||||
}
|
||||
|
||||
// No other extensions are stubbed.
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,6 +161,67 @@ const AutomaticUniforms = {
|
|||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* An automatic GLSL uniform representing a texture containing edge IDs
|
||||
* from the 3D Tiles edge rendering pass. Used for edge detection and
|
||||
* avoiding z-fighting between edges and surfaces.
|
||||
*
|
||||
* @example
|
||||
* // GLSL declaration
|
||||
* uniform sampler2D czm_edgeIdTexture;
|
||||
*
|
||||
* // Get the edge ID at the current fragment
|
||||
* vec2 coords = gl_FragCoord.xy / czm_viewport.zw;
|
||||
* vec4 edgeId = texture(czm_edgeIdTexture, coords);
|
||||
*/
|
||||
czm_edgeIdTexture: new AutomaticUniform({
|
||||
size: 1,
|
||||
datatype: WebGLConstants.SAMPLER_2D,
|
||||
getValue: function (uniformState) {
|
||||
return uniformState.edgeIdTexture;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* An automatic GLSL uniform containing the edge color texture.
|
||||
* This texture contains the edge content rendered during the CESIUM_3D_TILE_EDGES pass.
|
||||
*
|
||||
* @example
|
||||
* // GLSL declaration
|
||||
* uniform sampler2D czm_edgeColorTexture;
|
||||
*
|
||||
* // Sample the edge color at the current fragment
|
||||
* vec2 coords = gl_FragCoord.xy / czm_viewport.zw;
|
||||
* vec4 edgeColor = texture(czm_edgeColorTexture, coords);
|
||||
*/
|
||||
czm_edgeColorTexture: new AutomaticUniform({
|
||||
size: 1,
|
||||
datatype: WebGLConstants.SAMPLER_2D,
|
||||
getValue: function (uniformState) {
|
||||
return uniformState.edgeColorTexture;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* An automatic GLSL uniform containing the packed depth texture produced by the
|
||||
* edge visibility pass. The depth is packed via czm_packDepth and should be
|
||||
* unpacked with czm_unpackDepth.
|
||||
*
|
||||
* @example
|
||||
* // GLSL declaration
|
||||
* uniform sampler2D czm_edgeDepthTexture;
|
||||
*
|
||||
* vec2 coords = gl_FragCoord.xy / czm_viewport.zw;
|
||||
* float d = czm_unpackDepth(texture(czm_edgeDepthTexture, coords));
|
||||
*/
|
||||
czm_edgeDepthTexture: new AutomaticUniform({
|
||||
size: 1,
|
||||
datatype: WebGLConstants.SAMPLER_2D,
|
||||
getValue: function (uniformState) {
|
||||
return uniformState.edgeDepthTexture;
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* An automatic GLSL uniform representing a 4x4 model transformation matrix that
|
||||
* transforms model coordinates to world coordinates.
|
||||
|
|
|
|||
|
|
@ -15,14 +15,15 @@ const Pass = {
|
|||
COMPUTE: 1,
|
||||
GLOBE: 2,
|
||||
TERRAIN_CLASSIFICATION: 3,
|
||||
CESIUM_3D_TILE: 4,
|
||||
CESIUM_3D_TILE_CLASSIFICATION: 5,
|
||||
CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
|
||||
OPAQUE: 7,
|
||||
TRANSLUCENT: 8,
|
||||
VOXELS: 9,
|
||||
GAUSSIAN_SPLATS: 10,
|
||||
OVERLAY: 11,
|
||||
NUMBER_OF_PASSES: 12,
|
||||
CESIUM_3D_TILE_EDGES: 4,
|
||||
CESIUM_3D_TILE: 5,
|
||||
CESIUM_3D_TILE_CLASSIFICATION: 6,
|
||||
CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 7,
|
||||
OPAQUE: 8,
|
||||
TRANSLUCENT: 9,
|
||||
VOXELS: 10,
|
||||
GAUSSIAN_SPLATS: 11,
|
||||
OVERLAY: 12,
|
||||
NUMBER_OF_PASSES: 13,
|
||||
};
|
||||
export default Object.freeze(Pass);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@ function UniformState() {
|
|||
* @type {Texture}
|
||||
*/
|
||||
this.globeDepthTexture = undefined;
|
||||
/**
|
||||
* @type {Texture}
|
||||
*/
|
||||
this.edgeIdTexture = undefined;
|
||||
/**
|
||||
* @type {Texture}
|
||||
*/
|
||||
this.edgeColorTexture = undefined;
|
||||
/**
|
||||
* @type {Texture}
|
||||
*/
|
||||
this.edgeDepthTexture = undefined; // packed depth color attachment from edge pass
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
import defined from "../Core/defined.js";
|
||||
import destroyObject from "../Core/destroyObject.js";
|
||||
import PixelFormat from "../Core/PixelFormat.js";
|
||||
import Color from "../Core/Color.js";
|
||||
import PixelDatatype from "../Renderer/PixelDatatype.js";
|
||||
import FramebufferManager from "../Renderer/FramebufferManager.js";
|
||||
import ClearCommand from "../Renderer/ClearCommand.js";
|
||||
|
||||
/**
|
||||
* Creates and manages framebuffers for edge visibility rendering.
|
||||
*
|
||||
* @param {Object} options Object with the following properties:
|
||||
*
|
||||
* @alias EdgeFramebuffer
|
||||
* @constructor
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function EdgeFramebuffer(options) {
|
||||
options = options || {};
|
||||
|
||||
// Create framebuffer manager with multiple render targets (MRT)
|
||||
// Color attachment 0: edge color output (visualization / debug)
|
||||
// Color attachment 1: R: edge type, G: featureId (metadata / ids)
|
||||
// Color attachment 2: packed depth (czm_packDepth) for edge fragments
|
||||
this._framebufferManager = new FramebufferManager({
|
||||
colorAttachmentsLength: 3, // MRT: Color + ID + Depth (packed RGBA)
|
||||
createColorAttachments: true,
|
||||
depthStencil: true,
|
||||
supportsDepthTexture: true,
|
||||
color: true,
|
||||
});
|
||||
|
||||
this._framebuffer = undefined;
|
||||
this._colorTexture = undefined;
|
||||
this._idTexture = undefined;
|
||||
this._depthTexture = undefined; // packed depth color attachment (location = 2)
|
||||
this._depthStencilTexture = undefined;
|
||||
|
||||
this._clearCommand = new ClearCommand({
|
||||
color: new Color(0.0, 0.0, 0.0, 0.0),
|
||||
depth: 1.0,
|
||||
stencil: 0,
|
||||
owner: this,
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperties(EdgeFramebuffer.prototype, {
|
||||
/**
|
||||
* Gets the framebuffer for edge rendering.
|
||||
* @memberof EdgeFramebuffer.prototype
|
||||
* @type {Framebuffer}
|
||||
* @readonly
|
||||
*/
|
||||
framebuffer: {
|
||||
get: function () {
|
||||
return this._framebuffer;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the color texture.
|
||||
* @memberof EdgeFramebuffer.prototype
|
||||
* @type {Texture}
|
||||
* @readonly
|
||||
*/
|
||||
colorTexture: {
|
||||
get: function () {
|
||||
return this._colorTexture;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the ID texture.
|
||||
* @memberof EdgeFramebuffer.prototype
|
||||
* @type {Texture}
|
||||
* @readonly
|
||||
*/
|
||||
idTexture: {
|
||||
get: function () {
|
||||
return this._idTexture;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the packed depth texture written during the edge pass.
|
||||
* @memberof EdgeFramebuffer.prototype
|
||||
* @type {Texture}
|
||||
* @readonly
|
||||
*/
|
||||
depthTexture: {
|
||||
get: function () {
|
||||
return this._depthTexture;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the depth-stencil texture.
|
||||
* @memberof EdgeFramebuffer.prototype
|
||||
* @type {Texture}
|
||||
* @readonly
|
||||
*/
|
||||
depthStencilTexture: {
|
||||
get: function () {
|
||||
return this._depthStencilTexture;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the framebuffer.
|
||||
*
|
||||
* @param {Context} context The context.
|
||||
* @param {Viewport} viewport The viewport.
|
||||
* @param {boolean} hdr Whether HDR is enabled.
|
||||
* @param {Texture} [existingColorTexture] Optional existing color texture to reuse.
|
||||
* @param {Texture} [existingDepthTexture] Optional existing depth texture to reuse.
|
||||
*
|
||||
* @returns {boolean} True if the framebuffer was updated; otherwise, false.
|
||||
*/
|
||||
EdgeFramebuffer.prototype.update = function (
|
||||
context,
|
||||
viewport,
|
||||
hdr,
|
||||
existingColorTexture,
|
||||
existingDepthTexture,
|
||||
) {
|
||||
const width = viewport.width;
|
||||
const height = viewport.height;
|
||||
|
||||
const pixelDatatype = hdr
|
||||
? context.halfFloatingPointTexture
|
||||
? PixelDatatype.HALF_FLOAT
|
||||
: PixelDatatype.FLOAT
|
||||
: PixelDatatype.UNSIGNED_BYTE;
|
||||
|
||||
const changed = this._framebufferManager.update(
|
||||
context,
|
||||
width,
|
||||
height,
|
||||
1, // No MSAA
|
||||
pixelDatatype,
|
||||
PixelFormat.RGBA,
|
||||
);
|
||||
|
||||
// Always assign framebuffer if FramebufferManager has one
|
||||
if (this._framebufferManager.framebuffer) {
|
||||
this._framebuffer = this._framebufferManager.framebuffer;
|
||||
|
||||
// Get the textures from the framebuffer manager or use existing ones
|
||||
this._colorTexture = defined(existingColorTexture)
|
||||
? existingColorTexture
|
||||
: this._framebufferManager.getColorTexture(0); // Color attachment 0
|
||||
this._idTexture = this._framebufferManager.getColorTexture(1); // Color attachment 1: ID texture
|
||||
this._depthTexture = this._framebufferManager.getColorTexture(2); // Color attachment 2: packed depth
|
||||
this._depthStencilTexture = defined(existingDepthTexture)
|
||||
? existingDepthTexture
|
||||
: this._framebufferManager.getDepthStencilTexture();
|
||||
}
|
||||
|
||||
return changed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the framebuffer using ClearCommand.
|
||||
* @deprecated Use getClearCommand() instead for proper MRT clearing.
|
||||
*
|
||||
* @param {Context} context The context.
|
||||
* @param {PassState} passState The pass state.
|
||||
* @param {Color} clearColor The clear color.
|
||||
*/
|
||||
EdgeFramebuffer.prototype.clear = function (context, passState, clearColor) {
|
||||
const clearCommand = this.getClearCommand(clearColor);
|
||||
clearCommand.execute(context, passState);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the clear command for this framebuffer.
|
||||
*
|
||||
* @param {Color} [clearColor] The clear color to use. If undefined, uses the default.
|
||||
* @returns {ClearCommand} The clear command.
|
||||
*/
|
||||
EdgeFramebuffer.prototype.getClearCommand = function (clearColor) {
|
||||
this._clearCommand.framebuffer = this._framebuffer;
|
||||
|
||||
if (defined(clearColor)) {
|
||||
Color.clone(clearColor, this._clearCommand.color);
|
||||
}
|
||||
|
||||
return this._clearCommand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the edge framebuffer, creating it if necessary.
|
||||
*
|
||||
* @param {Context} context The context.
|
||||
* @param {Viewport} viewport The viewport.
|
||||
* @param {Texture} [existingColorTexture] Optional existing color texture to reuse.
|
||||
* @param {Texture} [existingDepthTexture] Optional existing depth texture to reuse.
|
||||
*
|
||||
* @returns {Framebuffer} The edge framebuffer.
|
||||
*/
|
||||
EdgeFramebuffer.prototype.getFramebuffer = function (
|
||||
context,
|
||||
viewport,
|
||||
existingColorTexture,
|
||||
existingDepthTexture,
|
||||
) {
|
||||
this.update(
|
||||
context,
|
||||
viewport,
|
||||
false,
|
||||
existingColorTexture,
|
||||
existingDepthTexture,
|
||||
);
|
||||
return this._framebuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this object was destroyed; otherwise, false.
|
||||
*
|
||||
* @returns {boolean} True if this object was destroyed; otherwise, false.
|
||||
*/
|
||||
EdgeFramebuffer.prototype.isDestroyed = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
|
||||
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
|
||||
* <br /><br />
|
||||
* Once an object is destroyed, it should not be used; calling any function other than
|
||||
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
|
||||
* assign the return value (<code>undefined</code>) to the object as done in the example.
|
||||
*
|
||||
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
||||
*/
|
||||
EdgeFramebuffer.prototype.destroy = function () {
|
||||
this._framebufferManager =
|
||||
this._framebufferManager && this._framebufferManager.destroy();
|
||||
this._clearCommand = undefined;
|
||||
return destroyObject(this);
|
||||
};
|
||||
|
||||
export default EdgeFramebuffer;
|
||||
|
|
@ -454,6 +454,18 @@ function FrameState(context, creditDisplay, jobScheduler) {
|
|||
* @type {PickedMetadataInfo|undefined}
|
||||
*/
|
||||
this.pickedMetadataInfo = undefined;
|
||||
|
||||
/**
|
||||
* Internal toggle indicating that at least one primitive for this frame requested
|
||||
* edge visibility rendering (EXT_mesh_primitive_edge_visibility). This allows
|
||||
* lazy allocation/activation of the edge MRT without storing a Scene reference
|
||||
* on the frame state (avoids passing entire Scene through internal APIs).
|
||||
* Set by model pipeline stages when they encounter edge visibility data.
|
||||
* Consumed by Scene to flip its _enableEdgeVisibility flag.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.edgeVisibilityRequested = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1441,13 +1441,18 @@ function loadIndices(
|
|||
loader,
|
||||
accessorId,
|
||||
primitive,
|
||||
draco,
|
||||
hasFeatureIds,
|
||||
needsPostProcessing,
|
||||
frameState,
|
||||
) {
|
||||
const accessor = loader.gltfJson.accessors[accessorId];
|
||||
const bufferViewId = accessor.bufferView;
|
||||
// Infer compression / extensions directly from the glTF primitive instead of passing in flags
|
||||
const extensions = primitive.extensions ?? Frozen.EMPTY_OBJECT;
|
||||
const draco = extensions.KHR_draco_mesh_compression;
|
||||
const hasEdgeVisibility = defined(
|
||||
extensions.EXT_mesh_primitive_edge_visibility,
|
||||
);
|
||||
|
||||
if (!defined(draco) && !defined(bufferViewId)) {
|
||||
return undefined;
|
||||
|
|
@ -1470,7 +1475,10 @@ function loadIndices(
|
|||
const outputTypedArrayOnly = loadAttributesAsTypedArray;
|
||||
const outputBuffer = !outputTypedArrayOnly;
|
||||
const outputTypedArray =
|
||||
loadAttributesAsTypedArray || loadForCpuOperations || loadForClassification;
|
||||
loadAttributesAsTypedArray ||
|
||||
loadForCpuOperations ||
|
||||
loadForClassification ||
|
||||
hasEdgeVisibility;
|
||||
|
||||
// Determine what to load right now:
|
||||
//
|
||||
|
|
@ -2036,8 +2044,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,
|
||||
};
|
||||
|
||||
// Load silhouette normals
|
||||
if (defined(edgeVisibilityExtension.silhouetteNormals)) {
|
||||
const silhouetteNormalsAccessor =
|
||||
loader.gltfJson.accessors[edgeVisibilityExtension.silhouetteNormals];
|
||||
if (defined(silhouetteNormalsAccessor)) {
|
||||
const silhouetteNormalsValues = loadAccessor(
|
||||
loader,
|
||||
silhouetteNormalsAccessor,
|
||||
);
|
||||
primitive.edgeVisibility.silhouetteNormals = silhouetteNormalsValues;
|
||||
}
|
||||
}
|
||||
|
||||
// Load line strings
|
||||
if (defined(edgeVisibilityExtension.lineStrings)) {
|
||||
primitivePlan.edgeVisibility.lineStrings =
|
||||
edgeVisibilityExtension.lineStrings;
|
||||
}
|
||||
}
|
||||
|
||||
//support the latest glTF spec and the legacy extension
|
||||
const spzExtension = fetchSpzExtensionFrom(extensions);
|
||||
|
||||
if (defined(spzExtension)) {
|
||||
needsPostProcessing = true;
|
||||
primitivePlan.needsGaussianSplats = true;
|
||||
|
|
@ -2108,7 +2152,6 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) {
|
|||
loader,
|
||||
indices,
|
||||
gltfPrimitive,
|
||||
draco,
|
||||
hasFeatureIds,
|
||||
needsPostProcessing,
|
||||
frameState,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import EdgeDetectionStageFS from "../../Shaders/Model/EdgeDetectionStageFS.js";
|
||||
|
||||
/**
|
||||
* Performs the screen-space edge visibility / composition pass. This stage does not
|
||||
* build edge geometry itself; that work is handled earlier by {@link EdgeVisibilityPipelineStage},
|
||||
* which extracts unique model edges and writes them during a dedicated edge render pass
|
||||
* into edge ID / color targets. The fragment logic added here then:
|
||||
* <ul>
|
||||
* <li>Samples the edge render targets (edge color + per-edge feature ID)</li>
|
||||
* <li>Compares per-edge feature IDs with underlying surface feature IDs to suppress
|
||||
* edges that belong to filtered or hidden features</li>
|
||||
* <li>Performs depth-based tests (e.g., against globe or scene depth) to discard
|
||||
* occluded edges</li>
|
||||
* </ul>
|
||||
* In summary: EdgeVisibilityPipelineStage = generate & encode edges; this stage = decide which of
|
||||
* those encoded edges are actually visible in the final frame and composite them.
|
||||
*
|
||||
* @namespace EdgeDetectionPipelineStage
|
||||
* @private
|
||||
*/
|
||||
const EdgeDetectionPipelineStage = {
|
||||
name: "EdgeDetectionPipelineStage",
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a primitive by injecting the fragment shader logic that consumes the
|
||||
* intermediate edge buffers produced by the edge geometry pass. It adds code to:
|
||||
* <ul>
|
||||
* <li>Read edge color / edge ID MRT outputs</li>
|
||||
* <li>Apply depth & feature ID based rejection</li>
|
||||
* <li>Emit final edge color for composition</li>
|
||||
* </ul>
|
||||
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
|
||||
* @private
|
||||
*/
|
||||
EdgeDetectionPipelineStage.process = function (renderResources) {
|
||||
const shaderBuilder = renderResources.shaderBuilder;
|
||||
|
||||
shaderBuilder.addFragmentLines([EdgeDetectionStageFS]);
|
||||
};
|
||||
|
||||
export default EdgeDetectionPipelineStage;
|
||||
|
|
@ -0,0 +1,800 @@
|
|||
import Buffer from "../../Renderer/Buffer.js";
|
||||
import BufferUsage from "../../Renderer/BufferUsage.js";
|
||||
import VertexArray from "../../Renderer/VertexArray.js";
|
||||
import defined from "../../Core/defined.js";
|
||||
import IndexDatatype from "../../Core/IndexDatatype.js";
|
||||
import ComponentDatatype from "../../Core/ComponentDatatype.js";
|
||||
import PrimitiveType from "../../Core/PrimitiveType.js";
|
||||
import Cartesian3 from "../../Core/Cartesian3.js";
|
||||
import Pass from "../../Renderer/Pass.js";
|
||||
import ShaderDestination from "../../Renderer/ShaderDestination.js";
|
||||
import EdgeVisibilityStageFS from "../../Shaders/Model/EdgeVisibilityStageFS.js";
|
||||
import ModelUtility from "./ModelUtility.js";
|
||||
import ModelReader from "./ModelReader.js";
|
||||
import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
|
||||
|
||||
/**
|
||||
* Builds derived line geometry for model edges using EXT_mesh_primitive_edge_visibility data.
|
||||
* It parses the encoded edge visibility bits, creates a separate edge-domain vertex array with
|
||||
* per-edge attributes (edge type, optional feature ID, silhouette normal, adjacent face normals),
|
||||
* sets up the required shader defines / varyings, and stores the resulting line list geometry on
|
||||
* the render resources for a later edge rendering pass.
|
||||
*
|
||||
* @namespace EdgeVisibilityPipelineStage
|
||||
* @private
|
||||
*/
|
||||
const EdgeVisibilityPipelineStage = {
|
||||
name: "EdgeVisibilityPipelineStage",
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a primitive to derive edge geometry and shader bindings. This modifies the render resources by:
|
||||
* <ul>
|
||||
* <li>Adding shader defines (<code>HAS_EDGE_VISIBILITY</code>, <code>HAS_EDGE_VISIBILITY_MRT</code>)</li>
|
||||
* <li>Injecting the fragment shader logic that outputs edge color / feature information</li>
|
||||
* <li>Adding per-vertex attributes: edge type, optional feature ID, silhouette normal, and adjacent face normals</li>
|
||||
* <li>Adding varyings to pass these attributes to the fragment stage</li>
|
||||
* <li>Creating and storing a derived line list vertex array in <code>renderResources.edgeGeometry</code></li>
|
||||
* </ul>
|
||||
* If the primitive does not contain edge visibility data, the function returns early.
|
||||
*
|
||||
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
|
||||
* @param {ModelComponents.Primitive} primitive The primitive to be rendered
|
||||
* @param {FrameState} frameState The frame state
|
||||
* @private
|
||||
*/
|
||||
EdgeVisibilityPipelineStage.process = function (
|
||||
renderResources,
|
||||
primitive,
|
||||
frameState,
|
||||
) {
|
||||
if (!defined(primitive.edgeVisibility)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback request: mark that edge visibility is needed this frame.
|
||||
frameState.edgeVisibilityRequested = true;
|
||||
|
||||
const shaderBuilder = renderResources.shaderBuilder;
|
||||
|
||||
// Add shader defines and fragment code
|
||||
shaderBuilder.addDefine(
|
||||
"HAS_EDGE_VISIBILITY",
|
||||
undefined,
|
||||
ShaderDestination.BOTH,
|
||||
);
|
||||
shaderBuilder.addDefine(
|
||||
"HAS_EDGE_VISIBILITY_MRT",
|
||||
undefined,
|
||||
ShaderDestination.FRAGMENT,
|
||||
);
|
||||
shaderBuilder.addFragmentLines(EdgeVisibilityStageFS);
|
||||
|
||||
// Add a uniform to distinguish between original geometry pass and edge pass
|
||||
shaderBuilder.addUniform("bool", "u_isEdgePass", ShaderDestination.BOTH);
|
||||
|
||||
// Add edge type attribute and varying
|
||||
const edgeTypeLocation = shaderBuilder.addAttribute("float", "a_edgeType");
|
||||
shaderBuilder.addVarying("float", "v_edgeType", "flat");
|
||||
|
||||
// Add edge feature ID attribute and varying
|
||||
const edgeFeatureIdLocation = shaderBuilder.addAttribute(
|
||||
"float",
|
||||
"a_edgeFeatureId",
|
||||
);
|
||||
|
||||
// Add silhouette normal attribute and varying for silhouette edges
|
||||
const silhouetteNormalLocation = shaderBuilder.addAttribute(
|
||||
"vec3",
|
||||
"a_silhouetteNormal",
|
||||
);
|
||||
shaderBuilder.addVarying("vec3", "v_silhouetteNormalView", "flat");
|
||||
|
||||
// Add face normal attributes for silhouette detection
|
||||
const faceNormalALocation = shaderBuilder.addAttribute(
|
||||
"vec3",
|
||||
"a_faceNormalA",
|
||||
);
|
||||
const faceNormalBLocation = shaderBuilder.addAttribute(
|
||||
"vec3",
|
||||
"a_faceNormalB",
|
||||
);
|
||||
shaderBuilder.addVarying("vec3", "v_faceNormalAView", "flat");
|
||||
shaderBuilder.addVarying("vec3", "v_faceNormalBView", "flat");
|
||||
|
||||
// Add varying for view space position for perspective-correct silhouette detection
|
||||
|
||||
// Pass edge type, silhouette normal, and face normals from vertex to fragment shader
|
||||
shaderBuilder.addFunctionLines("setDynamicVaryingsVS", [
|
||||
"#ifdef HAS_EDGE_VISIBILITY",
|
||||
" if (u_isEdgePass) {",
|
||||
" v_edgeType = a_edgeType;",
|
||||
"#ifdef HAS_EDGE_FEATURE_ID",
|
||||
" v_featureId_0 = a_edgeFeatureId;",
|
||||
"#endif",
|
||||
" // Transform normals from model space to view space",
|
||||
" v_silhouetteNormalView = czm_normal * a_silhouetteNormal;",
|
||||
" v_faceNormalAView = czm_normal * a_faceNormalA;",
|
||||
" v_faceNormalBView = czm_normal * a_faceNormalB;",
|
||||
" }",
|
||||
"#endif",
|
||||
]);
|
||||
|
||||
// Build triangle adjacency (mapping edges to adjacent triangles) and compute per-triangle face normals.
|
||||
const adjacencyData = buildTriangleAdjacency(primitive);
|
||||
|
||||
const edgeResult = extractVisibleEdges(primitive);
|
||||
|
||||
if (
|
||||
!defined(edgeResult) ||
|
||||
!defined(edgeResult.edgeIndices) ||
|
||||
edgeResult.edgeIndices.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate paired face normals for each unique edge (used to classify silhouette edges in the shader).
|
||||
const edgeFaceNormals = generateEdgeFaceNormals(
|
||||
adjacencyData,
|
||||
edgeResult.edgeIndices,
|
||||
);
|
||||
|
||||
// Create edge-domain line list geometry (2 vertices per edge) with all required attributes.
|
||||
const edgeGeometry = createCPULineEdgeGeometry(
|
||||
edgeResult.edgeIndices,
|
||||
edgeResult.edgeData,
|
||||
renderResources,
|
||||
frameState.context,
|
||||
edgeTypeLocation,
|
||||
silhouetteNormalLocation,
|
||||
faceNormalALocation,
|
||||
faceNormalBLocation,
|
||||
edgeFeatureIdLocation,
|
||||
primitive.edgeVisibility,
|
||||
edgeFaceNormals,
|
||||
);
|
||||
|
||||
if (!defined(edgeGeometry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (edgeGeometry.hasEdgeFeatureIds) {
|
||||
shaderBuilder.addDefine(
|
||||
"HAS_EDGE_FEATURE_ID",
|
||||
undefined,
|
||||
ShaderDestination.BOTH,
|
||||
);
|
||||
}
|
||||
|
||||
// Set default value for u_isEdgePass uniform (false for original geometry pass). A later pass overrides this.
|
||||
renderResources.uniformMap.u_isEdgePass = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Store edge geometry metadata so the renderer can issue a separate edges pass.
|
||||
renderResources.edgeGeometry = {
|
||||
vertexArray: edgeGeometry.vertexArray,
|
||||
indexCount: edgeGeometry.indexCount,
|
||||
primitiveType: PrimitiveType.LINES,
|
||||
pass: Pass.CESIUM_3D_TILE_EDGES,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build triangle adjacency information and per-triangle face normals in model space.
|
||||
* The adjacency map associates an undirected edge (minIndex,maxIndex) with the indices
|
||||
* of up to two adjacent triangles. Face normals are normalized and stored sequentially.
|
||||
*
|
||||
* @param {ModelComponents.Primitive} primitive The primitive containing triangle index + position data
|
||||
* @returns {{edgeMap:Map<string, number[]>, faceNormals:Float32Array, triangleCount:number}}
|
||||
* @private
|
||||
*/
|
||||
function buildTriangleAdjacency(primitive) {
|
||||
const indices = primitive.indices;
|
||||
if (!defined(indices)) {
|
||||
return {
|
||||
edgeMap: new Map(),
|
||||
faceNormals: new Float32Array(0),
|
||||
triangleCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const triangleIndexArray = indices.typedArray;
|
||||
const triangleCount = Math.floor(triangleIndexArray.length / 3);
|
||||
|
||||
// Get vertex positions for face normal calculation
|
||||
const positionAttribute = ModelUtility.getAttributeBySemantic(
|
||||
primitive,
|
||||
VertexAttributeSemantic.POSITION,
|
||||
);
|
||||
|
||||
// Retrieve raw (possibly quantized) position data. If the attribute is quantized
|
||||
// we must dequantize on the CPU here because we compute face normals and silhouette
|
||||
// classification data before the vertex shader's dequantization stage runs.
|
||||
let positions = defined(positionAttribute.typedArray)
|
||||
? positionAttribute.typedArray
|
||||
: ModelReader.readAttributeAsTypedArray(positionAttribute);
|
||||
|
||||
const quantization = positionAttribute.quantization;
|
||||
if (defined(quantization) && !quantization.octEncoded) {
|
||||
const count = positions.length; // length is 3 * vertexCount
|
||||
const dequantized = new Float32Array(count);
|
||||
const offset = quantization.quantizedVolumeOffset;
|
||||
const step = quantization.quantizedVolumeStepSize;
|
||||
for (let i = 0; i < count; i += 3) {
|
||||
dequantized[i] = offset.x + positions[i] * step.x;
|
||||
dequantized[i + 1] = offset.y + positions[i + 1] * step.y;
|
||||
dequantized[i + 2] = offset.z + positions[i + 2] * step.z;
|
||||
}
|
||||
positions = dequantized;
|
||||
}
|
||||
|
||||
// Build edge map: key = "min,max", value = [triangleA, triangleB?]
|
||||
const edgeMap = new Map();
|
||||
|
||||
// Calculate face normals for each triangle (model space)
|
||||
const faceNormals = new Float32Array(triangleCount * 3);
|
||||
|
||||
// Scratch vectors to avoid heap allocations per triangle
|
||||
const scratchP0 = new Cartesian3();
|
||||
const scratchP1 = new Cartesian3();
|
||||
const scratchP2 = new Cartesian3();
|
||||
const scratchE1 = new Cartesian3();
|
||||
const scratchE2 = new Cartesian3();
|
||||
const scratchCross = new Cartesian3();
|
||||
|
||||
function processEdge(a, b, triIndex) {
|
||||
const edgeKey = `${a < b ? a : b},${a < b ? b : a}`;
|
||||
let list = edgeMap.get(edgeKey);
|
||||
if (!defined(list)) {
|
||||
list = [];
|
||||
edgeMap.set(edgeKey, list);
|
||||
}
|
||||
if (list.length < 2) {
|
||||
list.push(triIndex);
|
||||
}
|
||||
}
|
||||
|
||||
for (let t = 0; t < triangleCount; t++) {
|
||||
const base = t * 3;
|
||||
const i0 = triangleIndexArray[base];
|
||||
const i1 = triangleIndexArray[base + 1];
|
||||
const i2 = triangleIndexArray[base + 2];
|
||||
|
||||
const i0o = i0 * 3;
|
||||
const i1o = i1 * 3;
|
||||
const i2o = i2 * 3;
|
||||
|
||||
scratchP0.x = positions[i0o];
|
||||
scratchP0.y = positions[i0o + 1];
|
||||
scratchP0.z = positions[i0o + 2];
|
||||
scratchP1.x = positions[i1o];
|
||||
scratchP1.y = positions[i1o + 1];
|
||||
scratchP1.z = positions[i1o + 2];
|
||||
scratchP2.x = positions[i2o];
|
||||
scratchP2.y = positions[i2o + 1];
|
||||
scratchP2.z = positions[i2o + 2];
|
||||
|
||||
Cartesian3.subtract(scratchP1, scratchP0, scratchE1);
|
||||
Cartesian3.subtract(scratchP2, scratchP0, scratchE2);
|
||||
Cartesian3.cross(scratchE1, scratchE2, scratchCross);
|
||||
Cartesian3.normalize(scratchCross, scratchCross);
|
||||
|
||||
faceNormals[base] = scratchCross.x;
|
||||
faceNormals[base + 1] = scratchCross.y;
|
||||
faceNormals[base + 2] = scratchCross.z;
|
||||
|
||||
// Edges
|
||||
processEdge(i0, i1, t);
|
||||
processEdge(i1, i2, t);
|
||||
processEdge(i2, i0, t);
|
||||
}
|
||||
|
||||
return { edgeMap, faceNormals, triangleCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* For each unique edge produce a pair of face normals (A,B). For boundary edges where only a single
|
||||
* adjacent triangle exists, the second normal is synthesized as the negation of the first to allow
|
||||
* the shader to reason about front/back facing transitions uniformly.
|
||||
*
|
||||
* @param {{edgeMap:Map<string,number[]>, faceNormals:Float32Array}} adjacencyData The adjacency data from buildTriangleAdjacency
|
||||
* @param {number[]} edgeIndices Packed array of 2 vertex indices per edge
|
||||
* @returns {Float32Array} Packed array: 6 floats per edge (normalA.xyz, normalB.xyz)
|
||||
* @private
|
||||
*/
|
||||
function generateEdgeFaceNormals(adjacencyData, edgeIndices) {
|
||||
const { edgeMap, faceNormals } = adjacencyData;
|
||||
const numEdges = edgeIndices.length / 2;
|
||||
|
||||
// Each edge needs 2 face normals (left and right side)
|
||||
const edgeFaceNormals = new Float32Array(numEdges * 6); // 2 normals * 3 components each
|
||||
|
||||
for (let i = 0; i < numEdges; i++) {
|
||||
const a = edgeIndices[i * 2];
|
||||
const b = edgeIndices[i * 2 + 1];
|
||||
const edgeKey = `${a < b ? a : b},${a < b ? b : a}`;
|
||||
const triangleList = edgeMap.get(edgeKey);
|
||||
|
||||
// Expect at least one triangle; silently skip if not found (defensive)
|
||||
if (!defined(triangleList) || triangleList.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tA = triangleList[0];
|
||||
const aBase = tA * 3;
|
||||
const nAx = faceNormals[aBase];
|
||||
const nAy = faceNormals[aBase + 1];
|
||||
const nAz = faceNormals[aBase + 2];
|
||||
|
||||
let nBx;
|
||||
let nBy;
|
||||
let nBz;
|
||||
if (triangleList.length > 1) {
|
||||
const tB = triangleList[1];
|
||||
const bBase = tB * 3;
|
||||
nBx = faceNormals[bBase];
|
||||
nBy = faceNormals[bBase + 1];
|
||||
nBz = faceNormals[bBase + 2];
|
||||
} else {
|
||||
// Boundary edge – synthesize opposite normal
|
||||
nBx = -nAx;
|
||||
nBy = -nAy;
|
||||
nBz = -nAz;
|
||||
}
|
||||
|
||||
const baseIdx = i * 6;
|
||||
edgeFaceNormals[baseIdx] = nAx;
|
||||
edgeFaceNormals[baseIdx + 1] = nAy;
|
||||
edgeFaceNormals[baseIdx + 2] = nAz;
|
||||
edgeFaceNormals[baseIdx + 3] = nBx;
|
||||
edgeFaceNormals[baseIdx + 4] = nBy;
|
||||
edgeFaceNormals[baseIdx + 5] = nBz;
|
||||
}
|
||||
|
||||
return edgeFaceNormals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the EXT_mesh_primitive_edge_visibility 2-bit edge encoding and extract
|
||||
* a unique set of edges that should be considered for rendering. Edge types:
|
||||
* <ul>
|
||||
* <li>0 HIDDEN - skipped</li>
|
||||
* <li>1 SILHOUETTE - candidates for conditional display based on facing</li>
|
||||
* <li>2 HARD - always displayed</li>
|
||||
* <li>3 REPEATED - secondary encoding for a hard edge (treated same as 2)</li>
|
||||
* </ul>
|
||||
* Deduplicates edges shared by adjacent triangles and records per-edge metadata.
|
||||
*
|
||||
* @param {ModelComponents.Primitive} primitive The primitive with EXT_mesh_primitive_edge_visibility data
|
||||
* @returns {{edgeIndices:number[], edgeData:Object[], silhouetteEdgeCount:number}} Edge extraction result
|
||||
* @private
|
||||
*/
|
||||
function extractVisibleEdges(primitive) {
|
||||
const edgeVisibility = primitive.edgeVisibility;
|
||||
const visibility = edgeVisibility.visibility;
|
||||
const indices = primitive.indices;
|
||||
|
||||
if (!defined(visibility) || !defined(indices)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const triangleIndexArray = indices.typedArray;
|
||||
const vertexCount = primitive.attributes[0].count;
|
||||
const edgeIndices = [];
|
||||
const edgeData = [];
|
||||
const seenEdgeHashes = new Set();
|
||||
let silhouetteEdgeCount = 0;
|
||||
|
||||
// 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++;
|
||||
|
||||
if (byteIndex >= visibility.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
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 big = Math.max(a, b);
|
||||
const hash = small * vertexCount + 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],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { edgeIndices, edgeData, silhouetteEdgeCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* modifying or duplicating the original triangle mesh. Two vertices are generated per unique edge.
|
||||
*
|
||||
* @param {number[]} edgeIndices Packed array [a0,b0, a1,b1, ...] of vertex indices into the source mesh
|
||||
* @param {Object[]} edgeData Array of edge metadata including edge type and silhouette normal lookup index
|
||||
* @param {PrimitiveRenderResources} renderResources The render resources for the primitive
|
||||
* @param {Context} context The WebGL rendering context
|
||||
* @param {number} edgeTypeLocation Shader attribute location for the edge type
|
||||
* @param {number} silhouetteNormalLocation Shader attribute location for input silhouette normal
|
||||
* @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 {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
|
||||
* @private
|
||||
*/
|
||||
function createCPULineEdgeGeometry(
|
||||
edgeIndices,
|
||||
edgeData,
|
||||
renderResources,
|
||||
context,
|
||||
edgeTypeLocation,
|
||||
silhouetteNormalLocation,
|
||||
faceNormalALocation,
|
||||
faceNormalBLocation,
|
||||
edgeFeatureIdLocation,
|
||||
edgeVisibility,
|
||||
edgeFaceNormals,
|
||||
) {
|
||||
if (!defined(edgeIndices) || edgeIndices.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const numEdges = edgeData.length;
|
||||
const vertsPerEdge = 2;
|
||||
const totalVerts = numEdges * vertsPerEdge;
|
||||
|
||||
// Always use location 0 for position to avoid conflicts
|
||||
const positionLocation = 0;
|
||||
|
||||
// Get original vertex positions
|
||||
const positionAttribute = ModelUtility.getAttributeBySemantic(
|
||||
renderResources.runtimePrimitive.primitive,
|
||||
VertexAttributeSemantic.POSITION,
|
||||
);
|
||||
const srcPos = defined(positionAttribute.typedArray)
|
||||
? positionAttribute.typedArray
|
||||
: ModelReader.readAttributeAsTypedArray(positionAttribute);
|
||||
|
||||
// Create edge-domain vertices (2 per edge)
|
||||
const edgePosArray = new Float32Array(totalVerts * 3);
|
||||
const edgeTypeArray = new Float32Array(totalVerts);
|
||||
const silhouetteNormalArray = new Float32Array(totalVerts * 3);
|
||||
const faceNormalAArray = new Float32Array(totalVerts * 3);
|
||||
const faceNormalBArray = new Float32Array(totalVerts * 3);
|
||||
let p = 0;
|
||||
|
||||
const maxSrcVertex = srcPos.length / 3 - 1;
|
||||
|
||||
for (let i = 0; i < numEdges; i++) {
|
||||
const a = edgeIndices[i * 2];
|
||||
const b = edgeIndices[i * 2 + 1];
|
||||
|
||||
// Validate vertex indices
|
||||
if (a < 0 || b < 0 || a > maxSrcVertex || b > maxSrcVertex) {
|
||||
// Fill with zeros to maintain indexing
|
||||
edgePosArray[p++] = 0;
|
||||
edgePosArray[p++] = 0;
|
||||
edgePosArray[p++] = 0;
|
||||
edgePosArray[p++] = 0;
|
||||
edgePosArray[p++] = 0;
|
||||
edgePosArray[p++] = 0;
|
||||
edgeTypeArray[i * 2] = 0;
|
||||
edgeTypeArray[i * 2 + 1] = 0;
|
||||
// Fill with default values
|
||||
const normalIdx = i * 2;
|
||||
silhouetteNormalArray[normalIdx * 3] = 0;
|
||||
silhouetteNormalArray[normalIdx * 3 + 1] = 0;
|
||||
silhouetteNormalArray[normalIdx * 3 + 2] = 1;
|
||||
silhouetteNormalArray[(normalIdx + 1) * 3] = 0;
|
||||
silhouetteNormalArray[(normalIdx + 1) * 3 + 1] = 0;
|
||||
silhouetteNormalArray[(normalIdx + 1) * 3 + 2] = 1;
|
||||
|
||||
// Fill face normals with default values
|
||||
faceNormalAArray[normalIdx * 3] = 0;
|
||||
faceNormalAArray[normalIdx * 3 + 1] = 0;
|
||||
faceNormalAArray[normalIdx * 3 + 2] = 1;
|
||||
faceNormalAArray[(normalIdx + 1) * 3] = 0;
|
||||
faceNormalAArray[(normalIdx + 1) * 3 + 1] = 0;
|
||||
faceNormalAArray[(normalIdx + 1) * 3 + 2] = 1;
|
||||
|
||||
faceNormalBArray[normalIdx * 3] = 0;
|
||||
faceNormalBArray[normalIdx * 3 + 1] = 0;
|
||||
faceNormalBArray[normalIdx * 3 + 2] = 1;
|
||||
faceNormalBArray[(normalIdx + 1) * 3] = 0;
|
||||
faceNormalBArray[(normalIdx + 1) * 3 + 1] = 0;
|
||||
faceNormalBArray[(normalIdx + 1) * 3 + 2] = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const ax = srcPos[a * 3];
|
||||
const ay = srcPos[a * 3 + 1];
|
||||
const az = srcPos[a * 3 + 2];
|
||||
const bx = srcPos[b * 3];
|
||||
const by = srcPos[b * 3 + 1];
|
||||
const bz = srcPos[b * 3 + 2];
|
||||
|
||||
// Add edge endpoints
|
||||
edgePosArray[p++] = ax;
|
||||
edgePosArray[p++] = ay;
|
||||
edgePosArray[p++] = az;
|
||||
edgePosArray[p++] = bx;
|
||||
edgePosArray[p++] = by;
|
||||
edgePosArray[p++] = bz;
|
||||
|
||||
const rawType = edgeData[i].edgeType;
|
||||
const t = rawType / 255.0;
|
||||
|
||||
edgeTypeArray[i * 2] = t;
|
||||
edgeTypeArray[i * 2 + 1] = t;
|
||||
|
||||
// Add silhouette normal for silhouette edges (type 1)
|
||||
let normalX = 0,
|
||||
normalY = 0,
|
||||
normalZ = 1; // Default normal pointing up
|
||||
|
||||
if (rawType === 1 && defined(edgeVisibility.silhouetteNormals)) {
|
||||
const mateVertexIndex = edgeData[i].mateVertexIndex;
|
||||
if (
|
||||
mateVertexIndex >= 0 &&
|
||||
mateVertexIndex < edgeVisibility.silhouetteNormals.length
|
||||
) {
|
||||
const silhouetteNormals = edgeVisibility.silhouetteNormals;
|
||||
const normal = silhouetteNormals[mateVertexIndex];
|
||||
|
||||
if (defined(normal)) {
|
||||
normalX = normal.x;
|
||||
normalY = normal.y;
|
||||
normalZ = normal.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set silhouette normal for both edge endpoints
|
||||
const normalIdx = i * 2;
|
||||
silhouetteNormalArray[normalIdx * 3] = normalX;
|
||||
silhouetteNormalArray[normalIdx * 3 + 1] = normalY;
|
||||
silhouetteNormalArray[normalIdx * 3 + 2] = normalZ;
|
||||
silhouetteNormalArray[(normalIdx + 1) * 3] = normalX;
|
||||
silhouetteNormalArray[(normalIdx + 1) * 3 + 1] = normalY;
|
||||
silhouetteNormalArray[(normalIdx + 1) * 3 + 2] = normalZ;
|
||||
|
||||
// Set face normals for both edge endpoints
|
||||
const faceNormalIdx = i * 6; // 6 floats per edge (2 normals * 3 components)
|
||||
const normalAX = edgeFaceNormals[faceNormalIdx];
|
||||
const normalAY = edgeFaceNormals[faceNormalIdx + 1];
|
||||
const normalAZ = edgeFaceNormals[faceNormalIdx + 2];
|
||||
const normalBX = edgeFaceNormals[faceNormalIdx + 3];
|
||||
const normalBY = edgeFaceNormals[faceNormalIdx + 4];
|
||||
const normalBZ = edgeFaceNormals[faceNormalIdx + 5];
|
||||
|
||||
// Face normal A for both endpoints
|
||||
faceNormalAArray[normalIdx * 3] = normalAX;
|
||||
faceNormalAArray[normalIdx * 3 + 1] = normalAY;
|
||||
faceNormalAArray[normalIdx * 3 + 2] = normalAZ;
|
||||
faceNormalAArray[(normalIdx + 1) * 3] = normalAX;
|
||||
faceNormalAArray[(normalIdx + 1) * 3 + 1] = normalAY;
|
||||
faceNormalAArray[(normalIdx + 1) * 3 + 2] = normalAZ;
|
||||
|
||||
// Face normal B for both endpoints
|
||||
faceNormalBArray[normalIdx * 3] = normalBX;
|
||||
faceNormalBArray[normalIdx * 3 + 1] = normalBY;
|
||||
faceNormalBArray[normalIdx * 3 + 2] = normalBZ;
|
||||
faceNormalBArray[(normalIdx + 1) * 3] = normalBX;
|
||||
faceNormalBArray[(normalIdx + 1) * 3 + 1] = normalBY;
|
||||
faceNormalBArray[(normalIdx + 1) * 3 + 2] = normalBZ;
|
||||
}
|
||||
|
||||
// Create vertex buffers
|
||||
const edgePosBuffer = Buffer.createVertexBuffer({
|
||||
context,
|
||||
typedArray: edgePosArray,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
});
|
||||
const edgeTypeBuffer = Buffer.createVertexBuffer({
|
||||
context,
|
||||
typedArray: edgeTypeArray,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
});
|
||||
const silhouetteNormalBuffer = Buffer.createVertexBuffer({
|
||||
context,
|
||||
typedArray: silhouetteNormalArray,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
});
|
||||
const faceNormalABuffer = Buffer.createVertexBuffer({
|
||||
context,
|
||||
typedArray: faceNormalAArray,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
});
|
||||
const faceNormalBBuffer = Buffer.createVertexBuffer({
|
||||
context,
|
||||
typedArray: faceNormalBArray,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
});
|
||||
|
||||
// Create sequential indices for line pairs
|
||||
const useU32 = totalVerts > 65534;
|
||||
const idx = new Array(totalVerts);
|
||||
for (let i = 0; i < totalVerts; i++) {
|
||||
idx[i] = i;
|
||||
}
|
||||
|
||||
const indexBuffer = Buffer.createIndexBuffer({
|
||||
context,
|
||||
typedArray: useU32 ? new Uint32Array(idx) : new Uint16Array(idx),
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
indexDatatype: useU32
|
||||
? IndexDatatype.UNSIGNED_INT
|
||||
: IndexDatatype.UNSIGNED_SHORT,
|
||||
});
|
||||
|
||||
// Create vertex array with position, edge type, silhouette normal, and face normal attributes
|
||||
const attributes = [
|
||||
{
|
||||
index: positionLocation,
|
||||
vertexBuffer: edgePosBuffer,
|
||||
componentsPerAttribute: 3,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
normalize: false,
|
||||
},
|
||||
{
|
||||
index: edgeTypeLocation,
|
||||
vertexBuffer: edgeTypeBuffer,
|
||||
componentsPerAttribute: 1,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
normalize: false,
|
||||
},
|
||||
{
|
||||
index: silhouetteNormalLocation,
|
||||
vertexBuffer: silhouetteNormalBuffer,
|
||||
componentsPerAttribute: 3,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
normalize: false,
|
||||
},
|
||||
{
|
||||
index: faceNormalALocation,
|
||||
vertexBuffer: faceNormalABuffer,
|
||||
componentsPerAttribute: 3,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
normalize: false,
|
||||
},
|
||||
{
|
||||
index: faceNormalBLocation,
|
||||
vertexBuffer: faceNormalBBuffer,
|
||||
componentsPerAttribute: 3,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
normalize: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Get feature ID from original geometry
|
||||
const primitive = renderResources.runtimePrimitive.primitive;
|
||||
const getFeatureIdForEdge = function () {
|
||||
// Try to get the first feature ID from the original primitive
|
||||
if (defined(primitive.featureIds) && primitive.featureIds.length > 0) {
|
||||
const firstFeatureIdSet = primitive.featureIds[0];
|
||||
|
||||
// Handle FeatureIdAttribute objects directly using setIndex
|
||||
if (defined(firstFeatureIdSet.setIndex)) {
|
||||
const featureIdAttribute = primitive.attributes.find(
|
||||
(attr) =>
|
||||
attr.semantic === VertexAttributeSemantic.FEATURE_ID &&
|
||||
attr.setIndex === firstFeatureIdSet.setIndex,
|
||||
);
|
||||
|
||||
if (defined(featureIdAttribute)) {
|
||||
const featureIds = defined(featureIdAttribute.typedArray)
|
||||
? featureIdAttribute.typedArray
|
||||
: ModelReader.readAttributeAsTypedArray(featureIdAttribute);
|
||||
|
||||
// Create edge feature ID buffer based on edge indices
|
||||
const edgeFeatureIds = new Float32Array(totalVerts);
|
||||
for (let i = 0; i < numEdges; i++) {
|
||||
const a = edgeIndices[i * 2];
|
||||
const featureId = a < featureIds.length ? featureIds[a] : 0;
|
||||
edgeFeatureIds[i * 2] = featureId;
|
||||
edgeFeatureIds[i * 2 + 1] = featureId;
|
||||
}
|
||||
|
||||
return edgeFeatureIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const edgeFeatureIds = getFeatureIdForEdge();
|
||||
const hasEdgeFeatureIds = defined(edgeFeatureIds);
|
||||
|
||||
if (hasEdgeFeatureIds) {
|
||||
const edgeFeatureIdBuffer = Buffer.createVertexBuffer({
|
||||
context,
|
||||
typedArray: edgeFeatureIds,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
});
|
||||
|
||||
attributes.push({
|
||||
index: edgeFeatureIdLocation,
|
||||
vertexBuffer: edgeFeatureIdBuffer,
|
||||
componentsPerAttribute: 1,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
normalize: false,
|
||||
});
|
||||
}
|
||||
|
||||
const vertexArray = new VertexArray({ context, indexBuffer, attributes });
|
||||
|
||||
if (!vertexArray || totalVerts === 0 || totalVerts % 2 !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
vertexArray,
|
||||
indexBuffer,
|
||||
indexCount: totalVerts,
|
||||
hasEdgeFeatureIds,
|
||||
};
|
||||
}
|
||||
|
||||
export default EdgeVisibilityPipelineStage;
|
||||
|
|
@ -50,6 +50,9 @@ function ModelDrawCommand(options) {
|
|||
const runtimePrimitive = renderResources.runtimePrimitive;
|
||||
this._runtimePrimitive = runtimePrimitive;
|
||||
|
||||
// Store render resources for edge command creation
|
||||
this._primitiveRenderResources = renderResources;
|
||||
|
||||
// If the command is translucent, or if the primitive's material is
|
||||
// double-sided, then back-face culling is automatically disabled for
|
||||
// the command. The user value for back-face culling will be ignored.
|
||||
|
|
@ -73,6 +76,8 @@ function ModelDrawCommand(options) {
|
|||
|
||||
const needsSilhouetteCommands = hasSilhouette;
|
||||
|
||||
const needsEdgeCommands = defined(renderResources.edgeGeometry);
|
||||
|
||||
this._command = command;
|
||||
|
||||
// None of the derived commands (non-2D) use a different model matrix
|
||||
|
|
@ -96,6 +101,7 @@ function ModelDrawCommand(options) {
|
|||
this._needsTranslucentCommand = needsTranslucentCommand;
|
||||
this._needsSkipLevelOfDetailCommands = needsSkipLevelOfDetailCommands;
|
||||
this._needsSilhouetteCommands = needsSilhouetteCommands;
|
||||
this._needsEdgeCommands = needsEdgeCommands;
|
||||
|
||||
// Derived commands
|
||||
this._originalCommand = undefined;
|
||||
|
|
@ -104,6 +110,7 @@ function ModelDrawCommand(options) {
|
|||
this._skipLodStencilCommand = undefined;
|
||||
this._silhouetteModelCommand = undefined;
|
||||
this._silhouetteColorCommand = undefined;
|
||||
this._edgeCommand = undefined;
|
||||
|
||||
// All derived commands (including 2D commands)
|
||||
this._derivedCommands = [];
|
||||
|
|
@ -215,6 +222,19 @@ function initialize(drawCommand) {
|
|||
derivedCommands.push(drawCommand._silhouetteModelCommand);
|
||||
derivedCommands.push(drawCommand._silhouetteColorCommand);
|
||||
}
|
||||
|
||||
if (drawCommand._needsEdgeCommands) {
|
||||
const renderResources = drawCommand._primitiveRenderResources;
|
||||
drawCommand._edgeCommand = new ModelDerivedCommand({
|
||||
command: deriveEdgeCommand(command, renderResources, model),
|
||||
updateShadows: false,
|
||||
updateBackFaceCulling: false,
|
||||
updateCullFace: false,
|
||||
updateDebugShowBoundingVolume: false,
|
||||
});
|
||||
|
||||
derivedCommands.push(drawCommand._edgeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(ModelDrawCommand.prototype, {
|
||||
|
|
@ -586,6 +606,24 @@ ModelDrawCommand.prototype.pushSilhouetteCommands = function (
|
|||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {FrameState} frameState The frame state.
|
||||
* @param {DrawCommand[]} result The draw commands to push to.
|
||||
* @returns {DrawCommand[]} The modified command list.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ModelDrawCommand.prototype.pushEdgeCommands = function (frameState, result) {
|
||||
if (!defined(this._edgeCommand)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const use2D = shouldUse2DCommands(this, frameState);
|
||||
pushCommand(result, this._edgeCommand, use2D);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
function pushCommand(commandList, derivedCommand, use2D) {
|
||||
commandList.push(derivedCommand.command);
|
||||
if (use2D) {
|
||||
|
|
@ -644,6 +682,7 @@ function derive2DCommands(drawCommand) {
|
|||
derive2DCommand(drawCommand, drawCommand._skipLodStencilCommand);
|
||||
derive2DCommand(drawCommand, drawCommand._silhouetteModelCommand);
|
||||
derive2DCommand(drawCommand, drawCommand._silhouetteColorCommand);
|
||||
derive2DCommand(drawCommand, drawCommand._edgeCommand);
|
||||
}
|
||||
|
||||
function deriveTranslucentCommand(command) {
|
||||
|
|
@ -752,6 +791,36 @@ function deriveSilhouetteColorCommand(command, model) {
|
|||
return silhouetteColorCommand;
|
||||
}
|
||||
|
||||
function deriveEdgeCommand(command, renderResources) {
|
||||
const edgeGeometry = renderResources.edgeGeometry;
|
||||
const edgeCommand = DrawCommand.shallowClone(command);
|
||||
|
||||
// Use the edge geometry instead of the original geometry
|
||||
edgeCommand.vertexArray = edgeGeometry.vertexArray;
|
||||
edgeCommand.primitiveType = edgeGeometry.primitiveType;
|
||||
edgeCommand.count = edgeGeometry.indexCount;
|
||||
|
||||
// Use the edge shader program if available
|
||||
if (defined(edgeGeometry.shaderProgram)) {
|
||||
edgeCommand.shaderProgram = edgeGeometry.shaderProgram;
|
||||
}
|
||||
|
||||
// Set pass for edge rendering (use the pass specified in edgeGeometry)
|
||||
edgeCommand.pass = edgeGeometry.pass;
|
||||
|
||||
// Override uniformMap to set u_isEdgePass to true for the edge pass
|
||||
const uniformMap = clone(command.uniformMap);
|
||||
uniformMap.u_isEdgePass = function () {
|
||||
return true; // This is the edge pass
|
||||
};
|
||||
edgeCommand.uniformMap = uniformMap;
|
||||
|
||||
edgeCommand.castShadows = false;
|
||||
edgeCommand.receiveShadows = false;
|
||||
|
||||
return edgeCommand;
|
||||
}
|
||||
|
||||
function updateSkipLodStencilCommand(drawCommand, tile, use2D) {
|
||||
const stencilDerivedComand = drawCommand._skipLodStencilCommand;
|
||||
const stencilCommand = stencilDerivedComand.command;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import CPUStylingPipelineStage from "./CPUStylingPipelineStage.js";
|
|||
import CustomShaderMode from "./CustomShaderMode.js";
|
||||
import CustomShaderPipelineStage from "./CustomShaderPipelineStage.js";
|
||||
import DequantizationPipelineStage from "./DequantizationPipelineStage.js";
|
||||
import EdgeDetectionPipelineStage from "./EdgeDetectionPipelineStage.js";
|
||||
import EdgeVisibilityPipelineStage from "./EdgeVisibilityPipelineStage.js";
|
||||
import FeatureIdPipelineStage from "./FeatureIdPipelineStage.js";
|
||||
import GeometryPipelineStage from "./GeometryPipelineStage.js";
|
||||
import ImageryPipelineStage from "./ImageryPipelineStage.js";
|
||||
|
|
@ -244,6 +246,8 @@ ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) {
|
|||
const hasOutlines =
|
||||
model._enableShowOutline && defined(primitive.outlineCoordinates);
|
||||
|
||||
const hasEdgeVisibility = defined(primitive.edgeVisibility);
|
||||
|
||||
const featureIdFlags = inspectFeatureIds(model, node, primitive);
|
||||
|
||||
const hasClassification = defined(model.classificationType);
|
||||
|
|
@ -324,6 +328,13 @@ ModelRuntimePrimitive.prototype.configurePipeline = function (frameState) {
|
|||
pipelineStages.push(PrimitiveOutlinePipelineStage);
|
||||
}
|
||||
|
||||
if (hasEdgeVisibility) {
|
||||
// Indicate to Scene (after primitive updates) that the edge MRT should be enabled.
|
||||
frameState.edgeVisibilityRequested = true;
|
||||
pipelineStages.push(EdgeVisibilityPipelineStage);
|
||||
pipelineStages.push(EdgeDetectionPipelineStage);
|
||||
}
|
||||
|
||||
pipelineStages.push(AlphaPipelineStage);
|
||||
|
||||
pipelineStages.push(PrimitiveStatisticsPipelineStage);
|
||||
|
|
|
|||
|
|
@ -985,6 +985,7 @@ function updatePrimitiveShowBoundingVolume(runtimePrimitive, options) {
|
|||
}
|
||||
|
||||
const scratchSilhouetteCommands = [];
|
||||
const scratchEdgeCommands = [];
|
||||
const scratchPushDrawCommandOptions = {
|
||||
frameState: undefined,
|
||||
hasSilhouette: undefined,
|
||||
|
|
@ -1006,6 +1007,10 @@ ModelSceneGraph.prototype.pushDrawCommands = function (frameState) {
|
|||
const silhouetteCommands = scratchSilhouetteCommands;
|
||||
silhouetteCommands.length = 0;
|
||||
|
||||
// Gather edge commands for the edge pass
|
||||
const edgeCommands = scratchEdgeCommands;
|
||||
edgeCommands.length = 0;
|
||||
|
||||
// Since this function is called each frame, the options object is
|
||||
// preallocated in a scratch variable
|
||||
const pushDrawCommandOptions = scratchPushDrawCommandOptions;
|
||||
|
|
@ -1020,6 +1025,7 @@ ModelSceneGraph.prototype.pushDrawCommands = function (frameState) {
|
|||
);
|
||||
|
||||
addAllToArray(frameState.commandList, silhouetteCommands);
|
||||
addAllToArray(frameState.commandList, edgeCommands);
|
||||
};
|
||||
|
||||
// Callback is defined here to avoid allocating a closure in the render loop
|
||||
|
|
@ -1029,6 +1035,7 @@ function pushPrimitiveDrawCommands(runtimePrimitive, options) {
|
|||
|
||||
const passes = frameState.passes;
|
||||
const silhouetteCommands = scratchSilhouetteCommands;
|
||||
const edgeCommands = scratchEdgeCommands;
|
||||
const primitiveDrawCommand = runtimePrimitive.drawCommand;
|
||||
primitiveDrawCommand.pushCommands(frameState, frameState.commandList);
|
||||
|
||||
|
|
@ -1039,6 +1046,11 @@ function pushPrimitiveDrawCommands(runtimePrimitive, options) {
|
|||
if (hasSilhouette && !passes.pick) {
|
||||
primitiveDrawCommand.pushSilhouetteCommands(frameState, silhouetteCommands);
|
||||
}
|
||||
|
||||
// Add edge commands to the edge pass
|
||||
if (defined(primitiveDrawCommand.pushEdgeCommands)) {
|
||||
primitiveDrawCommand.pushEdgeCommands(frameState, edgeCommands);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -531,10 +531,20 @@ function getTranslucentShaderProgram(context, shaderProgram, keyword, source) {
|
|||
|
||||
// Discarding the fragment in main is a workaround for ANGLE D3D9
|
||||
// shader compilation errors.
|
||||
//
|
||||
// CESIUM_REDIRECTED_COLOR_OUTPUT: A general-purpose flag to indicate that this shader
|
||||
// is a derived/modified version created by Cesium's rendering pipeline.
|
||||
// This flag can be used to avoid color attachment conflicts when shaders
|
||||
// need different output declarations for different rendering passes.
|
||||
// For example, MRT (Multiple Render Targets) features can check this flag
|
||||
// to conditionally declare their output variables only when not conflicting
|
||||
// with the derived shader's output layout.
|
||||
fs.sources.splice(
|
||||
0,
|
||||
0,
|
||||
`vec4 czm_out_FragColor;\n` + `bool czm_discard = false;\n`,
|
||||
`#define CESIUM_REDIRECTED_COLOR_OUTPUT\n` +
|
||||
`vec4 czm_out_FragColor;\n` +
|
||||
`bool czm_discard = false;\n`,
|
||||
);
|
||||
|
||||
const fragDataMatches = [...source.matchAll(/out_FragData_(\d+)/g)];
|
||||
|
|
|
|||
|
|
@ -761,6 +761,15 @@ function Scene(options) {
|
|||
*/
|
||||
this.light = new SunLight();
|
||||
|
||||
/**
|
||||
* Whether or not to enable edge visibility rendering for 3D tiles.
|
||||
* When enabled, creates a framebuffer with multiple render targets
|
||||
* for advanced edge detection and visibility techniques.
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this._enableEdgeVisibility = false;
|
||||
|
||||
// Give frameState, camera, and screen space camera controller initial state before rendering
|
||||
updateFrameNumber(this, 0.0, JulianDate.now());
|
||||
this.updateFrameState();
|
||||
|
|
@ -2567,6 +2576,48 @@ function performTranslucent3DTilesClassification(
|
|||
);
|
||||
}
|
||||
|
||||
function performCesium3DTileEdgesPass(scene, passState, frustumCommands) {
|
||||
scene.context.uniformState.updatePass(Pass.CESIUM_3D_TILE_EDGES);
|
||||
|
||||
const originalFramebuffer = passState.framebuffer;
|
||||
|
||||
scene.context.uniformState.edgeColorTexture = scene.context.defaultTexture;
|
||||
scene.context.uniformState.edgeIdTexture = scene.context.defaultTexture;
|
||||
scene.context.uniformState.edgeDepthTexture = scene.context.defaultTexture;
|
||||
|
||||
// Set edge framebuffer for rendering
|
||||
if (
|
||||
scene._enableEdgeVisibility &&
|
||||
defined(scene._view) &&
|
||||
defined(scene._view.edgeFramebuffer)
|
||||
) {
|
||||
passState.framebuffer = scene._view.edgeFramebuffer.framebuffer;
|
||||
}
|
||||
|
||||
// performPass
|
||||
const commands = frustumCommands.commands[Pass.CESIUM_3D_TILE_EDGES];
|
||||
const commandCount = frustumCommands.indices[Pass.CESIUM_3D_TILE_EDGES];
|
||||
|
||||
// clear edge framebuffer
|
||||
if (
|
||||
scene._enableEdgeVisibility &&
|
||||
defined(scene._view) &&
|
||||
defined(scene._view.edgeFramebuffer)
|
||||
) {
|
||||
const clearCommand = scene._view.edgeFramebuffer.getClearCommand(
|
||||
new Color(0.0, 0.0, 0.0, 0.0),
|
||||
);
|
||||
clearCommand.execute(scene.context, passState);
|
||||
}
|
||||
|
||||
// Then execute edge rendering commands
|
||||
for (let j = 0; j < commandCount; ++j) {
|
||||
executeCommand(commands[j], scene, passState);
|
||||
}
|
||||
|
||||
passState.framebuffer = originalFramebuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the draw commands for all the render passes.
|
||||
*
|
||||
|
|
@ -2709,6 +2760,48 @@ function executeCommands(scene, passState) {
|
|||
}
|
||||
|
||||
let commandCount;
|
||||
|
||||
// Draw edges FIRST - before binding textures to avoid feedback loop
|
||||
performCesium3DTileEdgesPass(scene, passState, frustumCommands);
|
||||
|
||||
if (
|
||||
scene._enableEdgeVisibility &&
|
||||
defined(scene._view) &&
|
||||
defined(scene._view.edgeFramebuffer)
|
||||
) {
|
||||
// Get edge color texture (attachment 0)
|
||||
const colorTexture = scene._view.edgeFramebuffer.colorTexture;
|
||||
if (defined(colorTexture)) {
|
||||
scene.context.uniformState.edgeColorTexture = colorTexture;
|
||||
} else {
|
||||
scene.context.uniformState.edgeColorTexture =
|
||||
scene.context.defaultTexture;
|
||||
}
|
||||
|
||||
// Get edge ID texture (attachment 1)
|
||||
const idTexture = scene._view.edgeFramebuffer.idTexture;
|
||||
if (defined(idTexture)) {
|
||||
scene.context.uniformState.edgeIdTexture = idTexture;
|
||||
} else {
|
||||
scene.context.uniformState.edgeIdTexture = scene.context.defaultTexture;
|
||||
}
|
||||
|
||||
// Get edge depth texture (attachment 2)
|
||||
const edgeDepthTexture = scene._view.edgeFramebuffer.depthTexture;
|
||||
if (defined(edgeDepthTexture)) {
|
||||
scene.context.uniformState.edgeDepthTexture = edgeDepthTexture;
|
||||
} else {
|
||||
scene.context.uniformState.edgeDepthTexture =
|
||||
scene.context.defaultTexture;
|
||||
}
|
||||
} else {
|
||||
scene.context.uniformState.edgeColorTexture =
|
||||
scene.context.defaultTexture;
|
||||
scene.context.uniformState.edgeIdTexture = scene.context.defaultTexture;
|
||||
scene.context.uniformState.edgeDepthTexture =
|
||||
scene.context.defaultTexture;
|
||||
}
|
||||
|
||||
if (!useInvertClassification || picking || renderTranslucentDepthForPick) {
|
||||
// Common/fastest path. Draw 3D Tiles and classification normally.
|
||||
|
||||
|
|
@ -3595,9 +3688,20 @@ function updateShadowMaps(scene) {
|
|||
function updateAndRenderPrimitives(scene) {
|
||||
const frameState = scene._frameState;
|
||||
|
||||
// Reset per-frame edge visibility request flag before primitives update
|
||||
frameState.edgeVisibilityRequested = false;
|
||||
|
||||
scene._groundPrimitives.update(frameState);
|
||||
scene._primitives.update(frameState);
|
||||
|
||||
// If any primitive requested edge visibility this frame, flip the scene flag lazily.
|
||||
if (
|
||||
frameState.edgeVisibilityRequested &&
|
||||
scene._enableEdgeVisibility === false
|
||||
) {
|
||||
scene._enableEdgeVisibility = true;
|
||||
}
|
||||
|
||||
updateDebugFrustumPlanes(scene);
|
||||
updateShadowMaps(scene);
|
||||
|
||||
|
|
@ -3714,6 +3818,13 @@ function updateAndClearFramebuffers(scene, passState, clearColor) {
|
|||
|
||||
const useInvertClassification = (environmentState.useInvertClassification =
|
||||
!picking && defined(passState.framebuffer) && scene.invertClassification);
|
||||
|
||||
// Update edge framebuffer for 3D tile edge rendering
|
||||
const useEdgeFramebuffer = !picking && scene._enableEdgeVisibility;
|
||||
if (useEdgeFramebuffer) {
|
||||
view.edgeFramebuffer.update(context, view.viewport, scene._hdr);
|
||||
}
|
||||
|
||||
if (useInvertClassification) {
|
||||
let depthFramebuffer;
|
||||
if (frameState.invertClassificationColor.alpha === 1.0) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ClearCommand from "../Renderer/ClearCommand.js";
|
|||
import Pass from "../Renderer/Pass.js";
|
||||
import PassState from "../Renderer/PassState.js";
|
||||
import Camera from "./Camera.js";
|
||||
import EdgeFramebuffer from "./EdgeFramebuffer.js";
|
||||
import FrustumCommands from "./FrustumCommands.js";
|
||||
import GlobeDepth from "./GlobeDepth.js";
|
||||
import GlobeTranslucencyFramebuffer from "./GlobeTranslucencyFramebuffer.js";
|
||||
|
|
@ -63,6 +64,7 @@ function View(scene, camera, viewport) {
|
|||
this.pickFramebuffer = new PickFramebuffer(context);
|
||||
this.pickDepthFramebuffer = new PickDepthFramebuffer();
|
||||
this.sceneFramebuffer = new SceneFramebuffer();
|
||||
this.edgeFramebuffer = new EdgeFramebuffer();
|
||||
this.globeDepth = globeDepth;
|
||||
this.globeTranslucencyFramebuffer = new GlobeTranslucencyFramebuffer();
|
||||
this.oit = oit;
|
||||
|
|
@ -445,6 +447,7 @@ View.prototype.destroy = function () {
|
|||
this.pickDepthFramebuffer && this.pickDepthFramebuffer.destroy();
|
||||
this.sceneFramebuffer =
|
||||
this.sceneFramebuffer && this.sceneFramebuffer.destroy();
|
||||
this.edgeFramebuffer = this.edgeFramebuffer && this.edgeFramebuffer.destroy();
|
||||
this.globeDepth = this.globeDepth && this.globeDepth.destroy();
|
||||
this.oit = this.oit && this.oit.destroy();
|
||||
this.translucentTileClassification =
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passCesium3DTile = 4.0;
|
||||
const float czm_passCesium3DTile = 5.0;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passCesium3DTileClassification = 5.0;
|
||||
const float czm_passCesium3DTileClassification = 6.0;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passCesium3DTileClassificationIgnoreShow = 6.0;
|
||||
const float czm_passCesium3DTileClassificationIgnoreShow = 7.0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* The automatic GLSL constant for {@link Pass#CESIUM_3D_TILE_EDGES}
|
||||
*
|
||||
* @name czm_passCesium3DTileEdges
|
||||
* @glslConstant
|
||||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passCesium3DTileEdges = 4.0;
|
||||
|
||||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passGaussianSplats = 10.0;
|
||||
const float czm_passGaussianSplats = 11.0;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passOpaque = 7.0;
|
||||
const float czm_passOpaque = 8.0;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passOverlay = 11.0;
|
||||
const float czm_passOverlay = 12.0;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passTranslucent = 8.0;
|
||||
const float czm_passTranslucent = 9.0;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
*
|
||||
* @see czm_pass
|
||||
*/
|
||||
const float czm_passVoxels = 9.0;
|
||||
const float czm_passVoxels = 10.0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
void edgeDetectionStage(inout vec4 color, inout FeatureIds featureIds) {
|
||||
if (u_isEdgePass) {
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 screenCoord = gl_FragCoord.xy / czm_viewport.zw;
|
||||
|
||||
vec4 edgeColor = texture(czm_edgeColorTexture, screenCoord);
|
||||
vec4 edgeId = texture(czm_edgeIdTexture, screenCoord);
|
||||
|
||||
// Packed window-space depth from edge pass (0..1)
|
||||
float edgeDepthWin = czm_unpackDepth(texture(czm_edgeDepthTexture, screenCoord));
|
||||
|
||||
// Near / far for current frustum
|
||||
float n = czm_currentFrustum.x;
|
||||
float f = czm_currentFrustum.y;
|
||||
|
||||
// geometry depth in eye coordinate
|
||||
vec4 geomEC = czm_windowToEyeCoordinates(gl_FragCoord);
|
||||
float geomDepthLinear = -geomEC.z;
|
||||
|
||||
// Convert edge depth to linear depth
|
||||
float z_ndc_edge = edgeDepthWin * 2.0 - 1.0;
|
||||
float edgeDepthLinear = (2.0 * n * f) / (f + n - z_ndc_edge * (f - n));
|
||||
|
||||
float d = abs(edgeDepthLinear - geomDepthLinear);
|
||||
|
||||
// Adaptive epsilon using linear depth fwidth for robustness
|
||||
float pixelStepLinear = fwidth(geomDepthLinear);
|
||||
float rel = geomDepthLinear * 0.0005;
|
||||
float eps = max(n * 1e-4, max(pixelStepLinear * 1.5, rel));
|
||||
|
||||
// If Edge isn't behind any geometry and the pixel has edge data
|
||||
if (d < eps && edgeId.r > 0.0) {
|
||||
#ifdef HAS_EDGE_FEATURE_ID
|
||||
float edgeFeatureId = edgeId.g;
|
||||
float currentFeatureId = float(featureIds.featureId_0);
|
||||
#endif
|
||||
float globeDepth = czm_unpackDepth(texture(czm_globeDepthTexture, screenCoord));
|
||||
// Background / sky / globe: always show edge
|
||||
bool isBackground = geomDepthLinear > globeDepth;
|
||||
bool drawEdge = isBackground;
|
||||
|
||||
#ifdef HAS_EDGE_FEATURE_ID
|
||||
bool hasEdgeFeature = edgeFeatureId > 0.0;
|
||||
bool hasCurrentFeature = currentFeatureId > 0.0;
|
||||
bool featuresMatch = edgeFeatureId == currentFeatureId;
|
||||
|
||||
drawEdge = drawEdge || !hasEdgeFeature || !hasCurrentFeature || featuresMatch;
|
||||
#else
|
||||
drawEdge = true;
|
||||
#endif
|
||||
|
||||
if (drawEdge) {
|
||||
color = edgeColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// CESIUM_REDIRECTED_COLOR_OUTPUT flag is used to avoid color attachment conflicts
|
||||
// when shaders are processed by different rendering pipelines (e.g., OIT).
|
||||
// Only declare MRT outputs when not in a derived shader context.
|
||||
#if defined(HAS_EDGE_VISIBILITY_MRT) && !defined(CESIUM_REDIRECTED_COLOR_OUTPUT)
|
||||
layout(location = 1) out vec4 out_id; // edge id / metadata
|
||||
layout(location = 2) out vec4 out_edgeDepth; // packed depth
|
||||
#endif
|
||||
|
||||
void edgeVisibilityStage(inout vec4 color, inout FeatureIds featureIds)
|
||||
{
|
||||
#ifdef HAS_EDGE_VISIBILITY
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Temporary color: white
|
||||
edgeColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
color = edgeColor;
|
||||
|
||||
#if defined(HAS_EDGE_VISIBILITY_MRT) && !defined(CESIUM_REDIRECTED_COLOR_OUTPUT)
|
||||
// Write edge metadata
|
||||
out_id = vec4(0.0);
|
||||
out_id.r = edgeTypeInt; // Edge type (0-3)
|
||||
#ifdef HAS_EDGE_FEATURE_ID
|
||||
out_id.g = float(featureIds.featureId_0); // Feature ID if available
|
||||
#else
|
||||
out_id.g = 0.0;
|
||||
#endif
|
||||
// Pack depth into separate MRT attachment
|
||||
out_edgeDepth = czm_packDepth(gl_FragCoord.z);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
|
@ -119,6 +119,11 @@ void main()
|
|||
atmosphereStage(color, attributes);
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EDGE_VISIBILITY
|
||||
edgeVisibilityStage(color, featureIds);
|
||||
edgeDetectionStage(color, featureIds);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
// When not picking metadata END
|
||||
//========================================================================
|
||||
|
|
|
|||
|
|
@ -13,4 +13,3 @@ void primitiveOutlineStage(inout czm_modelMaterial material) {
|
|||
|
||||
material.diffuse = mix(material.diffuse, model_outlineColor.rgb, model_outlineColor.a * outlineness);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
import { EdgeFramebuffer, Texture } from "../../index.js";
|
||||
import createScene from "../../../../Specs/createScene.js";
|
||||
|
||||
describe("Scene/EdgeFramebuffer", function () {
|
||||
let scene;
|
||||
let context;
|
||||
|
||||
beforeAll(function () {
|
||||
scene = createScene();
|
||||
context = scene.context;
|
||||
});
|
||||
|
||||
afterAll(function () {
|
||||
scene.destroyForSpecs();
|
||||
});
|
||||
|
||||
it("constructs", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
expect(edgeFramebuffer).toBeDefined();
|
||||
expect(edgeFramebuffer.isDestroyed()).toBe(false);
|
||||
});
|
||||
|
||||
it("creates framebuffer with correct dimensions", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
|
||||
expect(edgeFramebuffer.framebuffer).toBeDefined();
|
||||
expect(edgeFramebuffer.colorTexture.width).toBe(viewport.width);
|
||||
expect(edgeFramebuffer.colorTexture.height).toBe(viewport.height);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("creates multiple render targets", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
|
||||
const framebuffer = edgeFramebuffer.framebuffer;
|
||||
expect(framebuffer.numberOfColorAttachments).toBeGreaterThan(1);
|
||||
|
||||
expect(framebuffer.getColorTexture(0)).toBeDefined(); // Edge color texture
|
||||
expect(framebuffer.getColorTexture(1)).toBeDefined(); // Edge ID texture
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("updates framebuffer when dimensions change", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
let viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
const originalFramebuffer = edgeFramebuffer.framebuffer;
|
||||
|
||||
viewport = { width: 512, height: 512 };
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
|
||||
expect(edgeFramebuffer.framebuffer).not.toBe(originalFramebuffer);
|
||||
expect(edgeFramebuffer.colorTexture.width).toBe(viewport.width);
|
||||
expect(edgeFramebuffer.colorTexture.height).toBe(viewport.height);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("does not update framebuffer when dimensions are the same", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
const originalFramebuffer = edgeFramebuffer.framebuffer;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
expect(edgeFramebuffer.framebuffer).toBe(originalFramebuffer);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("provides access to edge color texture", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
|
||||
const edgeColorTexture = edgeFramebuffer.colorTexture;
|
||||
expect(edgeColorTexture).toBeDefined();
|
||||
expect(edgeColorTexture instanceof Texture).toBe(true);
|
||||
expect(edgeColorTexture.width).toBe(viewport.width);
|
||||
expect(edgeColorTexture.height).toBe(viewport.height);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("provides access to edge ID texture", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
|
||||
const edgeIdTexture = edgeFramebuffer.idTexture;
|
||||
expect(edgeIdTexture).toBeDefined();
|
||||
expect(edgeIdTexture instanceof Texture).toBe(true);
|
||||
expect(edgeIdTexture.width).toBe(viewport.width);
|
||||
expect(edgeIdTexture.height).toBe(viewport.height);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("clears the framebuffer", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
|
||||
expect(function () {
|
||||
const clearCommand = edgeFramebuffer.getClearCommand();
|
||||
expect(clearCommand).toBeDefined();
|
||||
}).not.toThrow();
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
|
||||
it("destroys properly", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const viewport = { width: 256, height: 256 };
|
||||
const hdr = false;
|
||||
|
||||
edgeFramebuffer.update(context, viewport, hdr);
|
||||
expect(edgeFramebuffer.isDestroyed()).toBe(false);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
expect(edgeFramebuffer.isDestroyed()).toBe(true);
|
||||
});
|
||||
|
||||
it("can be destroyed multiple times without errors", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
expect(edgeFramebuffer.isDestroyed()).toBe(false);
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
expect(edgeFramebuffer.isDestroyed()).toBe(true);
|
||||
|
||||
// Second destroy should not throw but object is already destroyed
|
||||
expect(edgeFramebuffer.isDestroyed()).toBe(true);
|
||||
});
|
||||
|
||||
it("handles invalid dimensions gracefully", function () {
|
||||
const edgeFramebuffer = new EdgeFramebuffer();
|
||||
const hdr = false;
|
||||
|
||||
expect(function () {
|
||||
edgeFramebuffer.update(context, { width: 0, height: 256 }, hdr);
|
||||
}).toThrowDeveloperError();
|
||||
|
||||
expect(function () {
|
||||
edgeFramebuffer.update(context, { width: 256, height: 0 }, hdr);
|
||||
}).toThrowDeveloperError();
|
||||
|
||||
edgeFramebuffer.destroy();
|
||||
});
|
||||
});
|
||||
|
|
@ -130,6 +130,8 @@ describe(
|
|||
"./Data/Models/glTF-2.0/BoxClearcoat/glTF/BoxClearcoat.gltf";
|
||||
const meshPrimitiveRestartTestData =
|
||||
"./Data/Models/glTF-2.0/MeshPrimitiveRestart/glTF/MeshPrimitiveRestart.gltf";
|
||||
const edgeVisibilityTestData =
|
||||
"./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb";
|
||||
|
||||
let scene;
|
||||
const gltfLoaders = [];
|
||||
|
|
@ -4261,6 +4263,132 @@ describe(
|
|||
expect(loadedPrimitives.length).toBe(8);
|
||||
});
|
||||
|
||||
it("loads model with EXT_mesh_primitive_edge_visibility extension", async function () {
|
||||
const gltfLoader = await loadGltf(edgeVisibilityTestData);
|
||||
const components = gltfLoader.components;
|
||||
const scene = components.scene;
|
||||
|
||||
expect(scene).toBeDefined();
|
||||
expect(scene.nodes).toBeDefined();
|
||||
expect(scene.nodes.length).toBeGreaterThan(0);
|
||||
|
||||
const primitive = scene.nodes[0].primitives[0];
|
||||
expect(primitive).toBeDefined();
|
||||
|
||||
expect(primitive.edgeVisibility).toBeDefined();
|
||||
expect(primitive.edgeVisibility.visibility).toBeDefined();
|
||||
expect(primitive.edgeVisibility.silhouetteNormals).toBeDefined();
|
||||
expect(primitive.edgeVisibility.silhouetteNormals.length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it("processes edge visibility data correctly", async function () {
|
||||
const gltfLoader = await loadGltf(edgeVisibilityTestData);
|
||||
const components = gltfLoader.components;
|
||||
const scene = components.scene;
|
||||
const primitive = scene.nodes[0].primitives[0];
|
||||
|
||||
const edgeVisibility = primitive.edgeVisibility;
|
||||
expect(edgeVisibility).toBeDefined();
|
||||
|
||||
const visibilityData = edgeVisibility.visibility;
|
||||
expect(visibilityData).toBeDefined();
|
||||
expect(visibilityData.length).toBeGreaterThan(0);
|
||||
|
||||
let hasValidVisibilityValues = false;
|
||||
for (let i = 0; i < visibilityData.length; i++) {
|
||||
const value = visibilityData[i];
|
||||
if (value !== 0) {
|
||||
hasValidVisibilityValues = true;
|
||||
expect(value).toBeGreaterThanOrEqual(0);
|
||||
expect(value).toBeLessThanOrEqual(255);
|
||||
}
|
||||
}
|
||||
expect(hasValidVisibilityValues).toBe(true);
|
||||
});
|
||||
|
||||
it("loads primitive without edge visibility extension", async function () {
|
||||
const gltfLoader = await loadGltf(triangle);
|
||||
const components = gltfLoader.components;
|
||||
const scene = components.scene;
|
||||
const primitive = scene.nodes[0].primitives[0];
|
||||
|
||||
expect(primitive.edgeVisibility).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates edge visibility bitfield format", async function () {
|
||||
const gltfLoader = await loadGltf(edgeVisibilityTestData);
|
||||
const components = gltfLoader.components;
|
||||
const scene = components.scene;
|
||||
const primitive = scene.nodes[0].primitives[0];
|
||||
|
||||
const edgeVisibility = primitive.edgeVisibility;
|
||||
expect(edgeVisibility).toBeDefined();
|
||||
expect(edgeVisibility.visibility).toBeDefined();
|
||||
|
||||
const visibilityData = edgeVisibility.visibility;
|
||||
expect(visibilityData.length).toBeGreaterThan(0);
|
||||
|
||||
let hasVisibleEdges = false;
|
||||
for (let i = 0; i < visibilityData.length; i++) {
|
||||
const byte = visibilityData[i];
|
||||
|
||||
for (let bit = 0; bit < 8; bit += 2) {
|
||||
const edgeVisibility = (byte >> bit) & 0x3;
|
||||
expect(edgeVisibility).toBeGreaterThanOrEqual(0);
|
||||
expect(edgeVisibility).toBeLessThanOrEqual(3);
|
||||
if (edgeVisibility === 1 || edgeVisibility === 2) {
|
||||
hasVisibleEdges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(hasVisibleEdges).toBe(true);
|
||||
});
|
||||
|
||||
it("handles edge visibility silhouette normals", async function () {
|
||||
const gltfLoader = await loadGltf(edgeVisibilityTestData);
|
||||
const components = gltfLoader.components;
|
||||
const scene = components.scene;
|
||||
const primitive = scene.nodes[0].primitives[0];
|
||||
|
||||
const edgeVisibility = primitive.edgeVisibility;
|
||||
expect(edgeVisibility).toBeDefined();
|
||||
expect(edgeVisibility.silhouetteNormals).toBeDefined();
|
||||
|
||||
const silhouetteNormals = edgeVisibility.silhouetteNormals;
|
||||
expect(silhouetteNormals.length).toBeGreaterThan(0);
|
||||
|
||||
for (let i = 0; i < silhouetteNormals.length; i++) {
|
||||
const normal = silhouetteNormals[i];
|
||||
expect(normal).toBeDefined();
|
||||
expect(normal.x).toBeDefined();
|
||||
expect(normal.y).toBeDefined();
|
||||
expect(normal.z).toBeDefined();
|
||||
expect(typeof normal.x).toBe("number");
|
||||
expect(typeof normal.y).toBe("number");
|
||||
expect(typeof normal.z).toBe("number");
|
||||
expect(isNaN(normal.x)).toBe(false);
|
||||
expect(isNaN(normal.y)).toBe(false);
|
||||
expect(isNaN(normal.z)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("validates edge visibility data loading", async function () {
|
||||
const gltfLoader = await loadGltf(edgeVisibilityTestData);
|
||||
const primitive = gltfLoader.components.scene.nodes[0].primitives[0];
|
||||
|
||||
expect(primitive.edgeVisibility).toBeDefined();
|
||||
expect(primitive.edgeVisibility.visibility).toBeDefined();
|
||||
expect(primitive.edgeVisibility.visibility.length).toBeGreaterThan(0);
|
||||
|
||||
if (primitive.edgeVisibility.silhouetteNormals) {
|
||||
expect(
|
||||
primitive.edgeVisibility.silhouetteNormals.length,
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("parses copyright field", function () {
|
||||
return loadGltf(boxWithCredits).then(function (gltfLoader) {
|
||||
const components = gltfLoader.components;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,513 @@
|
|||
import {
|
||||
Buffer,
|
||||
BufferUsage,
|
||||
ComponentDatatype,
|
||||
IndexDatatype,
|
||||
PrimitiveType,
|
||||
ShaderBuilder,
|
||||
ShaderDestination,
|
||||
VertexAttributeSemantic,
|
||||
} from "../../../index.js";
|
||||
import createContext from "../../../../../Specs/createContext.js";
|
||||
import EdgeVisibilityPipelineStage from "../../../Source/Scene/Model/EdgeVisibilityPipelineStage.js";
|
||||
|
||||
describe("Scene/Model/EdgeVisibilityPipelineStage", function () {
|
||||
let context;
|
||||
|
||||
beforeAll(function () {
|
||||
context = createContext();
|
||||
});
|
||||
|
||||
afterAll(function () {
|
||||
context.destroyForSpecs();
|
||||
});
|
||||
|
||||
function createTestEdgeVisibilityData() {
|
||||
// Test case from GltfLoader: Simple 2-triangle quad with shared silhouette edge
|
||||
// Triangles: [0,1,2, 0,2,3]
|
||||
// Edge visibility: [VISIBLE,HIDDEN,SILHOUETTE, HIDDEN,VISIBLE,HIDDEN] = [2,0,1, 0,2,0]
|
||||
// Expected bytes: [18, 2] = [00010010, 00000010]
|
||||
const testVisibilityBuffer = new Uint8Array([18, 2]);
|
||||
|
||||
return {
|
||||
visibility: testVisibilityBuffer,
|
||||
silhouetteNormals: new Float32Array([
|
||||
0.0,
|
||||
0.0,
|
||||
1.0, // Edge 0 silhouette normal
|
||||
0.0,
|
||||
1.0,
|
||||
0.0, // Edge 2 silhouette normal
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
function createTestPrimitive() {
|
||||
// Create a simple 2-triangle quad
|
||||
// Vertices: (0,0,0), (1,0,0), (1,1,0), (0,1,0)
|
||||
// Triangles: [0,1,2], [0,2,3]
|
||||
const positions = new Float32Array([
|
||||
0.0,
|
||||
0.0,
|
||||
0.0, // vertex 0
|
||||
1.0,
|
||||
0.0,
|
||||
0.0, // vertex 1
|
||||
1.0,
|
||||
1.0,
|
||||
0.0, // vertex 2
|
||||
0.0,
|
||||
1.0,
|
||||
0.0, // vertex 3
|
||||
]);
|
||||
|
||||
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
||||
|
||||
const primitive = {
|
||||
attributes: [
|
||||
{
|
||||
semantic: VertexAttributeSemantic.POSITION,
|
||||
componentDatatype: ComponentDatatype.FLOAT,
|
||||
count: 4,
|
||||
typedArray: positions,
|
||||
buffer: Buffer.createVertexBuffer({
|
||||
context: context,
|
||||
typedArray: positions,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
}),
|
||||
strideInBytes: 12,
|
||||
offsetInBytes: 0,
|
||||
},
|
||||
],
|
||||
indices: {
|
||||
indexDatatype: IndexDatatype.UNSIGNED_SHORT,
|
||||
count: 6,
|
||||
typedArray: indices,
|
||||
buffer: Buffer.createIndexBuffer({
|
||||
context: context,
|
||||
typedArray: indices,
|
||||
usage: BufferUsage.STATIC_DRAW,
|
||||
indexDatatype: IndexDatatype.UNSIGNED_SHORT,
|
||||
}),
|
||||
},
|
||||
mode: PrimitiveType.TRIANGLES,
|
||||
edgeVisibility: createTestEdgeVisibilityData(),
|
||||
};
|
||||
|
||||
return primitive;
|
||||
}
|
||||
|
||||
function createMockRenderResources(primitive) {
|
||||
const shaderBuilder = new ShaderBuilder();
|
||||
|
||||
// Pre-add the required function that EdgeVisibilityPipelineStage expects
|
||||
shaderBuilder.addFunction(
|
||||
"setDynamicVaryingsVS",
|
||||
"void setDynamicVaryingsVS()\n{\n}",
|
||||
ShaderDestination.VERTEX,
|
||||
);
|
||||
|
||||
return {
|
||||
shaderBuilder: shaderBuilder,
|
||||
uniformMap: {},
|
||||
runtimePrimitive: {
|
||||
primitive: primitive,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createMockFrameState() {
|
||||
return {
|
||||
context: context,
|
||||
};
|
||||
}
|
||||
|
||||
it("decodes edge visibility test data correctly", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
// Process the primitive through EdgeVisibilityPipelineStage
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
// Verify edge geometry was created
|
||||
expect(renderResources.edgeGeometry).toBeDefined();
|
||||
expect(renderResources.edgeGeometry.vertexArray).toBeDefined();
|
||||
expect(renderResources.edgeGeometry.indexCount).toBeGreaterThan(0);
|
||||
expect(renderResources.edgeGeometry.primitiveType).toBe(
|
||||
PrimitiveType.LINES,
|
||||
);
|
||||
});
|
||||
|
||||
it("extracts correct edge visibility values from test buffer", function () {
|
||||
const testVisibilityBuffer = new Uint8Array([18, 2]);
|
||||
|
||||
// Test decoding of each edge manually
|
||||
// Expected pattern: [HARD(2), HIDDEN(0), SILHOUETTE(1), HIDDEN(0), HARD(2), HIDDEN(0)]
|
||||
|
||||
// Edge 0: bits 0-1 of byte 0 (18 = 00010010)
|
||||
const edge0 = (testVisibilityBuffer[0] >> 0) & 0x3;
|
||||
expect(edge0).toBe(2); // HARD edge
|
||||
|
||||
// Edge 1: bits 2-3 of byte 0
|
||||
const edge1 = (testVisibilityBuffer[0] >> 2) & 0x3;
|
||||
expect(edge1).toBe(0); // HIDDEN edge
|
||||
|
||||
// Edge 2: bits 4-5 of byte 0
|
||||
const edge2 = (testVisibilityBuffer[0] >> 4) & 0x3;
|
||||
expect(edge2).toBe(1); // SILHOUETTE edge
|
||||
|
||||
// Edge 3: bits 6-7 of byte 0
|
||||
const edge3 = (testVisibilityBuffer[0] >> 6) & 0x3;
|
||||
expect(edge3).toBe(0); // HIDDEN edge
|
||||
|
||||
// Edge 4: bits 0-1 of byte 1 (2 = 00000010)
|
||||
const edge4 = (testVisibilityBuffer[1] >> 0) & 0x3;
|
||||
expect(edge4).toBe(2); // HARD edge
|
||||
|
||||
// Edge 5: bits 2-3 of byte 1
|
||||
const edge5 = (testVisibilityBuffer[1] >> 2) & 0x3;
|
||||
expect(edge5).toBe(0); // HIDDEN edge
|
||||
});
|
||||
|
||||
it("processes triangle edges in correct order", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const indices = primitive.indices.typedArray; // [0,1,2, 0,2,3]
|
||||
|
||||
// Verify triangle structure
|
||||
expect(indices.length).toBe(6);
|
||||
|
||||
// Triangle 0: vertices [0,1,2]
|
||||
expect(indices[0]).toBe(0);
|
||||
expect(indices[1]).toBe(1);
|
||||
expect(indices[2]).toBe(2);
|
||||
|
||||
// Triangle 1: vertices [0,2,3]
|
||||
expect(indices[3]).toBe(0);
|
||||
expect(indices[4]).toBe(2);
|
||||
expect(indices[5]).toBe(3);
|
||||
|
||||
// Expected edges from triangles:
|
||||
// Triangle 0: edges (0,1), (1,2), (2,0)
|
||||
// Triangle 1: edges (0,2), (2,3), (3,0)
|
||||
// Total 6 edges with visibility pattern [2,0,1, 0,2,0]
|
||||
});
|
||||
|
||||
it("filters edges based on visibility values", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
// With visibility pattern [2,0,1, 0,2,0]:
|
||||
// - Edge 0 (HARD, value 2): should be included
|
||||
// - Edge 1 (HIDDEN, value 0): should be excluded
|
||||
// - Edge 2 (SILHOUETTE, value 1): should be included
|
||||
// - Edge 3 (HIDDEN, value 0): should be excluded
|
||||
// - Edge 4 (HARD, value 2): should be included
|
||||
// - Edge 5 (HIDDEN, value 0): should be excluded
|
||||
|
||||
expect(renderResources.edgeGeometry).toBeDefined();
|
||||
|
||||
// Expected 3 unique visible edges: (0,1), (0,2), (2,3)
|
||||
// Each edge creates 2 indices (line primitive), so indexCount should be 6
|
||||
expect(renderResources.edgeGeometry.indexCount).toBe(6);
|
||||
expect(renderResources.edgeGeometry.indexCount % 2).toBe(0); // Even number for lines
|
||||
expect(renderResources.edgeGeometry.primitiveType).toBe(
|
||||
PrimitiveType.LINES,
|
||||
);
|
||||
});
|
||||
|
||||
it("handles silhouette edges correctly", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
// Verify silhouette-specific attributes are added
|
||||
const shaderBuilder = renderResources.shaderBuilder;
|
||||
const shaderProgram = shaderBuilder.buildShaderProgram(context);
|
||||
|
||||
// Check for edge visibility defines in the shader program
|
||||
expect(shaderProgram._vertexShaderText).toContain("HAS_EDGE_VISIBILITY");
|
||||
expect(shaderProgram._fragmentShaderText).toContain(
|
||||
"HAS_EDGE_VISIBILITY_MRT",
|
||||
);
|
||||
|
||||
// Check for silhouette-related attributes
|
||||
expect(shaderProgram._vertexShaderText).toContain("a_silhouetteNormal");
|
||||
expect(shaderProgram._vertexShaderText).toContain("a_faceNormalA");
|
||||
expect(shaderProgram._vertexShaderText).toContain("a_faceNormalB");
|
||||
});
|
||||
|
||||
it("creates proper edge geometry buffers", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
expect(renderResources.edgeGeometry).toBeDefined();
|
||||
|
||||
const vertexArray = renderResources.edgeGeometry.vertexArray;
|
||||
expect(vertexArray).toBeDefined();
|
||||
expect(vertexArray.indexBuffer).toBeDefined();
|
||||
expect(vertexArray._attributes).toBeDefined();
|
||||
|
||||
// Verify the vertex array has the expected attributes for edge rendering
|
||||
const attributes = vertexArray._attributes;
|
||||
expect(attributes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("handles edge deduplication correctly", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
|
||||
// Manually extract edges to test deduplication logic
|
||||
const indices = primitive.indices.typedArray; // [0,1,2, 0,2,3]
|
||||
const testVisibility = primitive.edgeVisibility.visibility; // [18, 2]
|
||||
|
||||
const expectedEdges = new Set();
|
||||
const visibilityValues = [];
|
||||
let edgeIndex = 0;
|
||||
|
||||
// Process each triangle's edges and collect expected results
|
||||
for (let i = 0; i + 2 < indices.length; i += 3) {
|
||||
const v0 = indices[i];
|
||||
const v1 = indices[i + 1];
|
||||
const v2 = indices[i + 2];
|
||||
|
||||
const triangleEdges = [
|
||||
[v0, v1], // Edge 0 of current triangle
|
||||
[v1, v2], // Edge 1 of current triangle
|
||||
[v2, v0], // Edge 2 of current triangle
|
||||
];
|
||||
|
||||
for (let e = 0; e < 3; e++) {
|
||||
const byteIndex = Math.floor(edgeIndex / 4);
|
||||
const bitPairOffset = (edgeIndex % 4) * 2;
|
||||
const visibility2Bit =
|
||||
(testVisibility[byteIndex] >> bitPairOffset) & 0x3;
|
||||
visibilityValues.push(visibility2Bit);
|
||||
|
||||
if (visibility2Bit !== 0) {
|
||||
// Not HIDDEN
|
||||
const [a, b] = triangleEdges[e];
|
||||
const edgeKey = `${Math.min(a, b)},${Math.max(a, b)}`;
|
||||
expectedEdges.add(edgeKey);
|
||||
}
|
||||
|
||||
edgeIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the expected visibility pattern [HARD(2), HIDDEN(0), SILHOUETTE(1), HIDDEN(0), HARD(2), HIDDEN(0)]
|
||||
expect(visibilityValues).toEqual([2, 0, 1, 0, 2, 0]);
|
||||
|
||||
// Expected visible edges after deduplication:
|
||||
// Triangle 0: edges (0,1)[HARD], (1,2)[HIDDEN], (2,0)[SILHOUETTE] → visible: (0,1), (0,2)
|
||||
// Triangle 1: edges (0,2)[HIDDEN], (2,3)[HARD], (3,0)[HIDDEN] → visible: (2,3)
|
||||
// Total unique visible edges: (0,1), (0,2), (2,3)
|
||||
const expectedEdgeKeys = new Set(["0,1", "0,2", "2,3"]);
|
||||
expect(expectedEdges).toEqual(expectedEdgeKeys);
|
||||
expect(expectedEdges.size).toBe(3);
|
||||
});
|
||||
|
||||
it("sets up uniforms correctly", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
expect(renderResources.uniformMap.u_isEdgePass).toBeDefined();
|
||||
expect(renderResources.uniformMap.u_isEdgePass()).toBe(false);
|
||||
});
|
||||
|
||||
it("validates primitive VAO vs edge VAO structure", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
// Original primitive VAO (triangles)
|
||||
const originalIndices = primitive.indices.typedArray; // [0,1,2, 0,2,3]
|
||||
expect(originalIndices.length).toBe(6); // 6 indices for 2 triangles
|
||||
expect(primitive.mode).toBe(PrimitiveType.TRIANGLES);
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
// Edge VAO (lines)
|
||||
expect(renderResources.edgeGeometry).toBeDefined();
|
||||
expect(renderResources.edgeGeometry.primitiveType).toBe(
|
||||
PrimitiveType.LINES,
|
||||
);
|
||||
|
||||
// With visibility pattern [2,0,1, 0,2,0] → 3 visible edges
|
||||
// Each edge creates 2 vertices, so 6 vertices total
|
||||
expect(renderResources.edgeGeometry.indexCount).toBe(6); // 3 edges × 2 vertices per edge
|
||||
});
|
||||
|
||||
it("validates edge VAO has 6 vertices for 3 visible edges", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
const edgeVertexArray = renderResources.edgeGeometry.vertexArray;
|
||||
expect(edgeVertexArray).toBeDefined();
|
||||
|
||||
// Verify vertex array structure
|
||||
const attributes = edgeVertexArray._attributes;
|
||||
expect(attributes.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that we have the expected vertex buffers
|
||||
let positionAttribute = null;
|
||||
let edgeTypeAttribute = null;
|
||||
let silhouetteNormalAttribute = null;
|
||||
let faceNormalAAttribute = null;
|
||||
let faceNormalBAttribute = null;
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i];
|
||||
if (attr.index === 0) {
|
||||
// Position at location 0
|
||||
positionAttribute = attr;
|
||||
} else if (attr.componentsPerAttribute === 1) {
|
||||
// Edge type (float)
|
||||
edgeTypeAttribute = attr;
|
||||
} else if (attr.componentsPerAttribute === 3) {
|
||||
// Normals (vec3)
|
||||
if (!silhouetteNormalAttribute) {
|
||||
silhouetteNormalAttribute = attr;
|
||||
} else if (!faceNormalAAttribute) {
|
||||
faceNormalAAttribute = attr;
|
||||
} else if (!faceNormalBAttribute) {
|
||||
faceNormalBAttribute = attr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(positionAttribute).toBeDefined();
|
||||
expect(edgeTypeAttribute).toBeDefined();
|
||||
expect(silhouetteNormalAttribute).toBeDefined();
|
||||
expect(faceNormalAAttribute).toBeDefined();
|
||||
expect(faceNormalBAttribute).toBeDefined();
|
||||
|
||||
// Verify buffer properties
|
||||
expect(positionAttribute.componentsPerAttribute).toBe(3); // vec3
|
||||
expect(positionAttribute.componentDatatype).toBe(ComponentDatatype.FLOAT);
|
||||
|
||||
expect(edgeTypeAttribute.componentsPerAttribute).toBe(1); // float
|
||||
expect(edgeTypeAttribute.componentDatatype).toBe(ComponentDatatype.FLOAT);
|
||||
|
||||
expect(silhouetteNormalAttribute.componentsPerAttribute).toBe(3); // vec3
|
||||
expect(silhouetteNormalAttribute.componentDatatype).toBe(
|
||||
ComponentDatatype.FLOAT,
|
||||
);
|
||||
});
|
||||
|
||||
it("validates edge VAO vertex data correctness", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
const edgeVertexArray = renderResources.edgeGeometry.vertexArray;
|
||||
|
||||
// With our test data:
|
||||
// - 3 visible edges: (0,1)[HARD], (0,2)[SILHOUETTE], (2,3)[HARD]
|
||||
// - Each edge has 2 vertices
|
||||
// - Total: 6 vertices in edge domain
|
||||
|
||||
// Verify index buffer
|
||||
expect(edgeVertexArray.indexBuffer).toBeDefined();
|
||||
expect(renderResources.edgeGeometry.indexCount).toBe(6);
|
||||
|
||||
// Expected vertex positions in edge domain:
|
||||
// Edge 0: vertices (0,1) → positions: (0,0,0), (1,0,0)
|
||||
// Edge 1: vertices (0,2) → positions: (0,0,0), (1,1,0)
|
||||
// Edge 2: vertices (2,3) → positions: (1,1,0), (0,1,0)
|
||||
|
||||
// The edge VAO creates a separate vertex domain with 6 vertices total
|
||||
const indexBuffer = edgeVertexArray.indexBuffer;
|
||||
expect(indexBuffer).toBeDefined();
|
||||
});
|
||||
|
||||
it("validates silhouette normal VAO data values", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
const edgeVertexArray = renderResources.edgeGeometry.vertexArray;
|
||||
const attributes = edgeVertexArray._attributes;
|
||||
|
||||
// Find silhouette normal attribute buffer
|
||||
let silhouetteNormalBuffer = null;
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i];
|
||||
// Look for vec3 attribute that's not position (index 0)
|
||||
if (attr.componentsPerAttribute === 3 && attr.index !== 0) {
|
||||
silhouetteNormalBuffer = attr.vertexBuffer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(silhouetteNormalBuffer).toBeDefined();
|
||||
|
||||
expect(silhouetteNormalBuffer.sizeInBytes).toBe(6 * 3 * 4);
|
||||
});
|
||||
|
||||
it("validates edge type VAO data values", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
const edgeVertexArray = renderResources.edgeGeometry.vertexArray;
|
||||
const attributes = edgeVertexArray._attributes;
|
||||
|
||||
// Find edge type attribute buffer (componentsPerAttribute === 1)
|
||||
let edgeTypeBuffer = null;
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i];
|
||||
if (attr.componentsPerAttribute === 1) {
|
||||
edgeTypeBuffer = attr.vertexBuffer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(edgeTypeBuffer).toBeDefined();
|
||||
|
||||
expect(edgeTypeBuffer.sizeInBytes).toBe(6 * 4);
|
||||
});
|
||||
|
||||
it("validates edge position VAO data values", function () {
|
||||
const primitive = createTestPrimitive();
|
||||
const renderResources = createMockRenderResources(primitive);
|
||||
const frameState = createMockFrameState();
|
||||
|
||||
EdgeVisibilityPipelineStage.process(renderResources, primitive, frameState);
|
||||
|
||||
const edgeVertexArray = renderResources.edgeGeometry.vertexArray;
|
||||
const attributes = edgeVertexArray._attributes;
|
||||
|
||||
// Find position attribute (index === 0)
|
||||
let positionBuffer = null;
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i];
|
||||
if (attr.index === 0) {
|
||||
positionBuffer = attr.vertexBuffer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(positionBuffer).toBeDefined();
|
||||
expect(positionBuffer.sizeInBytes).toBe(6 * 3 * 4);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
import { Cartesian3, Model, Pass, Transforms } from "../../../index.js";
|
||||
|
||||
import createScene from "../../../../../Specs/createScene.js";
|
||||
import pollToPromise from "../../../../../Specs/pollToPromise.js";
|
||||
|
||||
describe("Scene/Model/EdgeVisibilityRendering", function () {
|
||||
let scene;
|
||||
const edgeVisibilityTestData =
|
||||
"./Data/Models/glTF-2.0/EdgeVisibility/glTF-Binary/EdgeVisibility.glb";
|
||||
|
||||
beforeAll(function () {
|
||||
scene = createScene();
|
||||
});
|
||||
|
||||
afterAll(function () {
|
||||
scene.destroyForSpecs();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
scene.primitives.removeAll();
|
||||
scene._enableEdgeVisibility = true;
|
||||
});
|
||||
|
||||
function waitForModelReady(model) {
|
||||
return pollToPromise(function () {
|
||||
scene.renderForSpecs();
|
||||
return model.ready;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadEdgeVisibilityModel() {
|
||||
const model = await Model.fromGltfAsync({
|
||||
url: edgeVisibilityTestData,
|
||||
modelMatrix: Transforms.eastNorthUpToFixedFrame(
|
||||
Cartesian3.fromDegrees(0.0, 0.0, 100.0),
|
||||
),
|
||||
});
|
||||
|
||||
scene.primitives.add(model);
|
||||
await waitForModelReady(model);
|
||||
return model;
|
||||
}
|
||||
|
||||
it("validates u_isEdgePass uniform and framebuffer attachments", async function () {
|
||||
// Skip this test in WebGL stub environment
|
||||
if (!!window.webglStub) {
|
||||
pending("Skipping test in WebGL stub environment");
|
||||
}
|
||||
|
||||
await loadEdgeVisibilityModel();
|
||||
|
||||
scene._enableEdgeVisibility = true;
|
||||
scene.renderForSpecs();
|
||||
|
||||
const commands = scene.frameState.commandList;
|
||||
let edgeCommand = null;
|
||||
let regularCommand = null;
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
if (command.pass === Pass.CESIUM_3D_TILE_EDGES) {
|
||||
edgeCommand = command;
|
||||
} else if (command.pass === Pass.CESIUM_3D_TILE) {
|
||||
regularCommand = command;
|
||||
}
|
||||
}
|
||||
|
||||
expect(edgeCommand).toBeDefined();
|
||||
expect(regularCommand).toBeDefined();
|
||||
|
||||
if (
|
||||
edgeCommand &&
|
||||
edgeCommand.uniformMap &&
|
||||
edgeCommand.uniformMap.u_isEdgePass
|
||||
) {
|
||||
expect(edgeCommand.uniformMap.u_isEdgePass()).toBe(true);
|
||||
}
|
||||
|
||||
if (
|
||||
regularCommand &&
|
||||
regularCommand.uniformMap &&
|
||||
regularCommand.uniformMap.u_isEdgePass
|
||||
) {
|
||||
expect(regularCommand.uniformMap.u_isEdgePass()).toBe(false);
|
||||
}
|
||||
|
||||
// Verify edge framebuffer attachments are not default textures
|
||||
expect(scene._view.edgeFramebuffer).toBeDefined();
|
||||
const edgeFramebuffer = scene._view.edgeFramebuffer;
|
||||
expect(edgeFramebuffer.colorTexture).not.toBe(scene.context.defaultTexture);
|
||||
|
||||
if (edgeFramebuffer._supportsMRT && edgeFramebuffer.idTexture) {
|
||||
expect(edgeFramebuffer.idTexture).not.toBe(scene.context.defaultTexture);
|
||||
}
|
||||
});
|
||||
|
||||
it("validates EdgeVisibility shader code and uniforms", async function () {
|
||||
// Skip this test in WebGL stub environment
|
||||
if (!!window.webglStub) {
|
||||
pending("Skipping test in WebGL stub environment");
|
||||
}
|
||||
|
||||
await loadEdgeVisibilityModel();
|
||||
|
||||
scene._enableEdgeVisibility = true;
|
||||
scene.renderForSpecs();
|
||||
|
||||
const commands = scene.frameState.commandList;
|
||||
let edgeCommand = null;
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
if (command.pass === Pass.CESIUM_3D_TILE_EDGES) {
|
||||
edgeCommand = command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(edgeCommand).toBeDefined();
|
||||
|
||||
const vertexShader = edgeCommand.shaderProgram._vertexShaderText;
|
||||
const fragmentShader = edgeCommand.shaderProgram._fragmentShaderText;
|
||||
|
||||
// Verify EdgeVisibility stage shader defines
|
||||
expect(vertexShader).toContain("HAS_EDGE_VISIBILITY");
|
||||
expect(fragmentShader).toContain("HAS_EDGE_VISIBILITY");
|
||||
expect(fragmentShader).toContain("HAS_EDGE_VISIBILITY_MRT");
|
||||
|
||||
// Verify edge visibility uniforms and attributes
|
||||
expect(vertexShader).toContain("u_isEdgePass");
|
||||
expect(vertexShader).toContain("a_edgeType");
|
||||
expect(vertexShader).toContain("a_silhouetteNormal");
|
||||
expect(vertexShader).toContain("a_faceNormalA");
|
||||
expect(vertexShader).toContain("a_faceNormalB");
|
||||
|
||||
// Verify varying variables for normal calculations
|
||||
expect(vertexShader).toContain("v_edgeType");
|
||||
expect(vertexShader).toContain("v_faceNormalAView");
|
||||
expect(vertexShader).toContain("v_faceNormalBView");
|
||||
|
||||
// Verify fragment shader edge type color coding
|
||||
expect(fragmentShader).toContain("v_edgeType * 255.0");
|
||||
|
||||
// Verify silhouette normal calculation
|
||||
expect(fragmentShader).toContain("normalize(v_faceNormalAView)");
|
||||
expect(fragmentShader).toContain("normalize(v_faceNormalBView)");
|
||||
expect(fragmentShader).toContain("dot(normalA, viewDir)");
|
||||
expect(fragmentShader).toContain("dot(normalB, viewDir)");
|
||||
|
||||
// Verify MRT output (color attachment 1)
|
||||
expect(fragmentShader).toContain("out_id");
|
||||
expect(fragmentShader).toContain("featureIds.featureId_0");
|
||||
|
||||
expect(edgeCommand.uniformMap.u_isEdgePass()).toBe(true);
|
||||
});
|
||||
|
||||
it("validates EdgeDetection shader code and texture sampling", async function () {
|
||||
// Skip this test in WebGL stub environment
|
||||
if (!!window.webglStub) {
|
||||
pending("Skipping test in WebGL stub environment");
|
||||
}
|
||||
|
||||
await loadEdgeVisibilityModel();
|
||||
|
||||
scene._enableEdgeVisibility = true;
|
||||
scene.renderForSpecs();
|
||||
|
||||
const commands = scene.frameState.commandList;
|
||||
let regularCommand = null;
|
||||
|
||||
// Prefer the regular 3D Tiles pass command
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
if (command.pass === Pass.CESIUM_3D_TILE) {
|
||||
regularCommand = command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to content search if not found by pass
|
||||
if (!regularCommand) {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const cmd = commands[i];
|
||||
if (
|
||||
cmd.shaderProgram &&
|
||||
typeof cmd.shaderProgram._fragmentShaderText === "string" &&
|
||||
cmd.shaderProgram._fragmentShaderText.indexOf(
|
||||
"edgeDetectionStage",
|
||||
) !== -1
|
||||
) {
|
||||
regularCommand = cmd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(regularCommand).toBeDefined();
|
||||
|
||||
const fragmentShader = regularCommand.shaderProgram._fragmentShaderText;
|
||||
|
||||
// Verify EdgeDetection stage shader includes edge detection function
|
||||
expect(fragmentShader).toContain("edgeDetectionStage");
|
||||
expect(fragmentShader).toContain("u_isEdgePass");
|
||||
|
||||
// Verify screen coordinate calculation
|
||||
expect(fragmentShader).toContain("gl_FragCoord.xy / czm_viewport.zw");
|
||||
|
||||
// Verify texture sampling from EdgeVisibility pass output
|
||||
expect(fragmentShader).toContain("czm_edgeColorTexture");
|
||||
expect(fragmentShader).toContain("czm_edgeIdTexture");
|
||||
expect(fragmentShader).toContain("czm_globeDepthTexture");
|
||||
|
||||
// Verify edge ID presence and feature IDs visibility logic exists
|
||||
expect(fragmentShader).toContain("edgeId.r > 0.0");
|
||||
expect(fragmentShader).toContain("edgeId.g");
|
||||
expect(fragmentShader).toContain("featureIds.featureId_0");
|
||||
|
||||
// Verify depth usage for background/globe rendering
|
||||
expect(fragmentShader).toContain("czm_unpackDepth");
|
||||
expect(fragmentShader).toContain("geomDepthLinear > globeDepth");
|
||||
|
||||
// Verify the color can inherit from edge pass
|
||||
expect(fragmentShader).toContain("color = edgeColor");
|
||||
|
||||
// Verify the uniforms reference correct textures
|
||||
const uniformState = scene.context.uniformState;
|
||||
const edgeFramebuffer = scene._view.edgeFramebuffer;
|
||||
expect(uniformState.edgeColorTexture).toBe(edgeFramebuffer.colorTexture);
|
||||
if (edgeFramebuffer._supportsMRT) {
|
||||
expect(uniformState.edgeIdTexture).toBe(edgeFramebuffer.idTexture);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue