cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
680 lines (597 loc) • 20.7 kB
JavaScript
import BlendingState from "../BlendingState.js";
import BoundingSphere from "../../Core/BoundingSphere.js";
import CesiumMath from "../../Core/Math.js";
import Check from "../../Core/Check.js";
import clone from "../../Core/clone.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import DrawCommand from "../../Renderer/DrawCommand.js";
import Matrix4 from "../../Core/Matrix4.js";
import Pass from "../../Renderer/Pass.js";
import RenderState from "../../Renderer/RenderState.js";
import RuntimeError from "../../Core/RuntimeError.js";
import SceneMode from "../SceneMode.js";
import ShadowMode from "../ShadowMode.js";
import StyleCommandsNeeded from "./StyleCommandsNeeded.js";
import WebGLConstants from "../../Core/WebGLConstants.js";
/**
* A wrapper around the draw commands used to render a {@link ModelExperimentalPrimitive}.
* This manages the derived commands and returns 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.
* @param {Boolean} [options.useSilhouetteCommands=false] Whether the model has a silhouette and needs to derive silhouette commands.
* @alias ModelExperimentalDrawCommand
* @constructor
*
* @private
*/
function ModelExperimentalDrawCommand(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
const command = options.command;
const renderResources = options.primitiveRenderResources;
const useSilhouetteCommands = defaultValue(
options.useSilhouetteCommands,
false
);
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("options.command", command);
Check.typeOf.object("options.primitiveRenderResources", renderResources);
//>>includeEnd('debug');
this._command = command;
// Only derived if the original command wasn't translucent and the model's style
// has translucency.
this._translucentCommand = undefined;
this._modelMatrix = Matrix4.clone(command.modelMatrix, new Matrix4());
// The 2D projection of the model matrix depends on the frame state's
// map projection, so it must be updated when the commands are being
// retrieved in getCommands.
this._modelMatrix2DDirty = false;
this._styleCommandsNeeded = renderResources.styleCommandsNeeded;
this._backFaceCulling = command.renderState.cull.enabled;
this._cullFace = command.renderState.cull.face;
this._shadows = renderResources.model.shadows;
this._debugShowBoundingVolume = command.debugShowBoundingVolume;
this._useSilhouetteCommands = useSilhouetteCommands;
this._commandList = [];
this._commandList2D = [];
this._runtimePrimitive = renderResources.runtimePrimitive;
this._model = renderResources.model;
initialize(this);
}
function initialize(drawCommand) {
const command = drawCommand._command;
const styleCommandsNeeded = drawCommand._styleCommandsNeeded;
// If the command was originally translucent then there's no need to derive
// new commands. As of now, a style can't change an originally translucent
// feature to opaque since the style's alpha is modulated, not a replacement.
// When this changes, we need to derive new opaque commands in the constructor
// of ModelExperimentalDrawCommand.
if (defined(styleCommandsNeeded) && command.pass !== Pass.TRANSLUCENT) {
const translucentCommand = deriveTranslucentCommand(command);
switch (styleCommandsNeeded) {
case StyleCommandsNeeded.ALL_OPAQUE:
break;
case StyleCommandsNeeded.ALL_TRANSLUCENT:
case StyleCommandsNeeded.OPAQUE_AND_TRANSLUCENT:
drawCommand._translucentCommand = translucentCommand;
break;
//>>includeStart('debug', pragmas.debug);
default:
throw new RuntimeError("styleCommandsNeeded is not a valid value.");
//>>includeEnd('debug');
}
}
buildCommandList(drawCommand);
}
Object.defineProperties(ModelExperimentalDrawCommand.prototype, {
/**
* The main draw command that the other commands are derived from.
*
* @memberof ModelExperimentalDrawCommand.prototype
* @type {DrawCommand}
*
* @readonly
* @private
*/
command: {
get: function () {
return this._command;
},
},
/**
* The runtime primitive that the draw command belongs to.
*
* @memberof ModelExperimentalDrawCommand.prototype
* @type {ModelExperimentalPrimitive}
*
* @readonly
* @private
*/
runtimePrimitive: {
get: function () {
return this._runtimePrimitive;
},
},
/**
* The model that the draw command belongs to.
*
* @memberof ModelExperimentalDrawCommand.prototype
* @type {ModelExperimental}
*
* @readonly
* @private
*/
model: {
get: function () {
return this._model;
},
},
/**
* The primitive type of the main draw command.
*
* @memberof ModelExperimentalDrawCommand.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 ModelExperimentalDrawCommand.prototype
* @type {Matrix4}
*
* @readonly
* @private
*/
modelMatrix: {
get: function () {
return this._modelMatrix;
},
set: function (value) {
this._modelMatrix = Matrix4.clone(value, this._modelMatrix);
this._modelMatrix2DDirty = true;
updateModelMatrix(this);
},
},
/**
* The bounding volume of the main draw command. This is equivalent
* to the the primitive's bounding sphere transformed by the draw
* command's model matrix.
*
* @memberof ModelExperimentalDrawCommand.prototype
* @type {BoundingSphere}
*
* @readonly
* @private
*/
boundingVolume: {
get: function () {
return this._command.boundingVolume;
},
},
/**
* Whether the geometry casts or receives shadows from light sources.
*
* @memberof ModelExperimentalDrawCommand.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 model's color is
* translucent, if the command is drawing translucent geometry, or if the
* model is being drawn with a silhouette.
*
* @memberof ModelExperimentalDrawCommand.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 ModelExperimentalDrawCommand.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 ModelExperimentalDrawCommand.prototype
* @type {Boolean}
*
* @private
*/
debugShowBoundingVolume: {
get: function () {
return this._debugShowBoundingVolume;
},
set: function (value) {
if (this._debugShowBoundingVolume === value) {
return;
}
this._debugShowBoundingVolume = value;
updateShowBoundingVolume(this);
},
},
});
function buildCommandList(drawCommand) {
const commandList = drawCommand._commandList;
const commandList2D = drawCommand._commandList2D;
commandList.length = 0;
commandList2D.length = 0;
// Add opaque and translucent commands depending on the style commands needed.
const styleCommandsNeeded = drawCommand._styleCommandsNeeded;
const originalCommand = drawCommand._command;
const translucentCommand = drawCommand._translucentCommand;
if (defined(styleCommandsNeeded) && defined(translucentCommand)) {
switch (styleCommandsNeeded) {
case StyleCommandsNeeded.ALL_OPAQUE:
commandList.push(originalCommand);
break;
case StyleCommandsNeeded.ALL_TRANSLUCENT:
commandList.push(translucentCommand);
break;
case StyleCommandsNeeded.OPAQUE_AND_TRANSLUCENT:
commandList.push(originalCommand, translucentCommand);
break;
//>>includeStart('debug', pragmas.debug);
default:
throw new RuntimeError("styleCommandsNeeded is not a valid value.");
//>>includeEnd('debug');
}
} else {
commandList.push(originalCommand);
}
// Derive silhouette commands from the active commands.
if (drawCommand._useSilhouetteCommands) {
const length = commandList.length;
const silhouetteCommands = [];
const model = drawCommand._model;
for (let i = 0; i < length; i++) {
const command = commandList[i];
const silhouetteModelCommand = deriveSilhouetteModelCommand(
command,
model
);
const silhouetteColorCommand = deriveSilhouetteColorCommand(
command,
model
);
silhouetteCommands.push(silhouetteModelCommand);
silhouetteCommands.push(silhouetteColorCommand);
}
// Replace the original command list.
commandList.length = 0;
commandList.push.apply(commandList, silhouetteCommands);
}
}
function getAllCommands(drawCommand) {
const commandList = [];
commandList.push.apply(commandList, drawCommand._commandList);
commandList.push.apply(commandList, drawCommand._commandList2D);
return commandList;
}
const scratchMatrix2D = new Matrix4();
function updateModelMatrix(drawCommand) {
const modelMatrix = drawCommand.modelMatrix;
const boundingSphere = drawCommand.runtimePrimitive.boundingSphere;
const commandList = drawCommand._commandList;
const length = commandList.length;
for (let i = 0; i < length; i++) {
const command = commandList[i];
command.modelMatrix = Matrix4.clone(modelMatrix, command.modelMatrix);
command.boundingVolume = BoundingSphere.transform(
boundingSphere,
command.modelMatrix,
command.boundingVolume
);
}
}
function updateModelMatrix2D(drawCommand, frameState) {
const modelMatrix = drawCommand.modelMatrix;
const boundingSphere = drawCommand.runtimePrimitive.boundingSphere;
const commandList2D = drawCommand._commandList2D;
const length2D = commandList2D.length;
if (length2D === 0) {
return;
}
const modelMatrix2D = Matrix4.clone(modelMatrix, scratchMatrix2D);
// Change the translation's y-component so it appears on the opposite side
// of the map.
modelMatrix2D[13] -=
CesiumMath.sign(modelMatrix[13]) *
2.0 *
CesiumMath.PI *
frameState.mapProjection.ellipsoid.maximumRadius;
for (let i = 0; i < length2D; i++) {
const command = commandList2D[i];
command.modelMatrix = Matrix4.clone(modelMatrix2D, command.modelMatrix);
command.boundingVolume = BoundingSphere.transform(
boundingSphere,
command.modelMatrix,
command.boundingVolume
);
}
}
function updateShadows(drawCommand) {
const shadows = drawCommand.shadows;
const castShadows = ShadowMode.castShadows(shadows);
const receiveShadows = ShadowMode.receiveShadows(shadows);
const commandList = getAllCommands(drawCommand);
const commandLength = commandList.length;
for (let i = 0; i < commandLength; i++) {
const command = commandList[i];
// Shadows should stay disabled for the silhouette color command.
const model_silhouettePass = command.uniformMap.model_silhouettePass;
if (defined(model_silhouettePass) && model_silhouettePass()) {
continue;
}
command.castShadows = castShadows;
command.receiveShadows = receiveShadows;
}
}
function updateBackFaceCulling(drawCommand) {
let backFaceCulling = drawCommand.backFaceCulling;
const doubleSided =
drawCommand.runtimePrimitive.primitive.material.doubleSided;
const translucent = drawCommand._model.isTranslucent();
const useSilhouetteCommands = drawCommand._useSilhouetteCommands;
backFaceCulling =
backFaceCulling && !doubleSided && !translucent && !useSilhouetteCommands;
const commandList = getAllCommands(drawCommand);
const commandLength = commandList.length;
for (let i = 0; i < commandLength; i++) {
const command = commandList[i];
// Back-face culling should stay disabled if the command
// is drawing translucent geometry.
if (command.pass === Pass.TRANSLUCENT) {
continue;
}
const renderState = clone(command.renderState, true);
renderState.cull.enabled = backFaceCulling;
command.renderState = RenderState.fromCache(renderState);
}
}
function updateCullFace(drawCommand) {
const cullFace = drawCommand.cullFace;
const commandList = getAllCommands(drawCommand);
const commandLength = commandList.length;
for (let i = 0; i < commandLength; i++) {
const command = commandList[i];
const renderState = clone(command.renderState, true);
renderState.cull.face = cullFace;
command.renderState = RenderState.fromCache(renderState);
}
}
function updateShowBoundingVolume(drawCommand) {
const debugShowBoundingVolume = drawCommand.debugShowBoundingVolume;
const commandList = getAllCommands(drawCommand);
const commandLength = commandList.length;
for (let i = 0; i < commandLength; i++) {
const command = commandList[i];
command.debugShowBoundingVolume = debugShowBoundingVolume;
}
}
/**
* Returns an array of the draw commands necessary to render the primitive.
*
* @param {FrameState} frameState The frame state.
*
* @returns {DrawCommand[]} The draw commands.
*
* @private
*/
ModelExperimentalDrawCommand.prototype.getCommands = function (frameState) {
const commandList = this._commandList;
const commandList2D = this._commandList2D;
const use2D = shouldUse2DCommands(this, frameState);
if (use2D && commandList2D.length === 0) {
const length = commandList.length;
for (let i = 0; i < length; i++) {
const command2D = derive2DCommand(commandList[i]);
commandList2D.push(command2D);
}
this._modelMatrix2DDirty = true;
}
if (this._modelMatrix2DDirty) {
updateModelMatrix2D(this, frameState);
this._modelMatrix2DDirty = false;
}
const commands = [];
commands.push.apply(commands, commandList);
if (use2D) {
commands.push.apply(commands, commandList2D);
}
return commands;
};
/**
* @private
*/
function deriveTranslucentCommand(command) {
const derivedCommand = DrawCommand.shallowClone(command);
derivedCommand.pass = Pass.TRANSLUCENT;
const rs = clone(command.renderState, true);
rs.cull.enabled = false;
rs.depthTest.enabled = true;
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
derivedCommand.renderState = RenderState.fromCache(rs);
return derivedCommand;
}
/**
* 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.
*
* @param {DrawCommand} command The original draw command.
*
* @returns {DrawCommand} The derived command for rendering across the IDL in 2D.
*
* @private
*/
function derive2DCommand(command) {
const derivedCommand = DrawCommand.shallowClone(command);
// These will be computed in updateModelMatrix2D()
derivedCommand.modelMatrix = new Matrix4();
derivedCommand.boundingVolume = new BoundingSphere();
return derivedCommand;
}
/**
* Tracks how many silhouettes have been created. This value is used to
* assign a reference number to the stencil.
*
* @type {Number}
* @private
*/
ModelExperimentalDrawCommand.silhouettesLength = 0;
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 =
++ModelExperimentalDrawCommand.silhouettesLength % 255;
const silhouetteModelCommand = DrawCommand.shallowClone(command);
let 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()) {
// When the model is invisible, disable color and depth writes,
// but still write into the stencil buffer.
renderState.colorMask = {
red: false,
green: false,
blue: false,
alpha: false,
};
renderState.depthMask = false;
}
renderState = RenderState.fromCache(renderState);
silhouetteModelCommand.renderState = 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 = ModelExperimentalDrawCommand.silhouettesLength % 255;
const silhouetteColorCommand = DrawCommand.shallowClone(command);
let renderState = clone(command.renderState, true);
renderState.depthTest.enabled = true;
renderState.cull.enabled = false;
// Render the silhouette in the translucent pass if the command is translucent
// or if the silhouette color is translucent. This accounts 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,
},
};
renderState = RenderState.fromCache(renderState);
const uniformMap = clone(command.uniformMap);
uniformMap.model_silhouettePass = function () {
return true;
};
silhouetteColorCommand.renderState = renderState;
silhouetteColorCommand.uniformMap = uniformMap;
silhouetteColorCommand.castShadows = false;
silhouetteColorCommand.receiveShadows = false;
return silhouetteColorCommand;
}
function shouldUse2DCommands(drawCommand, frameState) {
if (frameState.mode !== SceneMode.SCENE2D || drawCommand.model._projectTo2D) {
return false;
}
const idl2D =
frameState.mapProjection.ellipsoid.maximumRadius * CesiumMath.PI;
// Using the draw command's bounding sphere might cause primitives to not render
// over the IDL, even if they are part of the same model.
const model = drawCommand.model;
const boundingSphere = model.sceneGraph._boundingSphere2D;
const left = boundingSphere.center.y - boundingSphere.radius;
const right = boundingSphere.center.y + boundingSphere.radius;
return (left < idl2D && right > idl2D) || (left < -idl2D && right > -idl2D);
}
export default ModelExperimentalDrawCommand;