import BoundingSphere from "../../Core/BoundingSphere.js"; import Cartesian2 from "../../Core/Cartesian2.js"; import CesiumMath from "../../Core/Math.js"; import Check from "../../Core/Check.js"; import clone from "../../Core/clone.js"; import Frozen from "../../Core/Frozen.js"; import defined from "../../Core/defined.js"; import Matrix4 from "../../Core/Matrix4.js"; import WebGLConstants from "../../Core/WebGLConstants.js"; import DrawCommand from "../../Renderer/DrawCommand.js"; import Pass from "../../Renderer/Pass.js"; import RenderState from "../../Renderer/RenderState.js"; import BlendingState from "../BlendingState.js"; import CullFace from "../CullFace.js"; import SceneMode from "../SceneMode.js"; import ShadowMode from "../ShadowMode.js"; import EdgeDisplayMode from "../EdgeDisplayMode.js"; import StencilConstants from "../StencilConstants.js"; import StencilFunction from "../StencilFunction.js"; import StencilOperation from "../StencilOperation.js"; import StyleCommandsNeeded from "./StyleCommandsNeeded.js"; /** * A wrapper around the draw commands used to render a {@link ModelRuntimePrimitive}. * This manages the derived commands and pushes only the necessary commands depending * on the given frame state. * * @param {object} options An object containing the following options: * @param {DrawCommand} options.command The draw command from which to derive other commands from. * @param {PrimitiveRenderResources} options.primitiveRenderResources The render resources of the primitive associated with the command. * * @alias ModelDrawCommand * @constructor * * @private */ function ModelDrawCommand(options) { options = options ?? Frozen.EMPTY_OBJECT; const command = options.command; const renderResources = options.primitiveRenderResources; //>>includeStart('debug', pragmas.debug); Check.typeOf.object("options.command", command); Check.typeOf.object("options.primitiveRenderResources", renderResources); //>>includeEnd('debug'); const model = renderResources.model; this._model = model; 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. const isTranslucent = command.pass === Pass.TRANSLUCENT; const isDoubleSided = runtimePrimitive.primitive.material.doubleSided; const usesBackFaceCulling = !isDoubleSided && !isTranslucent; const hasSilhouette = renderResources.hasSilhouette; // If the command was already translucent, there's no need to derive a new // translucent command. As of now, a style can't change an originally // translucent feature to opaque since the style's alpha is modulated, // not replaced. When this changes, we need to derive new opaque commands // in initialize(). // // Silhouettes for primitives with both opaque and translucent features // are not yet supported. const needsTranslucentCommand = !isTranslucent && !hasSilhouette; const needsSkipLevelOfDetailCommands = renderResources.hasSkipLevelOfDetail && !isTranslucent; const needsSilhouetteCommands = hasSilhouette; const needsEdgeCommands = defined(renderResources.edgeGeometry); this._command = command; // None of the derived commands (non-2D) use a different model matrix // or bounding volume than the original, so they all point to the // ModelDrawCommand's copy to save update time and memory. this._modelMatrix = Matrix4.clone(command.modelMatrix); this._boundingVolume = BoundingSphere.clone(command.boundingVolume); // The 2D model matrix depends on the frame state's map projection, // so it must be updated when the commands are handled in pushCommands. this._modelMatrix2D = new Matrix4(); this._boundingVolume2D = new BoundingSphere(); this._modelMatrix2DDirty = false; this._backFaceCulling = command.renderState.cull.enabled; this._cullFace = command.renderState.cull.face; this._shadows = model.shadows; this._debugShowBoundingVolume = command.debugShowBoundingVolume; this._usesBackFaceCulling = usesBackFaceCulling; this._needsTranslucentCommand = needsTranslucentCommand; this._needsSkipLevelOfDetailCommands = needsSkipLevelOfDetailCommands; this._needsSilhouetteCommands = needsSilhouetteCommands; this._needsEdgeCommands = needsEdgeCommands; // Non-behind planar fill primitives write their feature IDs in a pre-pass // so that behind fills can test same-object coplanarity. this._needsPlanarFillIdCommand = renderResources.planarFillIdPass; // Derived commands this._originalCommand = undefined; this._translucentCommand = undefined; this._skipLodBackfaceCommand = undefined; this._skipLodStencilCommand = undefined; this._silhouetteModelCommand = undefined; this._silhouetteColorCommand = undefined; this._edgeCommand = undefined; // Snap-pass variant of the edge command, pinned at 1 px width. // See deriveEdgeCommand. this._edgeSnapCommand = undefined; this._planarFillIdCommand = undefined; // All derived commands (including 2D commands) this._derivedCommands = []; this._has2DCommands = false; initialize(this); } function ModelDerivedCommand(options) { // The DrawCommand managed by this derived command. this.command = options.command; // These control whether the derived command should update the // values of the DrawCommand for the corresponding properties. this.updateShadows = options.updateShadows; this.updateBackFaceCulling = options.updateBackFaceCulling; this.updateCullFace = options.updateCullFace; this.updateDebugShowBoundingVolume = options.updateDebugShowBoundingVolume; // Whether this ModelDerivedCommand is in 2D. this.is2D = options.is2D ?? false; // A ModelDerivedCommand that is the 2D version of this one. this.derivedCommand2D = undefined; } ModelDerivedCommand.clone = function (derivedCommand) { return new ModelDerivedCommand({ command: derivedCommand.command, updateShadows: derivedCommand.updateShadows, updateBackFaceCulling: derivedCommand.updateBackFaceCulling, updateCullFace: derivedCommand.updateCullFace, updateDebugShowBoundingVolume: derivedCommand.updateDebugShowBoundingVolume, is2D: derivedCommand.is2D, derivedCommand2D: derivedCommand.derivedCommand2D, }); }; function initialize(drawCommand) { const command = drawCommand._command; command.modelMatrix = drawCommand._modelMatrix; command.boundingVolume = drawCommand._boundingVolume; const model = drawCommand._model; const usesBackFaceCulling = drawCommand._usesBackFaceCulling; const derivedCommands = drawCommand._derivedCommands; drawCommand._originalCommand = new ModelDerivedCommand({ command: command, updateShadows: true, updateBackFaceCulling: usesBackFaceCulling, updateCullFace: usesBackFaceCulling, updateDebugShowBoundingVolume: true, is2D: false, }); derivedCommands.push(drawCommand._originalCommand); if (drawCommand._needsTranslucentCommand) { drawCommand._translucentCommand = new ModelDerivedCommand({ command: deriveTranslucentCommand(command), updateShadows: true, updateBackFaceCulling: false, updateCullFace: false, updateDebugShowBoundingVolume: true, }); derivedCommands.push(drawCommand._translucentCommand); } if (drawCommand._needsSkipLevelOfDetailCommands) { drawCommand._skipLodBackfaceCommand = new ModelDerivedCommand({ command: deriveSkipLodBackfaceCommand(command), updateShadows: false, updateBackFaceCulling: false, updateCullFace: usesBackFaceCulling, updateDebugShowBoundingVolume: false, }); drawCommand._skipLodStencilCommand = new ModelDerivedCommand({ command: deriveSkipLodStencilCommand(command, model), updateShadows: true, updateBackFaceCulling: usesBackFaceCulling, updateCullFace: usesBackFaceCulling, updateDebugShowBoundingVolume: true, }); derivedCommands.push(drawCommand._skipLodBackfaceCommand); derivedCommands.push(drawCommand._skipLodStencilCommand); } if (drawCommand._needsSilhouetteCommands) { drawCommand._silhouetteModelCommand = new ModelDerivedCommand({ command: deriveSilhouetteModelCommand(command, model), updateShadows: true, updateBackFaceCulling: usesBackFaceCulling, updateCullFace: usesBackFaceCulling, updateDebugShowBoundingVolume: true, }); drawCommand._silhouetteColorCommand = new ModelDerivedCommand({ command: deriveSilhouetteColorCommand(command, model), updateShadows: false, updateBackFaceCulling: false, updateCullFace: false, updateDebugShowBoundingVolume: false, }); derivedCommands.push(drawCommand._silhouetteModelCommand); derivedCommands.push(drawCommand._silhouetteColorCommand); } if (drawCommand._needsEdgeCommands) { const renderResources = drawCommand._primitiveRenderResources; const edgeGeometry = renderResources.edgeGeometry; const colorLineWidth = defined(edgeGeometry.lineWidth) ? edgeGeometry.lineWidth : 1.0; // Snap-pass edge width is held at 1 px independent of the color width. // A wider snap band results in snap wiggle: as the cursor moves // perpendicular to an edge inside the band, the "edge pixel nearest // the cursor" shifts, so Snapping.snap returns a slightly different // point each frame. const snapLineWidth = 1.0; drawCommand._edgeCommand = new ModelDerivedCommand({ command: deriveEdgeCommand(command, renderResources, colorLineWidth), updateShadows: false, updateBackFaceCulling: false, updateCullFace: false, updateDebugShowBoundingVolume: false, }); drawCommand._edgeSnapCommand = new ModelDerivedCommand({ command: deriveEdgeCommand(command, renderResources, snapLineWidth), updateShadows: false, updateBackFaceCulling: false, updateCullFace: false, updateDebugShowBoundingVolume: false, }); derivedCommands.push(drawCommand._edgeCommand); derivedCommands.push(drawCommand._edgeSnapCommand); } if (drawCommand._needsPlanarFillIdCommand) { drawCommand._planarFillIdCommand = new ModelDerivedCommand({ command: derivePlanarFillIdCommand(command), updateShadows: false, updateBackFaceCulling: false, updateCullFace: false, updateDebugShowBoundingVolume: false, }); derivedCommands.push(drawCommand._planarFillIdCommand); } } Object.defineProperties(ModelDrawCommand.prototype, { /** * The main draw command that the other commands are derived from. * * @memberof ModelDrawCommand.prototype * @type {DrawCommand} * * @readonly * @private */ command: { get: function () { return this._command; }, }, /** * The runtime primitive that the draw command belongs to. * * @memberof ModelDrawCommand.prototype * @type {ModelRuntimePrimitive} * * @readonly * @private */ runtimePrimitive: { get: function () { return this._runtimePrimitive; }, }, /** * The model that the draw command belongs to. * * @memberof ModelDrawCommand.prototype * @type {Model} * * @readonly * @private */ model: { get: function () { return this._model; }, }, /** * The primitive type of the draw command. * * @memberof ModelDrawCommand.prototype * @type {PrimitiveType} * * @readonly * @private */ primitiveType: { get: function () { return this._command.primitiveType; }, }, /** * The current model matrix applied to the draw commands. If there are * 2D draw commands, their model matrix will be derived from the 3D one. * * @memberof ModelDrawCommand.prototype * @type {Matrix4} * * @readonly * @private */ modelMatrix: { get: function () { return this._modelMatrix; }, set: function (value) { this._modelMatrix = Matrix4.clone(value, this._modelMatrix); this._modelMatrix2DDirty = true; this._boundingVolume = BoundingSphere.transform( this.runtimePrimitive.boundingSphere, this._modelMatrix, this._boundingVolume, ); }, }, /** * The bounding volume of the main draw command. This is equivalent * to the primitive's bounding sphere transformed by the draw * command's model matrix. * * @memberof ModelDrawCommand.prototype * @type {BoundingSphere} * * @readonly * @private */ boundingVolume: { get: function () { return this._boundingVolume; }, }, /** * Whether the geometry casts or receives shadows from light sources. * * @memberof ModelDrawCommand.prototype * @type {ShadowMode} * * @private */ shadows: { get: function () { return this._shadows; }, set: function (value) { this._shadows = value; updateShadows(this); }, }, /** * Whether to cull back-facing geometry. When true, back face culling is * determined by the material's doubleSided property; when false, back face * culling is disabled. Back faces are not culled if the command is * translucent. * * @memberof ModelDrawCommand.prototype * @type {boolean} * * @private */ backFaceCulling: { get: function () { return this._backFaceCulling; }, set: function (value) { if (this._backFaceCulling === value) { return; } this._backFaceCulling = value; updateBackFaceCulling(this); }, }, /** * Determines which faces to cull, if culling is enabled. * * @memberof ModelDrawCommand.prototype * @type {CullFace} * * @private */ cullFace: { get: function () { return this._cullFace; }, set: function (value) { if (this._cullFace === value) { return; } this._cullFace = value; updateCullFace(this); }, }, /** * Whether to draw the bounding sphere associated with this draw command. * * @memberof ModelDrawCommand.prototype * @type {boolean} * * @private */ debugShowBoundingVolume: { get: function () { return this._debugShowBoundingVolume; }, set: function (value) { if (this._debugShowBoundingVolume === value) { return; } this._debugShowBoundingVolume = value; updateDebugShowBoundingVolume(this); }, }, }); function updateModelMatrix2D(drawCommand, frameState) { const modelMatrix = drawCommand._modelMatrix; drawCommand._modelMatrix2D = Matrix4.clone( modelMatrix, drawCommand._modelMatrix2D, ); // Change the translation's y-component so it appears on the opposite side // of the map. drawCommand._modelMatrix2D[13] -= CesiumMath.sign(modelMatrix[13]) * 2.0 * CesiumMath.PI * frameState.mapProjection.ellipsoid.maximumRadius; drawCommand._boundingVolume2D = BoundingSphere.transform( drawCommand.runtimePrimitive.boundingSphere, drawCommand._modelMatrix2D, drawCommand._boundingVolume2D, ); } function updateShadows(drawCommand) { const shadows = drawCommand.shadows; const castShadows = ShadowMode.castShadows(shadows); const receiveShadows = ShadowMode.receiveShadows(shadows); const derivedCommands = drawCommand._derivedCommands; for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateShadows) { const command = derivedCommand.command; command.castShadows = castShadows; command.receiveShadows = receiveShadows; } } } function updateBackFaceCulling(drawCommand) { const backFaceCulling = drawCommand.backFaceCulling; const derivedCommands = drawCommand._derivedCommands; for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateBackFaceCulling) { const command = derivedCommand.command; const renderState = clone(command.renderState, true); renderState.cull.enabled = backFaceCulling; command.renderState = RenderState.fromCache(renderState); } } } function updateCullFace(drawCommand) { const cullFace = drawCommand.cullFace; const derivedCommands = drawCommand._derivedCommands; for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateCullFace) { const command = derivedCommand.command; const renderState = clone(command.renderState, true); renderState.cull.face = cullFace; command.renderState = RenderState.fromCache(renderState); } } } function updateDebugShowBoundingVolume(drawCommand) { const debugShowBoundingVolume = drawCommand.debugShowBoundingVolume; const derivedCommands = drawCommand._derivedCommands; for (let i = 0; i < derivedCommands.length; ++i) { const derivedCommand = derivedCommands[i]; if (derivedCommand.updateDebugShowBoundingVolume) { const command = derivedCommand.command; command.debugShowBoundingVolume = debugShowBoundingVolume; } } } /** * Pushes the draw commands necessary to render the primitive. * This does not include the draw commands that render its silhouette. * * @param {FrameState} frameState The frame state. * @param {DrawCommand[]} result The array to push the draw commands to. * * @returns {DrawCommand[]} The modified result parameter. * * @private */ ModelDrawCommand.prototype.pushCommands = function (frameState, result) { const use2D = shouldUse2DCommands(this, frameState); if (use2D && !this._has2DCommands) { derive2DCommands(this); this._has2DCommands = true; this._modelMatrix2DDirty = true; } if (this._modelMatrix2DDirty) { updateModelMatrix2D(this, frameState); this._modelMatrix2DDirty = false; } const styleCommandsNeeded = this.model.styleCommandsNeeded; if (this._needsTranslucentCommand && defined(styleCommandsNeeded)) { // StyleCommandsNeeded has three values: all opaque, all translucent, or both. if (styleCommandsNeeded !== StyleCommandsNeeded.ALL_OPAQUE) { pushCommand(result, this._translucentCommand, use2D); } // Continue only if opaque commands are needed. if (styleCommandsNeeded === StyleCommandsNeeded.ALL_TRANSLUCENT) { return; } } if (this._needsSkipLevelOfDetailCommands) { const { tileset, tile } = this._model.content; if (tileset.hasMixedContent) { if (!tile._finalResolution) { pushCommand( tileset._backfaceCommands, this._skipLodBackfaceCommand, use2D, ); } updateSkipLodStencilCommand(this, tile, use2D); pushCommand(result, this._skipLodStencilCommand, use2D); return; } } if (this._needsSilhouetteCommands) { pushCommand(result, this._silhouetteModelCommand, use2D); return; } // Skip surface rendering if EDGES_ONLY mode is enabled and this primitive // has edge visibility data. Only the edges will render (via pushEdgeCommands). if ( this._model.edgeDisplayMode === EdgeDisplayMode.EDGES_ONLY && this._needsEdgeCommands ) { return result; } pushCommand(result, this._originalCommand, use2D); return result; }; /** * Pushes the draw commands necessary to render the silhouette. These should * be added to the command list after the draw commands of all primitives * in the model have been added. This way, the silhouette won't render on * top of the model. *
* This should only be called after pushCommands() has been invoked for * the ModelDrawCommand this frame. Otherwise, the silhouette commands may * not have been derived for 2D. The model matrix will also not have been * updated for 2D commands. *
* * @param {FrameState} frameState The frame state. * @param {DrawCommand[]} result The array to push the silhouette commands to. * * @returns {DrawCommand[]} The modified result parameter. * * @private */ ModelDrawCommand.prototype.pushSilhouetteCommands = function ( frameState, result, ) { const use2D = shouldUse2DCommands(this, frameState); pushCommand(result, this._silhouetteColorCommand, use2D); 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 mode = this._model.edgeDisplayMode; const passes = frameState.passes; const use2D = shouldUse2DCommands(this, frameState); if (passes.snap) { // Snapping pass: route the 1 px snap variant directly into the bound // snap framebuffer, regardless of display mode -- snapping works even // when edges are visually suppressed (SURFACES_ONLY). const edgeCommand = this._edgeSnapCommand; edgeCommand.command.pass = Pass.CESIUM_3D_TILE_EDGES_DIRECT; if (defined(edgeCommand.derivedCommand2D)) { edgeCommand.derivedCommand2D.command.pass = Pass.CESIUM_3D_TILE_EDGES_DIRECT; } pushCommand(result, edgeCommand, use2D); return result; } if (passes.pick) { // Plain pick pass: edges contribute nothing to the RGBA8 pick // framebuffer except in EDGES_ONLY mode, where they are the only // pickable geometry (pushCommands skips the surface command). if (mode !== EdgeDisplayMode.EDGES_ONLY) { return result; } const edgeCommand = this._edgeCommand; edgeCommand.command.pass = Pass.CESIUM_3D_TILE_EDGES_DIRECT; if (defined(edgeCommand.derivedCommand2D)) { edgeCommand.derivedCommand2D.command.pass = Pass.CESIUM_3D_TILE_EDGES_DIRECT; } pushCommand(result, edgeCommand, use2D); return result; } // Color pass. SURFACES_ONLY mode suppresses all edge rendering. if (mode === EdgeDisplayMode.SURFACES_ONLY) { return result; } // Use direct pass (renders to currently bound framebuffer) when EDGES_ONLY // mode is enabled, otherwise use MRT pass (renders to edge framebuffer for // compositing). const edgePass = mode === EdgeDisplayMode.EDGES_ONLY ? Pass.CESIUM_3D_TILE_EDGES_DIRECT : Pass.CESIUM_3D_TILE_EDGES; this._edgeCommand.command.pass = edgePass; if (defined(this._edgeCommand.derivedCommand2D)) { this._edgeCommand.derivedCommand2D.command.pass = edgePass; } pushCommand(result, this._edgeCommand, use2D); return result; }; /** * Push the planar fill feature-ID pre-pass command (if any). * * @param {FrameState} frameState The frame state. * @param {DrawCommand[]} result The draw commands to push to. * @returns {DrawCommand[]} The modified command list. * * @private */ ModelDrawCommand.prototype.pushPlanarFillIdCommands = function ( frameState, result, ) { if (!defined(this._planarFillIdCommand)) { return result; } const use2D = shouldUse2DCommands(this, frameState); pushCommand(result, this._planarFillIdCommand, use2D); return result; }; function pushCommand(commandList, derivedCommand, use2D) { commandList.push(derivedCommand.command); if (use2D) { commandList.push(derivedCommand.derivedCommand2D.command); } } function shouldUse2DCommands(drawCommand, frameState) { if (frameState.mode !== SceneMode.SCENE2D || drawCommand.model._projectTo2D) { return false; } // The draw command's bounding sphere might cause primitives not to render // over the IDL, even if they are part of the same model. Use the scene graph's // bounding sphere instead. const model = drawCommand.model; const boundingSphere = model.sceneGraph._boundingSphere2D; const left = boundingSphere.center.y - boundingSphere.radius; const right = boundingSphere.center.y + boundingSphere.radius; const idl2D = frameState.mapProjection.ellipsoid.maximumRadius * CesiumMath.PI; return (left < idl2D && right > idl2D) || (left < -idl2D && right > -idl2D); } function derive2DCommand(drawCommand, derivedCommand) { if (!defined(derivedCommand)) { return; } // If the model crosses the IDL in 2D, it will be drawn in one viewport but get // clipped by the other viewport. We create a second command that translates // the model matrix to the opposite side of the map so the part that was clipped // in one viewport is drawn in the other. const derivedCommand2D = ModelDerivedCommand.clone(derivedCommand); const command2D = DrawCommand.shallowClone(derivedCommand.command); command2D.modelMatrix = drawCommand._modelMatrix2D; command2D.boundingVolume = drawCommand._boundingVolume2D; derivedCommand2D.command = command2D; derivedCommand2D.updateShadows = false; // Shadows are disabled for 2D derivedCommand2D.is2D = true; derivedCommand.derivedCommand2D = derivedCommand2D; drawCommand._derivedCommands.push(derivedCommand2D); return derivedCommand2D; } function derive2DCommands(drawCommand) { derive2DCommand(drawCommand, drawCommand._originalCommand); derive2DCommand(drawCommand, drawCommand._translucentCommand); derive2DCommand(drawCommand, drawCommand._skipLodBackfaceCommand); derive2DCommand(drawCommand, drawCommand._skipLodStencilCommand); derive2DCommand(drawCommand, drawCommand._silhouetteModelCommand); derive2DCommand(drawCommand, drawCommand._silhouetteColorCommand); derive2DCommand(drawCommand, drawCommand._edgeCommand); derive2DCommand(drawCommand, drawCommand._edgeSnapCommand); derive2DCommand(drawCommand, drawCommand._planarFillIdCommand); } function deriveTranslucentCommand(command) { const derivedCommand = DrawCommand.shallowClone(command); derivedCommand.pass = Pass.TRANSLUCENT; const rs = clone(command.renderState, true); rs.cull.enabled = false; rs.depthMask = false; rs.blending = BlendingState.ALPHA_BLEND; derivedCommand.renderState = RenderState.fromCache(rs); return derivedCommand; } function deriveSilhouetteModelCommand(command, model) { // Wrap around after exceeding the 8-bit stencil limit. // The reference is unique to each model until this point. const stencilReference = model._silhouetteId % 255; const silhouetteModelCommand = DrawCommand.shallowClone(command); const renderState = clone(command.renderState, true); // Write the reference value into the stencil buffer. renderState.stencilTest = { enabled: true, frontFunction: WebGLConstants.ALWAYS, backFunction: WebGLConstants.ALWAYS, reference: stencilReference, mask: ~0, frontOperation: { fail: WebGLConstants.KEEP, zFail: WebGLConstants.KEEP, zPass: WebGLConstants.REPLACE, }, backOperation: { fail: WebGLConstants.KEEP, zFail: WebGLConstants.KEEP, zPass: WebGLConstants.REPLACE, }, }; if (model.isInvisible()) { renderState.colorMask = { red: false, green: false, blue: false, alpha: false, }; } silhouetteModelCommand.renderState = RenderState.fromCache(renderState); return silhouetteModelCommand; } function deriveSilhouetteColorCommand(command, model) { // Wrap around after exceeding the 8-bit stencil limit. // The reference is unique to each model until this point. const stencilReference = model._silhouetteId % 255; const silhouetteColorCommand = DrawCommand.shallowClone(command); const renderState = clone(command.renderState, true); renderState.cull.enabled = false; // Render the silhouette in the translucent pass if either the command // pass or the silhouette color is translucent. This will account for // translucent model color, since ModelColorPipelineStage sets the pass // to translucent. const silhouetteTranslucent = command.pass === Pass.TRANSLUCENT || model.silhouetteColor.alpha < 1.0; if (silhouetteTranslucent) { silhouetteColorCommand.pass = Pass.TRANSLUCENT; renderState.depthMask = false; renderState.blending = BlendingState.ALPHA_BLEND; } // Only render the pixels of the silhouette that don't conflict with // the stencil buffer. This way, the silhouette doesn't render over // the original model. renderState.stencilTest = { enabled: true, frontFunction: WebGLConstants.NOTEQUAL, backFunction: WebGLConstants.NOTEQUAL, reference: stencilReference, mask: ~0, frontOperation: { fail: WebGLConstants.KEEP, zFail: WebGLConstants.KEEP, zPass: WebGLConstants.KEEP, }, backOperation: { fail: WebGLConstants.KEEP, zFail: WebGLConstants.KEEP, zPass: WebGLConstants.KEEP, }, }; const uniformMap = clone(command.uniformMap); uniformMap.model_silhouettePass = function () { return true; }; silhouetteColorCommand.renderState = RenderState.fromCache(renderState); silhouetteColorCommand.uniformMap = uniformMap; silhouetteColorCommand.castShadows = false; silhouetteColorCommand.receiveShadows = false; return silhouetteColorCommand; } // Builds an edge draw command at the given screen-space line width. // // Two variants are derived per primitive (one for the color pass and one // for the snapping pass). Therefore, the visual line width and the snap-pass line // width can be set independently. The snap variant is pinned at 1 px to // avoid snap wiggle (see initialize). In practice both default to 1 px. function deriveEdgeCommand(command, renderResources, lineWidth) { 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 }; // Set line width uniform for quad-based edge rendering uniformMap.u_lineWidth = function () { return lineWidth; }; edgeCommand.uniformMap = uniformMap; edgeCommand.castShadows = false; edgeCommand.receiveShadows = false; return edgeCommand; } /** * Derive a command for the planar fill feature-ID pre-pass. * * This command uses the same geometry as the original but writes only a * feature-ID value to the color attachment (no lighting, no material). * The fragment shader outputsvec4(featureId, 0, 0, 1)
* via the PLANAR_FILL_ID_PASS define.
*
* No polygon offset is applied: the pre-pass uses the natural depth of the
* non-behind planar fill geometry (same program as the main command minus
* the POLYGON_OFFSET path that would shift it).
*
* Like the other derived commands (translucent, silhouette, edge, etc.),
* this is called once per draw command from initialize when the
* ModelDrawCommand is constructed — not per frame.
*
* @param {DrawCommand} command The original draw command.
* @returns {DrawCommand} The derived command for the feature-ID pass.
* @private
*/
function derivePlanarFillIdCommand(command) {
const derived = DrawCommand.shallowClone(command);
derived.pass = Pass.CESIUM_3D_TILE_PLANAR_FILL_ID;
derived.castShadows = false;
derived.receiveShadows = false;
// Use the same render state but disable blending.
const rs = clone(command.renderState, true);
rs.blending.enabled = false;
derived.renderState = RenderState.fromCache(rs);
// Override uniformMap to set u_isPlanarFillIdPass to true for the pre-pass
const uniformMap = clone(command.uniformMap);
uniformMap.u_isPlanarFillIdPass = function () {
return true;
};
derived.uniformMap = uniformMap;
return derived;
}
function updateSkipLodStencilCommand(drawCommand, tile, use2D) {
const stencilDerivedComand = drawCommand._skipLodStencilCommand;
const stencilCommand = stencilDerivedComand.command;
const selectionDepth = tile._selectionDepth;
const lastSelectionDepth = getLastSelectionDepth(stencilCommand);
if (selectionDepth !== lastSelectionDepth) {
const skipLodStencilReference = getStencilReference(selectionDepth);
const renderState = clone(stencilCommand.renderState, true);
renderState.stencilTest.reference = skipLodStencilReference;
stencilCommand.renderState = RenderState.fromCache(renderState);
if (use2D) {
stencilDerivedComand.derivedCommand2D.renderState = renderState;
}
}
}
function getLastSelectionDepth(stencilCommand) {
// Isolate the selection depth from the stencil reference.
const reference = stencilCommand.renderState.stencilTest.reference;
return (
(reference & StencilConstants.SKIP_LOD_MASK) >>>
StencilConstants.SKIP_LOD_BIT_SHIFT
);
}
function getStencilReference(selectionDepth) {
// Stencil test is masked to the most significant 3 bits so the reference is shifted.
// Writes 0 for the terrain bit.
return (
StencilConstants.CESIUM_3D_TILE_MASK |
(selectionDepth << StencilConstants.SKIP_LOD_BIT_SHIFT)
);
}
function deriveSkipLodBackfaceCommand(command) {
// Write just backface depth of unresolved tiles so resolved stenciled tiles
// do not appear in front.
const backfaceCommand = DrawCommand.shallowClone(command);
const renderState = clone(command.renderState, true);
renderState.cull.enabled = true;
renderState.cull.face = CullFace.FRONT;
// Back faces do not need to write color.
renderState.colorMask = {
red: false,
green: false,
blue: false,
alpha: false,
};
// Push back face depth away from the camera so it is less likely that back faces and front faces of the same tile
// intersect and overlap. This helps avoid flickering for very thin double-sided walls.
renderState.polygonOffset = {
enabled: true,
factor: 5.0,
units: 5.0,
};
// The stencil test is set in TilesetPipelineStage.
const uniformMap = clone(backfaceCommand.uniformMap);
const polygonOffset = new Cartesian2(5.0, 5.0);
uniformMap.u_polygonOffset = function () {
return polygonOffset;
};
backfaceCommand.renderState = RenderState.fromCache(renderState);
backfaceCommand.uniformMap = uniformMap;
backfaceCommand.castShadows = false;
backfaceCommand.receiveShadows = false;
return backfaceCommand;
}
function deriveSkipLodStencilCommand(command) {
// Tiles only draw if their selection depth is >= the tile drawn already. They write their
// selection depth to the stencil buffer to prevent ancestor tiles from drawing on top
const stencilCommand = DrawCommand.shallowClone(command);
const renderState = clone(command.renderState, true);
// The stencil reference is updated dynamically; see updateSkipLodStencilCommand().
const { stencilTest } = renderState;
stencilTest.enabled = true;
stencilTest.mask = StencilConstants.SKIP_LOD_MASK;
stencilTest.reference = StencilConstants.CESIUM_3D_TILE_MASK;
stencilTest.frontFunction = StencilFunction.GREATER_OR_EQUAL;
stencilTest.frontOperation.zPass = StencilOperation.REPLACE;
stencilTest.backFunction = StencilFunction.GREATER_OR_EQUAL;
stencilTest.backOperation.zPass = StencilOperation.REPLACE;
renderState.stencilMask =
StencilConstants.CESIUM_3D_TILE_MASK | StencilConstants.SKIP_LOD_MASK;
stencilCommand.renderState = RenderState.fromCache(renderState);
return stencilCommand;
}
export default ModelDrawCommand;