import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian3 from "../Core/Cartesian3.js";
import clone from "../Core/clone.js";
import Color from "../Core/Color.js";
import ComponentDatatype from "../Core/ComponentDatatype.js";
import defaultValue from "../Core/defaultValue.js";
import defer from "../Core/defer.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import ImageBasedLighting from "./ImageBasedLighting.js";
import Matrix4 from "../Core/Matrix4.js";
import PrimitiveType from "../Core/PrimitiveType.js";
import Resource from "../Core/Resource.js";
import RuntimeError from "../Core/RuntimeError.js";
import Transforms from "../Core/Transforms.js";
import Buffer from "../Renderer/Buffer.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import Pass from "../Renderer/Pass.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import ForEach from "./GltfPipeline/ForEach.js";
import Model from "./Model.js";
import ModelInstance from "./ModelInstance.js";
import ModelUtility from "./ModelUtility.js";
import SceneMode from "./SceneMode.js";
import ShadowMode from "./ShadowMode.js";
import SplitDirection from "./SplitDirection.js";
const LoadState = {
NEEDS_LOAD: 0,
LOADING: 1,
LOADED: 2,
FAILED: 3,
};
/**
* A 3D model instance collection. All instances reference the same underlying model, but have unique
* per-instance properties like model matrix, pick id, etc.
*
* Instances are rendered relative-to-center and for best results instances should be positioned close to one another.
* Otherwise there may be precision issues if, for example, instances are placed on opposite sides of the globe.
*
* @alias ModelInstanceCollection
* @constructor
*
* @param {Object} options Object with the following properties:
* @param {Object[]} [options.instances] An array of instances, where each instance contains a modelMatrix and optional batchId when options.batchTable is defined.
* @param {Cesium3DTileBatchTable} [options.batchTable] The batch table of the instanced 3D Tile.
* @param {Resource|String} [options.url] The url to the .gltf file.
* @param {Object} [options.requestType] The request type, used for request prioritization
* @param {Object|ArrayBuffer|Uint8Array} [options.gltf] A glTF JSON object, or a binary glTF buffer.
* @param {Resource|String} [options.basePath=''] The base path that paths in the glTF JSON are relative to.
* @param {Boolean} [options.dynamic=false] Hint if instance model matrices will be updated frequently.
* @param {Boolean} [options.show=true] Determines if the collection will be shown.
* @param {Boolean} [options.allowPicking=true] When true
, each instance is pickable with {@link Scene#pick}.
* @param {Boolean} [options.asynchronous=true] Determines if model WebGL resource creation will be spread out over several frames or block until completion once all glTF files are loaded.
* @param {Boolean} [options.incrementallyLoadTextures=true] Determine if textures may continue to stream in after the model is loaded.
* @param {ShadowMode} [options.shadows=ShadowMode.ENABLED] Determines whether the collection casts or receives shadows from light sources.
* @param {Cartesian3} [options.lightColor] The light color when shading models. When undefined
the scene's light color is used instead.
* @param {ImageBasedLighting} [options.imageBasedLighting] The properties for managing image-based lighting for this tileset.
* @param {Cartesian2} [options.imageBasedLightingFactor=new Cartesian2(1.0, 1.0)] Scales diffuse and specular image-based lighting from the earth, sky, atmosphere and star skybox. Deprecated in Cesium 1.92, will be removed in Cesium 1.94.
* @param {Number} [options.luminanceAtZenith=0.2] The sun's luminance at the zenith in kilo candela per meter squared to use for this model's procedural environment map. Deprecated in Cesium 1.92, will be removed in Cesium 1.94.
* @param {Cartesian3[]} [options.sphericalHarmonicCoefficients] The third order spherical harmonic coefficients used for the diffuse color of image-based lighting. Deprecated in Cesium 1.92, will be removed in Cesium 1.94.
* @param {String} [options.specularEnvironmentMaps] A URL to a KTX2 file that contains a cube map of the specular lighting and the convoluted specular mipmaps. Deprecated in Cesium 1.92, will be removed in Cesium 1.94.
* @param {Boolean} [options.backFaceCulling=true] Whether to cull back-facing geometry. When true, back face culling is determined by the glTF material's doubleSided property; when false, back face culling is disabled.
* @param {Boolean} [options.showCreditsOnScreen=false] Whether to display the credits of this model on screen.
* @param {SplitDirection} [options.splitDirection=SplitDirection.NONE] The {@link SplitDirection} split to apply to this collection.
* @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for the collection.
* @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the instances in wireframe.
* @exception {DeveloperError} Must specify either or , but not both.
* @exception {DeveloperError} Shader program cannot be optimized for instancing. Parameters cannot have any of the following semantics: MODEL, MODELINVERSE, MODELVIEWINVERSE, MODELVIEWPROJECTIONINVERSE, MODELINVERSETRANSPOSE.
*
* @private
*/
function ModelInstanceCollection(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
//>>includeStart('debug', pragmas.debug);
if (!defined(options.gltf) && !defined(options.url)) {
throw new DeveloperError("Either options.gltf or options.url is required.");
}
if (defined(options.gltf) && defined(options.url)) {
throw new DeveloperError(
"Cannot pass in both options.gltf and options.url."
);
}
//>>includeEnd('debug');
this.show = defaultValue(options.show, true);
this._instancingSupported = false;
this._dynamic = defaultValue(options.dynamic, false);
this._allowPicking = defaultValue(options.allowPicking, true);
this._ready = false;
this._readyPromise = defer();
this._state = LoadState.NEEDS_LOAD;
this._dirty = false;
// Undocumented options
this._cull = defaultValue(options.cull, true);
this._opaquePass = defaultValue(options.opaquePass, Pass.OPAQUE);
this._instances = createInstances(this, options.instances);
// When the model instance collection is backed by an i3dm tile,
// use its batch table resources to modify the shaders, attributes, and uniform maps.
this._batchTable = options.batchTable;
this._model = undefined;
this._vertexBufferTypedArray = undefined; // Hold onto the vertex buffer contents when dynamic is true
this._vertexBuffer = undefined;
this._batchIdBuffer = undefined;
this._instancedUniformsByProgram = undefined;
this._drawCommands = [];
this._modelCommands = undefined;
this._renderStates = undefined;
this._disableCullingRenderStates = undefined;
this._boundingSphere = createBoundingSphere(this);
this._center = Cartesian3.clone(this._boundingSphere.center);
this._rtcTransform = new Matrix4();
this._rtcModelView = new Matrix4(); // Holds onto uniform
this._mode = undefined;
this.modelMatrix = Matrix4.clone(Matrix4.IDENTITY);
this._modelMatrix = Matrix4.clone(this.modelMatrix);
// Passed on to Model
this._url = Resource.createIfNeeded(options.url);
this._requestType = options.requestType;
this._gltf = options.gltf;
this._basePath = Resource.createIfNeeded(options.basePath);
this._asynchronous = options.asynchronous;
this._incrementallyLoadTextures = options.incrementallyLoadTextures;
this._upAxis = options.upAxis; // Undocumented option
this._forwardAxis = options.forwardAxis; // Undocumented option
this.shadows = defaultValue(options.shadows, ShadowMode.ENABLED);
this._shadows = this.shadows;
this._pickIdLoaded = options.pickIdLoaded;
/**
* The {@link SplitDirection} to apply to this collection.
*
* @type {SplitDirection}
* @default {@link SplitDirection.NONE}
*/
this.splitDirection = defaultValue(
options.splitDirection,
SplitDirection.NONE
);
this.debugShowBoundingVolume = defaultValue(
options.debugShowBoundingVolume,
false
);
this._debugShowBoundingVolume = false;
this.debugWireframe = defaultValue(options.debugWireframe, false);
this._debugWireframe = false;
if (defined(options.imageBasedLighting)) {
this._imageBasedLighting = options.imageBasedLighting;
this._shouldDestroyImageBasedLighting = false;
} else {
// Create image-based lighting from the old constructor parameters.
this._imageBasedLighting = new ImageBasedLighting({
imageBasedLightingFactor: options.imageBasedLightingFactor,
luminanceAtZenith: options.luminanceAtZenith,
sphericalHarmonicCoefficients: options.sphericalHarmonicCoefficients,
specularEnvironmentMaps: options.specularEnvironmentMaps,
});
this._shouldDestroyImageBasedLighting = true;
}
this.backFaceCulling = defaultValue(options.backFaceCulling, true);
this._backFaceCulling = this.backFaceCulling;
this.showCreditsOnScreen = defaultValue(options.showCreditsOnScreen, false);
}
Object.defineProperties(ModelInstanceCollection.prototype, {
allowPicking: {
get: function () {
return this._allowPicking;
},
},
length: {
get: function () {
return this._instances.length;
},
},
activeAnimations: {
get: function () {
return this._model.activeAnimations;
},
},
ready: {
get: function () {
return this._ready;
},
},
readyPromise: {
get: function () {
return this._readyPromise.promise;
},
},
imageBasedLighting: {
get: function () {
return this._imageBasedLighting;
},
set: function (value) {
if (value !== this._imageBasedLighting) {
if (
this._shouldDestroyImageBasedLighting &&
!this._imageBasedLighting.isDestroyed()
) {
this._imageBasedLighting.destroy();
}
this._imageBasedLighting = value;
this._shouldDestroyImageBasedLighting = false;
}
},
},
imageBasedLightingFactor: {
get: function () {
return this._imageBasedLighting.imageBasedLightingFactor;
},
set: function (value) {
this._imageBasedLighting.imageBasedLightingFactor = value;
},
},
luminanceAtZenith: {
get: function () {
return this._imageBasedLighting.luminanceAtZenith;
},
set: function (value) {
this._imageBasedLighting.luminanceAtZenith = value;
},
},
sphericalHarmonicCoefficients: {
get: function () {
return this._imageBasedLighting.sphericalHarmonicCoefficients;
},
set: function (value) {
this._imageBasedLighting.sphericalHarmonicCoefficients = value;
},
},
specularEnvironmentMaps: {
get: function () {
return this._imageBasedLighting.specularEnvironmentMaps;
},
set: function (value) {
this._imageBasedLighting.specularEnvironmentMaps = value;
},
},
});
function createInstances(collection, instancesOptions) {
instancesOptions = defaultValue(instancesOptions, []);
const length = instancesOptions.length;
const instances = new Array(length);
for (let i = 0; i < length; ++i) {
const instanceOptions = instancesOptions[i];
const modelMatrix = instanceOptions.modelMatrix;
const instanceId = defaultValue(instanceOptions.batchId, i);
instances[i] = new ModelInstance(collection, modelMatrix, instanceId);
}
return instances;
}
function createBoundingSphere(collection) {
const instancesLength = collection.length;
const points = new Array(instancesLength);
for (let i = 0; i < instancesLength; ++i) {
points[i] = Matrix4.getTranslation(
collection._instances[i]._modelMatrix,
new Cartesian3()
);
}
return BoundingSphere.fromPoints(points);
}
const scratchCartesian = new Cartesian3();
const scratchMatrix = new Matrix4();
ModelInstanceCollection.prototype.expandBoundingSphere = function (
instanceModelMatrix
) {
const translation = Matrix4.getTranslation(
instanceModelMatrix,
scratchCartesian
);
BoundingSphere.expand(
this._boundingSphere,
translation,
this._boundingSphere
);
};
function getCheckUniformSemanticFunction(
modelSemantics,
supportedSemantics,
programId,
uniformMap
) {
return function (uniform, uniformName) {
const semantic = uniform.semantic;
if (defined(semantic) && modelSemantics.indexOf(semantic) > -1) {
if (supportedSemantics.indexOf(semantic) > -1) {
uniformMap[uniformName] = semantic;
} else {
throw new RuntimeError(
`${
"Shader program cannot be optimized for instancing. " + 'Uniform "'
}${uniformName}" in program "${programId}" uses unsupported semantic "${semantic}"`
);
}
}
};
}
function getInstancedUniforms(collection, programId) {
if (defined(collection._instancedUniformsByProgram)) {
return collection._instancedUniformsByProgram[programId];
}
const instancedUniformsByProgram = {};
collection._instancedUniformsByProgram = instancedUniformsByProgram;
// When using CESIUM_RTC_MODELVIEW the CESIUM_RTC center is ignored. Instances are always rendered relative-to-center.
const modelSemantics = [
"MODEL",
"MODELVIEW",
"CESIUM_RTC_MODELVIEW",
"MODELVIEWPROJECTION",
"MODELINVERSE",
"MODELVIEWINVERSE",
"MODELVIEWPROJECTIONINVERSE",
"MODELINVERSETRANSPOSE",
"MODELVIEWINVERSETRANSPOSE",
];
const supportedSemantics = [
"MODELVIEW",
"CESIUM_RTC_MODELVIEW",
"MODELVIEWPROJECTION",
"MODELVIEWINVERSETRANSPOSE",
];
const techniques = collection._model._sourceTechniques;
for (const techniqueId in techniques) {
if (techniques.hasOwnProperty(techniqueId)) {
const technique = techniques[techniqueId];
const program = technique.program;
// Different techniques may share the same program, skip if already processed.
// This assumes techniques that share a program do not declare different semantics for the same uniforms.
if (!defined(instancedUniformsByProgram[program])) {
const uniformMap = {};
instancedUniformsByProgram[program] = uniformMap;
ForEach.techniqueUniform(
technique,
getCheckUniformSemanticFunction(
modelSemantics,
supportedSemantics,
programId,
uniformMap
)
);
}
}
}
return instancedUniformsByProgram[programId];
}
function getVertexShaderCallback(collection) {
return function (vs, programId) {
const instancedUniforms = getInstancedUniforms(collection, programId);
const usesBatchTable = defined(collection._batchTable);
let renamedSource = ShaderSource.replaceMain(vs, "czm_instancing_main");
let globalVarsHeader = "";
let globalVarsMain = "";
for (const uniform in instancedUniforms) {
if (instancedUniforms.hasOwnProperty(uniform)) {
const semantic = instancedUniforms[uniform];
let varName;
if (semantic === "MODELVIEW" || semantic === "CESIUM_RTC_MODELVIEW") {
varName = "czm_instanced_modelView";
} else if (semantic === "MODELVIEWPROJECTION") {
varName = "czm_instanced_modelViewProjection";
globalVarsHeader += "mat4 czm_instanced_modelViewProjection;\n";
globalVarsMain +=
"czm_instanced_modelViewProjection = czm_projection * czm_instanced_modelView;\n";
} else if (semantic === "MODELVIEWINVERSETRANSPOSE") {
varName = "czm_instanced_modelViewInverseTranspose";
globalVarsHeader += "mat3 czm_instanced_modelViewInverseTranspose;\n";
globalVarsMain +=
"czm_instanced_modelViewInverseTranspose = mat3(czm_instanced_modelView);\n";
}
// Remove the uniform declaration
let regex = new RegExp(`uniform.*${uniform}.*`);
renamedSource = renamedSource.replace(regex, "");
// Replace all occurrences of the uniform with the global variable
regex = new RegExp(`${uniform}\\b`, "g");
renamedSource = renamedSource.replace(regex, varName);
}
}
// czm_instanced_model is the model matrix of the instance relative to center
// czm_instanced_modifiedModelView is the transform from the center to view
// czm_instanced_nodeTransform is the local offset of the node within the model
const uniforms =
"uniform mat4 czm_instanced_modifiedModelView;\n" +
"uniform mat4 czm_instanced_nodeTransform;\n";
let batchIdAttribute;
let pickAttribute;
let pickVarying;
if (usesBatchTable) {
batchIdAttribute = "attribute float a_batchId;\n";
pickAttribute = "";
pickVarying = "";
} else {
batchIdAttribute = "";
pickAttribute =
"attribute vec4 pickColor;\n" + "varying vec4 v_pickColor;\n";
pickVarying = " v_pickColor = pickColor;\n";
}
let instancedSource =
`${uniforms + globalVarsHeader}mat4 czm_instanced_modelView;\n` +
`attribute vec4 czm_modelMatrixRow0;\n` +
`attribute vec4 czm_modelMatrixRow1;\n` +
`attribute vec4 czm_modelMatrixRow2;\n${batchIdAttribute}${pickAttribute}${renamedSource}void main()\n` +
`{\n` +
` mat4 czm_instanced_model = mat4(czm_modelMatrixRow0.x, czm_modelMatrixRow1.x, czm_modelMatrixRow2.x, 0.0, czm_modelMatrixRow0.y, czm_modelMatrixRow1.y, czm_modelMatrixRow2.y, 0.0, czm_modelMatrixRow0.z, czm_modelMatrixRow1.z, czm_modelMatrixRow2.z, 0.0, czm_modelMatrixRow0.w, czm_modelMatrixRow1.w, czm_modelMatrixRow2.w, 1.0);\n` +
` czm_instanced_modelView = czm_instanced_modifiedModelView * czm_instanced_model * czm_instanced_nodeTransform;\n${globalVarsMain} czm_instancing_main();\n${pickVarying}}\n`;
if (usesBatchTable) {
const gltf = collection._model.gltf;
const diffuseAttributeOrUniformName = ModelUtility.getDiffuseAttributeOrUniform(
gltf,
programId
);
instancedSource = collection._batchTable.getVertexShaderCallback(
true,
"a_batchId",
diffuseAttributeOrUniformName
)(instancedSource);
}
return instancedSource;
};
}
function getFragmentShaderCallback(collection) {
return function (fs, programId) {
const batchTable = collection._batchTable;
if (defined(batchTable)) {
const gltf = collection._model.gltf;
const diffuseAttributeOrUniformName = ModelUtility.getDiffuseAttributeOrUniform(
gltf,
programId
);
fs = batchTable.getFragmentShaderCallback(
true,
diffuseAttributeOrUniformName,
false
)(fs);
} else {
fs = `varying vec4 v_pickColor;\n${fs}`;
}
return fs;
};
}
function createModifiedModelView(collection, context) {
return function () {
return Matrix4.multiply(
context.uniformState.view,
collection._rtcTransform,
collection._rtcModelView
);
};
}
function createNodeTransformFunction(node) {
return function () {
return node.computedMatrix;
};
}
function getUniformMapCallback(collection, context) {
return function (uniformMap, programId, node) {
uniformMap = clone(uniformMap);
uniformMap.czm_instanced_modifiedModelView = createModifiedModelView(
collection,
context
);
uniformMap.czm_instanced_nodeTransform = createNodeTransformFunction(node);
// Remove instanced uniforms from the uniform map
const instancedUniforms = getInstancedUniforms(collection, programId);
for (const uniform in instancedUniforms) {
if (instancedUniforms.hasOwnProperty(uniform)) {
delete uniformMap[uniform];
}
}
if (defined(collection._batchTable)) {
uniformMap = collection._batchTable.getUniformMapCallback()(uniformMap);
}
return uniformMap;
};
}
function getVertexShaderNonInstancedCallback(collection) {
return function (vs, programId) {
if (defined(collection._batchTable)) {
const gltf = collection._model.gltf;
const diffuseAttributeOrUniformName = ModelUtility.getDiffuseAttributeOrUniform(
gltf,
programId
);
vs = collection._batchTable.getVertexShaderCallback(
true,
"a_batchId",
diffuseAttributeOrUniformName
)(vs);
// Treat a_batchId as a uniform rather than a vertex attribute
vs = `uniform float a_batchId\n;${vs}`;
}
return vs;
};
}
function getFragmentShaderNonInstancedCallback(collection) {
return function (fs, programId) {
const batchTable = collection._batchTable;
if (defined(batchTable)) {
const gltf = collection._model.gltf;
const diffuseAttributeOrUniformName = ModelUtility.getDiffuseAttributeOrUniform(
gltf,
programId
);
fs = batchTable.getFragmentShaderCallback(
true,
diffuseAttributeOrUniformName,
false
)(fs);
} else {
fs = `uniform vec4 czm_pickColor;\n${fs}`;
}
return fs;
};
}
function getUniformMapNonInstancedCallback(collection) {
return function (uniformMap) {
if (defined(collection._batchTable)) {
uniformMap = collection._batchTable.getUniformMapCallback()(uniformMap);
}
return uniformMap;
};
}
function getVertexBufferTypedArray(collection) {
const instances = collection._instances;
const instancesLength = collection.length;
const collectionCenter = collection._center;
const vertexSizeInFloats = 12;
let bufferData = collection._vertexBufferTypedArray;
if (!defined(bufferData)) {
bufferData = new Float32Array(instancesLength * vertexSizeInFloats);
}
if (collection._dynamic) {
// Hold onto the buffer data so we don't have to allocate new memory every frame.
collection._vertexBufferTypedArray = bufferData;
}
for (let i = 0; i < instancesLength; ++i) {
const modelMatrix = instances[i]._modelMatrix;
// Instance matrix is relative to center
const instanceMatrix = Matrix4.clone(modelMatrix, scratchMatrix);
instanceMatrix[12] -= collectionCenter.x;
instanceMatrix[13] -= collectionCenter.y;
instanceMatrix[14] -= collectionCenter.z;
const offset = i * vertexSizeInFloats;
// First three rows of the model matrix
bufferData[offset + 0] = instanceMatrix[0];
bufferData[offset + 1] = instanceMatrix[4];
bufferData[offset + 2] = instanceMatrix[8];
bufferData[offset + 3] = instanceMatrix[12];
bufferData[offset + 4] = instanceMatrix[1];
bufferData[offset + 5] = instanceMatrix[5];
bufferData[offset + 6] = instanceMatrix[9];
bufferData[offset + 7] = instanceMatrix[13];
bufferData[offset + 8] = instanceMatrix[2];
bufferData[offset + 9] = instanceMatrix[6];
bufferData[offset + 10] = instanceMatrix[10];
bufferData[offset + 11] = instanceMatrix[14];
}
return bufferData;
}
function createVertexBuffer(collection, context) {
let i;
const instances = collection._instances;
const instancesLength = collection.length;
const dynamic = collection._dynamic;
const usesBatchTable = defined(collection._batchTable);
if (usesBatchTable) {
const batchIdBufferData = new Uint16Array(instancesLength);
for (i = 0; i < instancesLength; ++i) {
batchIdBufferData[i] = instances[i]._instanceId;
}
collection._batchIdBuffer = Buffer.createVertexBuffer({
context: context,
typedArray: batchIdBufferData,
usage: BufferUsage.STATIC_DRAW,
});
}
if (!usesBatchTable) {
const pickIdBuffer = new Uint8Array(instancesLength * 4);
for (i = 0; i < instancesLength; ++i) {
const pickId = collection._pickIds[i];
const pickColor = pickId.color;
const offset = i * 4;
pickIdBuffer[offset] = Color.floatToByte(pickColor.red);
pickIdBuffer[offset + 1] = Color.floatToByte(pickColor.green);
pickIdBuffer[offset + 2] = Color.floatToByte(pickColor.blue);
pickIdBuffer[offset + 3] = Color.floatToByte(pickColor.alpha);
}
collection._pickIdBuffer = Buffer.createVertexBuffer({
context: context,
typedArray: pickIdBuffer,
usage: BufferUsage.STATIC_DRAW,
});
}
const vertexBufferTypedArray = getVertexBufferTypedArray(collection);
collection._vertexBuffer = Buffer.createVertexBuffer({
context: context,
typedArray: vertexBufferTypedArray,
usage: dynamic ? BufferUsage.STREAM_DRAW : BufferUsage.STATIC_DRAW,
});
}
function updateVertexBuffer(collection) {
const vertexBufferTypedArray = getVertexBufferTypedArray(collection);
collection._vertexBuffer.copyFromArrayView(vertexBufferTypedArray);
}
function createPickIds(collection, context) {
// PERFORMANCE_IDEA: we could skip the pick buffer completely by allocating
// a continuous range of pickIds and then converting the base pickId + batchId
// to RGBA in the shader. The only consider is precision issues, which might
// not be an issue in WebGL 2.
const instances = collection._instances;
const instancesLength = instances.length;
const pickIds = new Array(instancesLength);
for (let i = 0; i < instancesLength; ++i) {
pickIds[i] = context.createPickId(instances[i]);
}
return pickIds;
}
function createModel(collection, context) {
const instancingSupported = collection._instancingSupported;
const usesBatchTable = defined(collection._batchTable);
const allowPicking = collection._allowPicking;
const modelOptions = {
url: collection._url,
requestType: collection._requestType,
gltf: collection._gltf,
basePath: collection._basePath,
shadows: collection._shadows,
cacheKey: undefined,
asynchronous: collection._asynchronous,
allowPicking: allowPicking,
incrementallyLoadTextures: collection._incrementallyLoadTextures,
upAxis: collection._upAxis,
forwardAxis: collection._forwardAxis,
precreatedAttributes: undefined,
vertexShaderLoaded: undefined,
fragmentShaderLoaded: undefined,
uniformMapLoaded: undefined,
pickIdLoaded: collection._pickIdLoaded,
ignoreCommands: true,
opaquePass: collection._opaquePass,
imageBasedLighting: collection._imageBasedLighting,
showOutline: collection.showOutline,
showCreditsOnScreen: collection.showCreditsOnScreen,
};
if (!usesBatchTable) {
collection._pickIds = createPickIds(collection, context);
}
if (instancingSupported) {
createVertexBuffer(collection, context);
const vertexSizeInFloats = 12;
const componentSizeInBytes = ComponentDatatype.getSizeInBytes(
ComponentDatatype.FLOAT
);
const instancedAttributes = {
czm_modelMatrixRow0: {
index: 0, // updated in Model
vertexBuffer: collection._vertexBuffer,
componentsPerAttribute: 4,
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: 0,
strideInBytes: componentSizeInBytes * vertexSizeInFloats,
instanceDivisor: 1,
},
czm_modelMatrixRow1: {
index: 0, // updated in Model
vertexBuffer: collection._vertexBuffer,
componentsPerAttribute: 4,
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: componentSizeInBytes * 4,
strideInBytes: componentSizeInBytes * vertexSizeInFloats,
instanceDivisor: 1,
},
czm_modelMatrixRow2: {
index: 0, // updated in Model
vertexBuffer: collection._vertexBuffer,
componentsPerAttribute: 4,
componentDatatype: ComponentDatatype.FLOAT,
normalize: false,
offsetInBytes: componentSizeInBytes * 8,
strideInBytes: componentSizeInBytes * vertexSizeInFloats,
instanceDivisor: 1,
},
};
// When using a batch table, add a batch id attribute
if (usesBatchTable) {
instancedAttributes.a_batchId = {
index: 0, // updated in Model
vertexBuffer: collection._batchIdBuffer,
componentsPerAttribute: 1,
componentDatatype: ComponentDatatype.UNSIGNED_SHORT,
normalize: false,
offsetInBytes: 0,
strideInBytes: 0,
instanceDivisor: 1,
};
}
if (!usesBatchTable) {
instancedAttributes.pickColor = {
index: 0, // updated in Model
vertexBuffer: collection._pickIdBuffer,
componentsPerAttribute: 4,
componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
normalize: true,
offsetInBytes: 0,
strideInBytes: 0,
instanceDivisor: 1,
};
}
modelOptions.precreatedAttributes = instancedAttributes;
modelOptions.vertexShaderLoaded = getVertexShaderCallback(collection);
modelOptions.fragmentShaderLoaded = getFragmentShaderCallback(collection);
modelOptions.uniformMapLoaded = getUniformMapCallback(collection, context);
if (defined(collection._url)) {
modelOptions.cacheKey = `${collection._url.getUrlComponent()}#instanced`;
}
} else {
modelOptions.vertexShaderLoaded = getVertexShaderNonInstancedCallback(
collection
);
modelOptions.fragmentShaderLoaded = getFragmentShaderNonInstancedCallback(
collection
);
modelOptions.uniformMapLoaded = getUniformMapNonInstancedCallback(
collection,
context
);
}
if (defined(collection._url)) {
collection._model = Model.fromGltf(modelOptions);
} else {
collection._model = new Model(modelOptions);
}
}
function updateWireframe(collection, force) {
if (collection._debugWireframe !== collection.debugWireframe || force) {
collection._debugWireframe = collection.debugWireframe;
// This assumes the original primitive was TRIANGLES and that the triangles
// are connected for the wireframe to look perfect.
const primitiveType = collection.debugWireframe
? PrimitiveType.LINES
: PrimitiveType.TRIANGLES;
const commands = collection._drawCommands;
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].primitiveType = primitiveType;
}
}
}
function getDisableCullingRenderState(renderState) {
const rs = clone(renderState, true);
rs.cull.enabled = false;
return RenderState.fromCache(rs);
}
function updateBackFaceCulling(collection, force) {
if (collection._backFaceCulling !== collection.backFaceCulling || force) {
collection._backFaceCulling = collection.backFaceCulling;
const commands = collection._drawCommands;
const length = commands.length;
let i;
if (!defined(collection._disableCullingRenderStates)) {
collection._disableCullingRenderStates = new Array(length);
collection._renderStates = new Array(length);
for (i = 0; i < length; ++i) {
const renderState = commands[i].renderState;
const derivedRenderState = getDisableCullingRenderState(renderState);
collection._disableCullingRenderStates[i] = derivedRenderState;
collection._renderStates[i] = renderState;
}
}
for (i = 0; i < length; ++i) {
commands[i].renderState = collection._backFaceCulling
? collection._renderStates[i]
: collection._disableCullingRenderStates[i];
}
}
}
function updateShowBoundingVolume(collection, force) {
if (
collection.debugShowBoundingVolume !==
collection._debugShowBoundingVolume ||
force
) {
collection._debugShowBoundingVolume = collection.debugShowBoundingVolume;
const commands = collection._drawCommands;
const length = commands.length;
for (let i = 0; i < length; ++i) {
commands[i].debugShowBoundingVolume = collection.debugShowBoundingVolume;
}
}
}
function createCommands(collection, drawCommands) {
const commandsLength = drawCommands.length;
const instancesLength = collection.length;
const boundingSphere = collection._boundingSphere;
const cull = collection._cull;
for (let i = 0; i < commandsLength; ++i) {
const drawCommand = DrawCommand.shallowClone(drawCommands[i]);
drawCommand.instanceCount = instancesLength;
drawCommand.boundingVolume = boundingSphere;
drawCommand.cull = cull;
if (defined(collection._batchTable)) {
drawCommand.pickId = collection._batchTable.getPickId();
} else {
drawCommand.pickId = "v_pickColor";
}
collection._drawCommands.push(drawCommand);
}
}
function createBatchIdFunction(batchId) {
return function () {
return batchId;
};
}
function createPickColorFunction(color) {
return function () {
return color;
};
}
function createCommandsNonInstanced(collection, drawCommands) {
// When instancing is disabled, create commands for every instance.
const instances = collection._instances;
const commandsLength = drawCommands.length;
const instancesLength = collection.length;
const batchTable = collection._batchTable;
const usesBatchTable = defined(batchTable);
const cull = collection._cull;
for (let i = 0; i < commandsLength; ++i) {
for (let j = 0; j < instancesLength; ++j) {
const drawCommand = DrawCommand.shallowClone(drawCommands[i]);
drawCommand.modelMatrix = new Matrix4(); // Updated in updateCommandsNonInstanced
drawCommand.boundingVolume = new BoundingSphere(); // Updated in updateCommandsNonInstanced
drawCommand.cull = cull;
drawCommand.uniformMap = clone(drawCommand.uniformMap);
if (usesBatchTable) {
drawCommand.uniformMap.a_batchId = createBatchIdFunction(
instances[j]._instanceId
);
} else {
const pickId = collection._pickIds[j];
drawCommand.uniformMap.czm_pickColor = createPickColorFunction(
pickId.color
);
}
collection._drawCommands.push(drawCommand);
}
}
}
function updateCommandsNonInstanced(collection) {
const modelCommands = collection._modelCommands;
const commandsLength = modelCommands.length;
const instancesLength = collection.length;
const collectionTransform = collection._rtcTransform;
const collectionCenter = collection._center;
for (let i = 0; i < commandsLength; ++i) {
const modelCommand = modelCommands[i];
for (let j = 0; j < instancesLength; ++j) {
const commandIndex = i * instancesLength + j;
const drawCommand = collection._drawCommands[commandIndex];
let instanceMatrix = Matrix4.clone(
collection._instances[j]._modelMatrix,
scratchMatrix
);
instanceMatrix[12] -= collectionCenter.x;
instanceMatrix[13] -= collectionCenter.y;
instanceMatrix[14] -= collectionCenter.z;
instanceMatrix = Matrix4.multiply(
collectionTransform,
instanceMatrix,
scratchMatrix
);
const nodeMatrix = modelCommand.modelMatrix;
const modelMatrix = drawCommand.modelMatrix;
Matrix4.multiply(instanceMatrix, nodeMatrix, modelMatrix);
const nodeBoundingSphere = modelCommand.boundingVolume;
const boundingSphere = drawCommand.boundingVolume;
BoundingSphere.transform(
nodeBoundingSphere,
instanceMatrix,
boundingSphere
);
}
}
}
function getModelCommands(model) {
const nodeCommands = model._nodeCommands;
const length = nodeCommands.length;
const drawCommands = [];
for (let i = 0; i < length; ++i) {
const nc = nodeCommands[i];
if (nc.show) {
drawCommands.push(nc.command);
}
}
return drawCommands;
}
function commandsDirty(model) {
const nodeCommands = model._nodeCommands;
const length = nodeCommands.length;
let commandsDirty = false;
for (let i = 0; i < length; i++) {
const nc = nodeCommands[i];
if (nc.command.dirty) {
nc.command.dirty = false;
commandsDirty = true;
}
}
return commandsDirty;
}
function generateModelCommands(modelInstanceCollection, instancingSupported) {
modelInstanceCollection._drawCommands = [];
const modelCommands = getModelCommands(modelInstanceCollection._model);
if (instancingSupported) {
createCommands(modelInstanceCollection, modelCommands);
} else {
createCommandsNonInstanced(modelInstanceCollection, modelCommands);
updateCommandsNonInstanced(modelInstanceCollection);
}
}
function updateShadows(collection, force) {
if (collection.shadows !== collection._shadows || force) {
collection._shadows = collection.shadows;
const castShadows = ShadowMode.castShadows(collection.shadows);
const receiveShadows = ShadowMode.receiveShadows(collection.shadows);
const drawCommands = collection._drawCommands;
const length = drawCommands.length;
for (let i = 0; i < length; ++i) {
const drawCommand = drawCommands[i];
drawCommand.castShadows = castShadows;
drawCommand.receiveShadows = receiveShadows;
}
}
}
ModelInstanceCollection.prototype.update = function (frameState) {
if (frameState.mode === SceneMode.MORPHING) {
return;
}
if (!this.show) {
return;
}
if (this.length === 0) {
return;
}
const context = frameState.context;
if (this._state === LoadState.NEEDS_LOAD) {
this._state = LoadState.LOADING;
this._instancingSupported = context.instancedArrays;
createModel(this, context);
const that = this;
this._model.readyPromise.catch(function (error) {
that._state = LoadState.FAILED;
that._readyPromise.reject(error);
});
}
const instancingSupported = this._instancingSupported;
const model = this._model;
model.imageBasedLighting = this._imageBasedLighting;
model.showCreditsOnScreen = this.showCreditsOnScreen;
model.splitDirection = this.splitDirection;
model.update(frameState);
if (model.ready && this._state === LoadState.LOADING) {
this._state = LoadState.LOADED;
this._ready = true;
// Expand bounding volume to fit the radius of the loaded model including the model's offset from the center
const modelRadius =
model.boundingSphere.radius +
Cartesian3.magnitude(model.boundingSphere.center);
this._boundingSphere.radius += modelRadius;
this._modelCommands = getModelCommands(model);
generateModelCommands(this, instancingSupported);
this._readyPromise.resolve(this);
return;
}
if (this._state !== LoadState.LOADED) {
return;
}
const modeChanged = frameState.mode !== this._mode;
const modelMatrix = this.modelMatrix;
const modelMatrixChanged = !Matrix4.equals(this._modelMatrix, modelMatrix);
if (modeChanged || modelMatrixChanged) {
this._mode = frameState.mode;
Matrix4.clone(modelMatrix, this._modelMatrix);
let rtcTransform = Matrix4.multiplyByTranslation(
this._modelMatrix,
this._center,
this._rtcTransform
);
if (this._mode !== SceneMode.SCENE3D) {
rtcTransform = Transforms.basisTo2D(
frameState.mapProjection,
rtcTransform,
rtcTransform
);
}
Matrix4.getTranslation(rtcTransform, this._boundingSphere.center);
}
if (instancingSupported && this._dirty) {
// If at least one instance has moved assume the collection is now dynamic
this._dynamic = true;
this._dirty = false;
// PERFORMANCE_IDEA: only update dirty sub-sections instead of the whole collection
updateVertexBuffer(this);
}
// If the model was set to rebuild shaders during update, rebuild instanced commands.
const modelCommandsDirty = commandsDirty(model);
if (modelCommandsDirty) {
generateModelCommands(this, instancingSupported);
}
// If any node changes due to an animation, update the commands. This could be inefficient if the model is
// composed of many nodes and only one changes, however it is probably fine in the general use case.
// Only applies when instancing is disabled. The instanced shader automatically handles node transformations.
if (
!instancingSupported &&
(model.dirty || this._dirty || modeChanged || modelMatrixChanged)
) {
updateCommandsNonInstanced(this);
}
updateShadows(this, modelCommandsDirty);
updateWireframe(this, modelCommandsDirty);
updateBackFaceCulling(this, modelCommandsDirty);
updateShowBoundingVolume(this, modelCommandsDirty);
const passes = frameState.passes;
if (!passes.render && !passes.pick) {
return;
}
const commandList = frameState.commandList;
const commands = this._drawCommands;
const commandsLength = commands.length;
for (let i = 0; i < commandsLength; ++i) {
commandList.push(commands[i]);
}
};
ModelInstanceCollection.prototype.isDestroyed = function () {
return false;
};
ModelInstanceCollection.prototype.destroy = function () {
this._model = this._model && this._model.destroy();
const pickIds = this._pickIds;
if (defined(pickIds)) {
const length = pickIds.length;
for (let i = 0; i < length; ++i) {
pickIds[i].destroy();
}
}
if (
this._shouldDestroyImageBasedLighting &&
!this._imageBasedLighting.isDestroyed()
) {
this._imageBasedLighting.destroy();
}
this._imageBasedLighting = undefined;
return destroyObject(this);
};
export default ModelInstanceCollection;