UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,812 lines (1,635 loc) 59.5 kB
import buildVoxelDrawCommands from "./buildVoxelDrawCommands.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Cartographic from "../Core/Cartographic.js"; import Cesium3DTilesetStatistics from "./Cesium3DTilesetStatistics.js"; import CesiumMath from "../Core/Math.js"; import Check from "../Core/Check.js"; import Color from "../Core/Color.js"; import ClippingPlaneCollection from "./ClippingPlaneCollection.js"; import clone from "../Core/clone.js"; import CustomShader from "./Model/CustomShader.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import Event from "../Core/Event.js"; import JulianDate from "../Core/JulianDate.js"; import Material from "./Material.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import MetadataComponentType from "./MetadataComponentType.js"; import MetadataType from "./MetadataType.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import PolylineCollection from "./PolylineCollection.js"; import VerticalExaggeration from "../Core/VerticalExaggeration.js"; import VoxelContent from "./VoxelContent.js"; import VoxelShapeType from "./VoxelShapeType.js"; import VoxelTraversal from "./VoxelTraversal.js"; import VoxelMetadataOrder from "./VoxelMetadataOrder.js"; /** * A primitive that renders voxel data from a {@link VoxelProvider}. * * @alias VoxelPrimitive * @constructor * * @param {object} [options] Object with the following properties: * @param {VoxelProvider} [options.provider] The voxel provider that supplies the primitive with tile data. * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The model matrix used to transform the primitive. * @param {CustomShader} [options.customShader] The custom shader used to style the primitive. * @param {Clock} [options.clock] The clock used to control time dynamic behavior. * @param {Boolean} [options.calculateStatistics] Generate statistics for performance profile. * * @see VoxelProvider * @see Cesium3DTilesVoxelProvider * @see VoxelShapeType * @see {@link https://github.com/CesiumGS/cesium/tree/main/Documentation/CustomShaderGuide|Custom Shader Guide} * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. */ function VoxelPrimitive(options) { options = options ?? Frozen.EMPTY_OBJECT; /** * @type {boolean} * @private */ this._ready = false; /** * @type {VoxelProvider} * @private */ this._provider = options.provider ?? VoxelPrimitive.DefaultProvider; /** * This member is not created until the provider and shape are ready. * * @type {VoxelTraversal} * @private */ this._traversal = undefined; /** * @type {Cesium3DTilesetStatistics} * @private */ this._statistics = new Cesium3DTilesetStatistics(); /** * @type {boolean} * @private */ this._calculateStatistics = options.calculateStatistics ?? false; /** * This member is not created until the provider is ready. * * @type {VoxelShape} * @private */ this._shape = undefined; /** * @type {boolean} * @private */ this._shapeVisible = false; /** * This member is not created until the provider is ready. * * @type {Cartesian3} * @private */ this._dimensions = new Cartesian3(); /** * This member is not created until the provider is ready. * * @type {Cartesian3} * @private */ this._inputDimensions = new Cartesian3(); /** * This member is not created until the provider is ready. * * @type {Cartesian3} * @private */ this._paddingBefore = new Cartesian3(); /** * This member is not created until the provider is ready. * * @type {Cartesian3} * @private */ this._paddingAfter = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minBounds = new Cartesian3(); /** * Used to detect if the shape is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minBoundsOld = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxBounds = new Cartesian3(); /** * Used to detect if the shape is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxBoundsOld = new Cartesian3(); /** * Minimum bounds with vertical exaggeration applied * * @type {Cartesian3} * @private */ this._exaggeratedMinBounds = new Cartesian3(); /** * Used to detect if the shape is dirty. * * @type {Cartesian3} * @private */ this._exaggeratedMinBoundsOld = new Cartesian3(); /** * Maximum bounds with vertical exaggeration applied * * @type {Cartesian3} * @private */ this._exaggeratedMaxBounds = new Cartesian3(); /** * Used to detect if the shape is dirty. * * @type {Cartesian3} * @private */ this._exaggeratedMaxBoundsOld = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minClippingBounds = new Cartesian3(); /** * Used to detect if the clipping is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minClippingBoundsOld = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxClippingBounds = new Cartesian3(); /** * Used to detect if the clipping is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxClippingBoundsOld = new Cartesian3(); /** * Clipping planes on the primitive * * @type {ClippingPlaneCollection} * @private */ this._clippingPlanes = undefined; /** * Keeps track of when the clipping planes change * * @type {number} * @private */ this._clippingPlanesState = 0; /** * Keeps track of when the clipping planes are enabled / disabled * * @type {boolean} * @private */ this._clippingPlanesEnabled = false; /** * The primitive's model matrix. * * @type {Matrix4} * @private */ this._modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY); /** * Model matrix with vertical exaggeration applied. Only used for BOX shape type. * * @type {Matrix4} * @private */ this._exaggeratedModelMatrix = Matrix4.clone(this._modelMatrix); /** * The primitive's model matrix multiplied by the provider's model matrix. * This member is not known until the provider is ready. * * @type {Matrix4} * @private */ this._compoundModelMatrix = new Matrix4(); /** * Used to detect if the shape is dirty. * This member is not known until the provider is ready. * * @type {Matrix4} * @private */ this._compoundModelMatrixOld = new Matrix4(); /** * @type {CustomShader} * @private */ this._customShader = options.customShader ?? VoxelPrimitive.DefaultCustomShader; /** * @type {Event} * @private */ this._customShaderCompilationEvent = new Event(); /** * @type {boolean} * @private */ this._shaderDirty = true; /** * @type {DrawCommand} * @private */ this._drawCommand = undefined; /** * @type {DrawCommand} * @private */ this._drawCommandPick = undefined; /** * @type {object} * @private */ this._pickId = undefined; /** * @type {Clock} * @private */ this._clock = options.clock; // Transforms and other values that are computed when the shape changes /** * @type {Matrix4} * @private */ this._transformPositionWorldToUv = new Matrix4(); /** * @type {Matrix3} * @private */ this._transformDirectionWorldToUv = new Matrix3(); /** * @type {Matrix4} * @private */ this._transformPositionUvToWorld = new Matrix4(); /** * @type {Matrix3} * @private */ this._transformDirectionWorldToLocal = new Matrix3(); // Rendering /** * @type {boolean} * @private */ this._nearestSampling = false; /** * @type {number} * @private */ this._levelBlendFactor = 0.0; /** * @type {number} * @private */ this._stepSizeMultiplier = 1.0; /** * @type {boolean} * @private */ this._depthTest = true; /** * @type {boolean} * @private */ this._useLogDepth = undefined; /** * @type {number} * @private */ this._screenSpaceError = 4.0; // in pixels // Debug / statistics /** * @type {PolylineCollection} * @private */ this._debugPolylines = new PolylineCollection(); /** * @type {boolean} * @private */ this._debugDraw = false; /** * @type {boolean} * @private */ this._disableRender = false; /** * @type {boolean} * @private */ this._disableUpdate = false; /** * @type {Object<string, any>} * @private */ this._uniforms = { octreeInternalNodeTexture: undefined, octreeInternalNodeTilesPerRow: 0, octreeInternalNodeTexelSizeUv: new Cartesian2(), octreeLeafNodeTexture: undefined, octreeLeafNodeTilesPerRow: 0, octreeLeafNodeTexelSizeUv: new Cartesian2(), megatextureTextures: [], megatextureSliceDimensions: new Cartesian2(), megatextureTileDimensions: new Cartesian2(), megatextureVoxelSizeUv: new Cartesian2(), megatextureSliceSizeUv: new Cartesian2(), megatextureTileSizeUv: new Cartesian2(), dimensions: new Cartesian3(), inputDimensions: new Cartesian3(), paddingBefore: new Cartesian3(), paddingAfter: new Cartesian3(), transformPositionViewToUv: new Matrix4(), transformPositionUvToView: new Matrix4(), transformDirectionViewToLocal: new Matrix3(), cameraPositionUv: new Cartesian3(), cameraDirectionUv: new Cartesian3(), ndcSpaceAxisAlignedBoundingBox: new Cartesian4(), clippingPlanesTexture: undefined, clippingPlanesMatrix: new Matrix4(), stepSize: 0, pickColor: new Color(), }; /** * Shape specific shader defines from the previous shape update. Used to detect if the shader needs to be rebuilt. * @type {Object<string, any>} * @private */ this._shapeDefinesOld = {}; /** * Map uniform names to functions that return the uniform values. * @type {Object<string, function():any>} * @private */ this._uniformMap = {}; const uniforms = this._uniforms; const uniformMap = this._uniformMap; for (const key in uniforms) { if (uniforms.hasOwnProperty(key)) { const name = `u_${key}`; uniformMap[name] = function () { return uniforms[key]; }; } } /** * The event fired to indicate that a tile's content was loaded. * <p> * This event is fired during the tileset traversal while the frame is being rendered * so that updates to the tile take effect in the same frame. Do not create or modify * Cesium entities or primitives during the event listener. * </p> * * @type {Event} * * @example * voxelPrimitive.tileLoad.addEventListener(function() { * console.log('A tile was loaded.'); * }); */ this.tileLoad = new Event(); /** * This event fires once for each visible tile in a frame. * <p> * This event is fired during the traversal while the frame is being rendered. * * @type {Event} * * @example * voxelPrimitive.tileVisible.addEventListener(function() { * console.log('A tile is visible.'); * }); * */ this.tileVisible = new Event(); /** * The event fired to indicate that a tile's content failed to load. * * @type {Event} * * @example * voxelPrimitive.tileFailed.addEventListener(function() { * console.log('An error occurred loading tile.'); * }); */ this.tileFailed = new Event(); /** * The event fired to indicate that a tile's content was unloaded. * * @type {Event} * * @example * voxelPrimitive.tileUnload.addEventListener(function() { * console.log('A tile was unloaded from the cache.'); * }); * */ this.tileUnload = new Event(); /** * The event fired to indicate progress of loading new tiles. This event is fired when a new tile * is requested, when a requested tile is finished downloading, and when a downloaded tile has been * processed and is ready to render. * <p> * The number of pending tile requests, <code>numberOfPendingRequests</code>, and number of tiles * processing, <code>numberOfTilesProcessing</code> are passed to the event listener. * </p> * <p> * This event is fired at the end of the frame after the scene is rendered. * </p> * * @type {Event} * * @example * voxelPrimitive.loadProgress.addEventListener(function(numberOfPendingRequests, numberOfTilesProcessing) { * if ((numberOfPendingRequests === 0) && (numberOfTilesProcessing === 0)) { * console.log('Finished loading'); * return; * } * * console.log(`Loading: requests: ${numberOfPendingRequests}, processing: ${numberOfTilesProcessing}`); * }); */ this.loadProgress = new Event(); /** * The event fired to indicate that all tiles that meet the screen space error this frame are loaded. The voxel * primitive is completely loaded for this view. * <p> * This event is fired at the end of the frame after the scene is rendered. * </p> * * @type {Event} * * @example * voxelPrimitive.allTilesLoaded.addEventListener(function() { * console.log('All tiles are loaded'); * }); */ this.allTilesLoaded = new Event(); /** * The event fired to indicate that all tiles that meet the screen space error this frame are loaded. This event * is fired once when all tiles in the initial view are loaded. * <p> * This event is fired at the end of the frame after the scene is rendered. * </p> * * @type {Event} * * @example * voxelPrimitive.initialTilesLoaded.addEventListener(function() { * console.log('Initial tiles are loaded'); * }); * * @see Cesium3DTileset#allTilesLoaded */ this.initialTilesLoaded = new Event(); // If the provider fails to initialize the primitive will fail too. const provider = this._provider; initialize(this, provider); } function initialize(primitive, provider) { // Set the bounds const { shape: shapeType, minBounds = VoxelShapeType.getMinBounds(shapeType), maxBounds = VoxelShapeType.getMaxBounds(shapeType), } = provider; primitive.minBounds = minBounds; primitive.maxBounds = maxBounds; primitive.minClippingBounds = minBounds.clone(); primitive.maxClippingBounds = maxBounds.clone(); // Initialize the exaggerated versions of bounds and model matrix primitive._exaggeratedMinBounds = Cartesian3.clone( primitive._minBounds, primitive._exaggeratedMinBounds, ); primitive._exaggeratedMaxBounds = Cartesian3.clone( primitive._maxBounds, primitive._exaggeratedMaxBounds, ); primitive._exaggeratedModelMatrix = Matrix4.clone( primitive._modelMatrix, primitive._exaggeratedModelMatrix, ); checkTransformAndBounds(primitive, provider); // Create the shape object, and update it so it is valid for VoxelTraversal const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType); primitive._shape = new ShapeConstructor(); primitive._shapeVisible = updateShapeAndTransforms( primitive, primitive._shape, provider, ); } Object.defineProperties(VoxelPrimitive.prototype, { /** * Gets a value indicating whether or not the primitive is ready for use. * * @memberof VoxelPrimitive.prototype * @type {boolean} * @readonly */ ready: { get: function () { return this._ready; }, }, /** * Gets the {@link VoxelProvider} associated with this primitive. * * @memberof VoxelPrimitive.prototype * @type {VoxelProvider} * @readonly */ provider: { get: function () { return this._provider; }, }, /** * Gets the bounding sphere. * * @memberof VoxelPrimitive.prototype * @type {BoundingSphere} * @readonly */ boundingSphere: { get: function () { return this._shape.boundingSphere; }, }, /** * Gets the oriented bounding box. * * @memberof VoxelPrimitive.prototype * @type {OrientedBoundingBox} * @readonly */ orientedBoundingBox: { get: function () { return this._shape.orientedBoundingBox; }, }, /** * Gets the model matrix. * * @memberof VoxelPrimitive.prototype * @type {Matrix4} * @readonly */ modelMatrix: { get: function () { return this._modelMatrix; }, set: function (modelMatrix) { //>>includeStart('debug', pragmas.debug); Check.typeOf.object("modelMatrix", modelMatrix); //>>includeEnd('debug'); this._modelMatrix = Matrix4.clone(modelMatrix, this._modelMatrix); }, }, /** * Gets the shape type. * * @memberof VoxelPrimitive.prototype * @type {VoxelShapeType} * @readonly */ shape: { get: function () { return this._provider.shape; }, }, /** * Gets the dimensions of each voxel tile, in z-up orientation. * Does not include padding. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly */ dimensions: { get: function () { return this._dimensions; }, }, /** * Gets the dimensions of one tile of the input voxel data, in the input orientation. * Includes padding. * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly */ inputDimensions: { get: function () { return this._inputDimensions; }, }, /** * Gets the padding before the voxel data. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly */ paddingBefore: { get: function () { return this._paddingBefore; }, }, /** * Gets the padding after the voxel data. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly */ paddingAfter: { get: function () { return this._paddingAfter; }, }, /** * Gets the minimum value per channel of the voxel data. * * @memberof VoxelPrimitive.prototype * @type {number[][]} * @readonly */ minimumValues: { get: function () { return this._provider.minimumValues; }, }, /** * Gets the maximum value per channel of the voxel data. * * @memberof VoxelPrimitive.prototype * @type {number[][]} * @readonly */ maximumValues: { get: function () { return this._provider.maximumValues; }, }, /** * Gets or sets whether or not this primitive should be displayed. * * @memberof VoxelPrimitive.prototype * @type {boolean} */ show: { get: function () { return !this._disableRender; }, set: function (show) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("show", show); //>>includeEnd('debug'); this._disableRender = !show; }, }, /** * Gets or sets whether or not the primitive should update when the view changes. * * @memberof VoxelPrimitive.prototype * @type {boolean} */ disableUpdate: { get: function () { return this._disableUpdate; }, set: function (disableUpdate) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("disableUpdate", disableUpdate); //>>includeEnd('debug'); this._disableUpdate = disableUpdate; }, }, /** * Gets or sets whether or not to render debug visualizations. * * @memberof VoxelPrimitive.prototype * @type {boolean} */ debugDraw: { get: function () { return this._debugDraw; }, set: function (debugDraw) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("debugDraw", debugDraw); //>>includeEnd('debug'); this._debugDraw = debugDraw; }, }, /** * Gets or sets whether or not to test against depth when rendering. * * @memberof VoxelPrimitive.prototype * @type {boolean} */ depthTest: { get: function () { return this._depthTest; }, set: function (depthTest) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("depthTest", depthTest); //>>includeEnd('debug'); if (this._depthTest !== depthTest) { this._depthTest = depthTest; this._shaderDirty = true; } }, }, /** * Gets or sets the nearest sampling. * * @memberof VoxelPrimitive.prototype * @type {boolean} */ nearestSampling: { get: function () { return this._nearestSampling; }, set: function (nearestSampling) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("nearestSampling", nearestSampling); //>>includeEnd('debug'); if (this._nearestSampling !== nearestSampling) { this._nearestSampling = nearestSampling; this._shaderDirty = true; } }, }, /** * Controls how quickly to blend between different levels of the tree. * 0.0 means an instantaneous pop. * 1.0 means a full linear blend. * * @memberof VoxelPrimitive.prototype * @type {number} * @private */ levelBlendFactor: { get: function () { return this._levelBlendFactor; }, set: function (levelBlendFactor) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("levelBlendFactor", levelBlendFactor); //>>includeEnd('debug'); this._levelBlendFactor = CesiumMath.clamp(levelBlendFactor, 0.0, 1.0); }, }, /** * Gets or sets the screen space error in pixels. If the screen space size * of a voxel is greater than the screen space error, the tile is subdivided. * Lower screen space error corresponds with higher detail rendering, but could * result in worse performance and higher memory consumption. * * @memberof VoxelPrimitive.prototype * @type {number} */ screenSpaceError: { get: function () { return this._screenSpaceError; }, set: function (screenSpaceError) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("screenSpaceError", screenSpaceError); //>>includeEnd('debug'); this._screenSpaceError = screenSpaceError; }, }, /** * Gets or sets the step size multiplier used during raymarching. * The lower the value, the higher the rendering quality, but * also the worse the performance. * * @memberof VoxelPrimitive.prototype * @type {number} */ stepSize: { get: function () { return this._stepSizeMultiplier; }, set: function (stepSize) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("stepSize", stepSize); //>>includeEnd('debug'); this._stepSizeMultiplier = stepSize; }, }, /** * Gets or sets the minimum bounds in the shape's local coordinate system. * Voxel data is stretched or squashed to fit the bounds. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} */ minBounds: { get: function () { return this._minBounds; }, set: function (minBounds) { //>>includeStart('debug', pragmas.debug); Check.defined("minBounds", minBounds); //>>includeEnd('debug'); this._minBounds = Cartesian3.clone(minBounds, this._minBounds); }, }, /** * Gets or sets the maximum bounds in the shape's local coordinate system. * Voxel data is stretched or squashed to fit the bounds. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} */ maxBounds: { get: function () { return this._maxBounds; }, set: function (maxBounds) { //>>includeStart('debug', pragmas.debug); Check.defined("maxBounds", maxBounds); //>>includeEnd('debug'); this._maxBounds = Cartesian3.clone(maxBounds, this._maxBounds); }, }, /** * Gets or sets the minimum clipping location in the shape's local coordinate system. * Any voxel content outside the range is clipped. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} */ minClippingBounds: { get: function () { return this._minClippingBounds; }, set: function (minClippingBounds) { //>>includeStart('debug', pragmas.debug); Check.defined("minClippingBounds", minClippingBounds); //>>includeEnd('debug'); this._minClippingBounds = Cartesian3.clone( minClippingBounds, this._minClippingBounds, ); }, }, /** * Gets or sets the maximum clipping location in the shape's local coordinate system. * Any voxel content outside the range is clipped. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} */ maxClippingBounds: { get: function () { return this._maxClippingBounds; }, set: function (maxClippingBounds) { //>>includeStart('debug', pragmas.debug); Check.defined("maxClippingBounds", maxClippingBounds); //>>includeEnd('debug'); this._maxClippingBounds = Cartesian3.clone( maxClippingBounds, this._maxClippingBounds, ); }, }, /** * The {@link ClippingPlaneCollection} used to selectively disable rendering the primitive. * * @memberof VoxelPrimitive.prototype * @type {ClippingPlaneCollection} */ clippingPlanes: { get: function () { return this._clippingPlanes; }, set: function (clippingPlanes) { // Don't need to check if undefined, it's handled in the setOwner function ClippingPlaneCollection.setOwner(clippingPlanes, this, "_clippingPlanes"); }, }, /** * Gets or sets the custom shader. If undefined, {@link VoxelPrimitive.DefaultCustomShader} is set. * * @memberof VoxelPrimitive.prototype * @type {CustomShader} * @see {@link https://github.com/CesiumGS/cesium/tree/main/Documentation/CustomShaderGuide|Custom Shader Guide} */ customShader: { get: function () { return this._customShader; }, set: function (customShader) { if (this._customShader !== customShader) { // Delete old custom shader entries from the uniform map const uniformMap = this._uniformMap; const oldCustomShader = this._customShader; const oldCustomShaderUniformMap = oldCustomShader.uniformMap; for (const uniformName in oldCustomShaderUniformMap) { if (oldCustomShaderUniformMap.hasOwnProperty(uniformName)) { // If the custom shader was set but the voxel shader was never // built, the custom shader uniforms wouldn't have been added to // the uniform map. But it doesn't matter because the delete // operator ignores if the key doesn't exist. delete uniformMap[uniformName]; } } if (!defined(customShader)) { this._customShader = VoxelPrimitive.DefaultCustomShader; } else { this._customShader = customShader; } this._shaderDirty = true; } }, }, /** * Gets an event that is raised whenever a custom shader is compiled. * * @memberof VoxelPrimitive.prototype * @type {Event} * @readonly */ customShaderCompilationEvent: { get: function () { return this._customShaderCompilationEvent; }, }, /** * Loading and rendering information for requested content * To use `visited` and `numberOfTilesWithContentReady` statistics, set options._calculateStatistics` to `true` in the constructor. * @type {Cesium3DTilesetStatistics} * @readonly * @private */ statistics: { get: function () { return this._statistics; }, }, }); const scratchIntersect = new Cartesian4(); const scratchNdcAabb = new Cartesian4(); const scratchTransformPositionWorldToLocal = new Matrix4(); const scratchTransformPositionLocalToWorld = new Matrix4(); const scratchTransformPositionLocalToProjection = new Matrix4(); const transformPositionLocalToUv = Matrix4.fromRotationTranslation( Matrix3.fromUniformScale(0.5, new Matrix3()), new Cartesian3(0.5, 0.5, 0.5), new Matrix4(), ); const transformPositionUvToLocal = Matrix4.fromRotationTranslation( Matrix3.fromUniformScale(2.0, new Matrix3()), new Cartesian3(-1.0, -1.0, -1.0), new Matrix4(), ); /** * Updates the voxel primitive. * * @param {FrameState} frameState * @private */ VoxelPrimitive.prototype.update = function (frameState) { const provider = this._provider; // Update the custom shader in case it has texture uniforms. this._customShader.update(frameState); // Initialize from the ready provider. This only happens once. const context = frameState.context; if (!this._ready) { initFromProvider(this, provider, context); // Set the primitive as ready after the first frame render since the user might set up events subscribed to // the post render event, and the primitive may not be ready for those past the first frame. frameState.afterRender.push(() => { this._ready = true; return true; }); // Don't render until the next frame after ready is set to true return; } updateVerticalExaggeration(this, frameState); // Check if the shape is dirty before updating it. This needs to happen every // frame because the member variables can be modified externally via the // getters. const shapeDirty = checkTransformAndBounds(this, provider); const shape = this._shape; if (shapeDirty) { this._shapeVisible = updateShapeAndTransforms(this, shape, provider); if (checkShapeDefines(this, shape)) { this._shaderDirty = true; } } if (!this._shapeVisible) { return; } // Update the traversal and prepare for rendering. const keyframeLocation = getKeyframeLocation( provider.timeIntervalCollection, this._clock, ); const traversal = this._traversal; const sampleCountOld = traversal._sampleCount; traversal.update( frameState, keyframeLocation, shapeDirty, // recomputeBoundingVolumes this._disableUpdate, // pauseUpdate ); if (sampleCountOld !== traversal._sampleCount) { this._shaderDirty = true; } if (!traversal.isRenderable(traversal.rootNode)) { return; } if (this._debugDraw) { // Debug draw bounding boxes and other things. Must go after traversal update // because that's what updates the tile bounding boxes. debugDraw(this, frameState); } if (this._disableRender) { return; } // Check if log depth changed if (this._useLogDepth !== frameState.useLogDepth) { this._useLogDepth = frameState.useLogDepth; this._shaderDirty = true; } // Check if clipping planes changed const clippingPlanesChanged = updateClippingPlanes(this, frameState); if (clippingPlanesChanged) { this._shaderDirty = true; } const leafNodeTexture = traversal.leafNodeTexture; const uniforms = this._uniforms; if (defined(leafNodeTexture)) { uniforms.octreeLeafNodeTexture = traversal.leafNodeTexture; uniforms.octreeLeafNodeTexelSizeUv = Cartesian2.clone( traversal.leafNodeTexelSizeUv, uniforms.octreeLeafNodeTexelSizeUv, ); uniforms.octreeLeafNodeTilesPerRow = traversal.leafNodeTilesPerRow; } // Rebuild shaders if (this._shaderDirty) { buildVoxelDrawCommands(this, context); this._shaderDirty = false; } // Calculate the NDC-space AABB to "scissor" the fullscreen quad const transformPositionWorldToProjection = context.uniformState.viewProjection; const orientedBoundingBox = shape.orientedBoundingBox; const ndcAabb = orientedBoundingBoxToNdcAabb( orientedBoundingBox, transformPositionWorldToProjection, scratchNdcAabb, ); // If the object is offscreen, don't render it. const offscreen = ndcAabb.x === +1.0 || ndcAabb.y === +1.0 || ndcAabb.z === -1.0 || ndcAabb.w === -1.0; if (offscreen) { return; } // Prepare to render: update uniforms that can change every frame // Using a uniform instead of going through RenderState's scissor because the viewport is not accessible here, and the scissor command needs pixel coordinates. uniforms.ndcSpaceAxisAlignedBoundingBox = Cartesian4.clone( ndcAabb, uniforms.ndcSpaceAxisAlignedBoundingBox, ); const transformPositionViewToWorld = context.uniformState.inverseView; uniforms.transformPositionViewToUv = Matrix4.multiplyTransformation( this._transformPositionWorldToUv, transformPositionViewToWorld, uniforms.transformPositionViewToUv, ); const transformPositionWorldToView = context.uniformState.view; uniforms.transformPositionUvToView = Matrix4.multiplyTransformation( transformPositionWorldToView, this._transformPositionUvToWorld, uniforms.transformPositionUvToView, ); const transformDirectionViewToWorld = context.uniformState.inverseViewRotation; uniforms.transformDirectionViewToLocal = Matrix3.multiply( this._transformDirectionWorldToLocal, transformDirectionViewToWorld, uniforms.transformDirectionViewToLocal, ); uniforms.cameraPositionUv = Matrix4.multiplyByPoint( this._transformPositionWorldToUv, frameState.camera.positionWC, uniforms.cameraPositionUv, ); uniforms.cameraDirectionUv = Matrix3.multiplyByVector( this._transformDirectionWorldToUv, frameState.camera.directionWC, uniforms.cameraDirectionUv, ); uniforms.cameraDirectionUv = Cartesian3.normalize( uniforms.cameraDirectionUv, uniforms.cameraDirectionUv, ); uniforms.stepSize = this._stepSizeMultiplier; // Render the primitive const command = frameState.passes.pick ? this._drawCommandPick : frameState.passes.pickVoxel ? this._drawCommandPickVoxel : this._drawCommand; command.boundingVolume = shape.boundingSphere; frameState.commandList.push(command); }; const scratchExaggerationScale = new Cartesian3(); const scratchExaggerationCenter = new Cartesian3(); const scratchCartographicCenter = new Cartographic(); const scratchExaggerationTranslation = new Cartesian3(); /** * Update the exaggerated bounds of a primitive to account for vertical exaggeration * Currently only applies to Ellipsoid shape type * @param {VoxelPrimitive} primitive * @param {FrameState} frameState * @private */ function updateVerticalExaggeration(primitive, frameState) { primitive._exaggeratedMinBounds = Cartesian3.clone( primitive._minBounds, primitive._exaggeratedMinBounds, ); primitive._exaggeratedMaxBounds = Cartesian3.clone( primitive._maxBounds, primitive._exaggeratedMaxBounds, ); if (primitive.shape === VoxelShapeType.ELLIPSOID) { // Apply the exaggeration by stretching the height bounds const relativeHeight = frameState.verticalExaggerationRelativeHeight; const exaggeration = frameState.verticalExaggeration; primitive._exaggeratedMinBounds.z = (primitive._minBounds.z - relativeHeight) * exaggeration + relativeHeight; primitive._exaggeratedMaxBounds.z = (primitive._maxBounds.z - relativeHeight) * exaggeration + relativeHeight; } else { // Apply the exaggeration via the model matrix const exaggerationScale = Cartesian3.fromElements( 1.0, 1.0, frameState.verticalExaggeration, scratchExaggerationScale, ); primitive._exaggeratedModelMatrix = Matrix4.multiplyByScale( primitive._modelMatrix, exaggerationScale, primitive._exaggeratedModelMatrix, ); primitive._exaggeratedModelMatrix = Matrix4.multiplyByTranslation( primitive._exaggeratedModelMatrix, computeBoxExaggerationTranslation(primitive, frameState), primitive._exaggeratedModelMatrix, ); } } function computeBoxExaggerationTranslation(primitive, frameState) { // Compute translation based on box center, relative height, and exaggeration const { shapeTransform = Matrix4.IDENTITY, globalTransform = Matrix4.IDENTITY, } = primitive._provider; // Find the Cartesian position of the center of the OBB const initialCenter = Matrix4.getTranslation( shapeTransform, scratchExaggerationCenter, ); const intermediateCenter = Matrix4.multiplyByPoint( primitive._modelMatrix, initialCenter, scratchExaggerationCenter, ); const transformedCenter = Matrix4.multiplyByPoint( globalTransform, intermediateCenter, scratchExaggerationCenter, ); // Find the cartographic height const ellipsoid = Ellipsoid.WGS84; const centerCartographic = ellipsoid.cartesianToCartographic( transformedCenter, scratchCartographicCenter, ); let centerHeight = 0.0; if (defined(centerCartographic)) { centerHeight = centerCartographic.height; } // Find the shift that will put the center in the right position relative // to relativeHeight, after it is scaled by verticalExaggeration const exaggeratedHeight = VerticalExaggeration.getHeight( centerHeight, frameState.verticalExaggeration, frameState.verticalExaggerationRelativeHeight, ); return Cartesian3.fromElements( 0.0, 0.0, (exaggeratedHeight - centerHeight) / frameState.verticalExaggeration, scratchExaggerationTranslation, ); } /** * Initialize primitive properties that are derived from the voxel provider * @param {VoxelPrimitive} primitive * @param {VoxelProvider} provider * @param {Context} context * @private */ function initFromProvider(primitive, provider, context) { const uniforms = primitive._uniforms; primitive._pickId = context.createPickId({ primitive }); uniforms.pickColor = Color.clone(primitive._pickId.color, uniforms.pickColor); const { shaderDefines, shaderUniforms: shapeUniforms } = primitive._shape; primitive._shapeDefinesOld = clone(shaderDefines, true); // Add shape uniforms to the uniform map const uniformMap = primitive._uniformMap; for (const key in shapeUniforms) { if (shapeUniforms.hasOwnProperty(key)) { const name = `u_${key}`; //>>includeStart('debug', pragmas.debug); if (defined(uniformMap[name])) { oneTimeWarning( `VoxelPrimitive: Uniform name "${name}" is already defined`, ); } //>>includeEnd('debug'); uniformMap[name] = function () { return shapeUniforms[key]; }; } } // Set uniforms that come from the provider. // Note that minBounds and maxBounds can be set dynamically, so their uniforms aren't set here. primitive._dimensions = Cartesian3.clone( provider.dimensions, primitive._dimensions, ); uniforms.dimensions = Cartesian3.clone( primitive._dimensions, uniforms.dimensions, ); primitive._paddingBefore = Cartesian3.clone( provider.paddingBefore ?? Cartesian3.ZERO, primitive._paddingBefore, ); uniforms.paddingBefore = Cartesian3.clone( primitive._paddingBefore, uniforms.paddingBefore, ); primitive._paddingAfter = Cartesian3.clone( provider.paddingAfter ?? Cartesian3.ZERO, primitive._paddingAfter, ); uniforms.paddingAfter = Cartesian3.clone( primitive._paddingAfter, uniforms.paddingAfter, ); primitive._inputDimensions = Cartesian3.add( primitive._dimensions, primitive._paddingBefore, primitive._inputDimensions, ); primitive._inputDimensions = Cartesian3.add( primitive._inputDimensions, primitive._paddingAfter, primitive._inputDimensions, ); if (provider.metadataOrder === VoxelMetadataOrder.Y_UP) { const inputDimensionsY = primitive._inputDimensions.y; primitive._inputDimensions.y = primitive._inputDimensions.z; primitive._inputDimensions.z = inputDimensionsY; } uniforms.inputDimensions = Cartesian3.clone( primitive._inputDimensions, uniforms.inputDimensions, ); // Create the VoxelTraversal, and set related uniforms const keyframeCount = provider.keyframeCount ?? 1; primitive._traversal = new VoxelTraversal(primitive, context, keyframeCount); primitive.statistics.texturesByteLength = primitive._traversal.textureMemoryByteLength; setTraversalUniforms(primitive._traversal, uniforms); } /** * Track changes in provider transform and primitive bounds * @param {VoxelPrimitive} primitive * @param {VoxelProvider} provider * @returns {boolean} Whether any of the transform or bounds changed * @private */ function checkTransformAndBounds(primitive, provider) { const shapeTransform = provider.shapeTransform ?? Matrix4.IDENTITY; const globalTransform = provider.globalTransform ?? Matrix4.IDENTITY; // Compound model matrix = global transform * model matrix * shape transform Matrix4.multiplyTransformation( globalTransform, primitive._exaggeratedModelMatrix, primitive._compoundModelMatrix, ); Matrix4.multiplyTransformation( primitive._compoundModelMatrix, shapeTransform, primitive._compoundModelMatrix, ); const numChanges = updateBound(primitive, "_compoundModelMatrix", "_compoundModelMatrixOld") + updateBound(primitive, "_minBounds", "_minBoundsOld") + updateBound(primitive, "_maxBounds", "_maxBoundsOld") + updateBound( primitive, "_exaggeratedMinBounds", "_exaggeratedMinBoundsOld", ) + updateBound( primitive, "_exaggeratedMaxBounds", "_exaggeratedMaxBoundsOld", ) + updateBound(primitive, "_minClippingBounds", "_minClippingBoundsOld") + updateBound(primitive, "_maxClippingBounds", "_maxClippingBoundsOld"); return numChanges > 0; } /** * Compare old and new values of a bound and update the old if it is different. * @param {VoxelPrimitive} primitive The primitive with bounds properties * @param {string} newBoundKey A key pointing to a bounds property of type Cartesian3 or Matrix4 * @param {string} oldBoundKey A key pointing to a bounds property of the same type as the property at newBoundKey * @returns {number} 1 if the bound value changed, 0 otherwise * * @private */ function updateBound(primitive, newBoundKey, oldBoundKey) { const newBound = primitive[newBoundKey]; const oldBound = primitive[oldBoundKey]; const changed = !newBound.equals(oldBound); if (changed) { newBound.clone(oldBound); } return changed ? 1 : 0; } /** * Update the shape and related transforms * @param {VoxelPrimitive} primitive * @param {VoxelShape} shape * @param {VoxelProvider} provider * @returns {boolean} True if the shape is visible * @private */ function updateShapeAndTransforms(primitive, shape, provider) { const visible = shape.update( primitive._compoundModelMatrix, primitive._exaggeratedMinBounds, primitive._exaggeratedMaxBounds, primitive.minClippingBounds, primitive.maxClippingBounds, ); if (!visible) { return false; } const transformPositionLocalToWorld = shape.shapeTransform; const transformPositionWorldToLocal = Matrix4.inverse( transformPositionLocalToWorld, scratchTransformPositionWorldToLocal, ); // Set member variables when the shape is dirty primitive._transformPositionWorldToUv = Matrix4.multiplyTransformation( transformPositionLocalToUv, transformPositionWorldToLocal, primitive._transformPositionWorldToUv, ); primitive._transformDirectionWorldToUv = Matrix4.getMatrix3( primitive._transformPositionWorldToUv, primitive._transformDirectionWorldToUv, ); primitive._transformPositionUvToWorld = Matrix4.multiplyTransformation( transformPositionLocalToWorld, transformPositionUvToLocal, primitive._transformPositionUvToWorld, ); primitive._transformDirectionWorldToLocal = Matrix4.getMatrix3( transformPositionWorldToLocal, primitive._transformDirectionWorldToLocal, ); return true; } /** * Set uniforms that come from the traversal. * @param {VoxelTraversal} traversal * @param {object} uniforms * @private */ function setTraversalUniforms(traversal, uniforms) { uniforms.octreeInternalNodeTexture = traversal.internalNodeTexture; uniforms.octreeInternalNodeTexelSizeUv = Cartesian2.clone( traversal.internalNodeTexelSizeUv, uniforms.octreeInternalNodeTexelSizeUv, ); uniforms.octreeInternalNodeTilesPerRow = traversal.internalNodeTilesPerRow; const megatextures = traversal.megatextures; const megatexture = megatextures[0]; const megatextureLength = megatextures.length; uniforms.megatextureTextures = new Array(megatextureLength); for (let i = 0; i < megatextureLength; i++) { uniforms.megatextureTextures[i] = megatextures[i].texture; } uniforms.megatextureSliceDimensions = Cartesian2.clone( megatexture.sliceCountPerRegion, uniforms.megatextureSliceDimensions, ); uniforms.megatextureTileDimensions = Cartesian2.clone( megatexture.regionCountPerMegatexture, uniforms.megatextureTileDimensions, ); uniforms.megatextureVoxelSizeUv = Cartesian2.clone( megatexture.voxelSizeUv, uniforms.megatextureVoxelSizeUv, ); uniforms.megatextureSliceSizeUv = Cartesian2.clone( megatexture.sliceSizeUv, uniforms.megatextureSliceSizeUv, ); uniforms.megatextureTileSizeUv = Cartesian2.clone( megatexture.regionSizeUv, uniforms.megatextureTileSizeUv, ); } /** * Track changes in shape-related shader defines * @param {VoxelPrimitive} primitive * @param {VoxelShape} shape * @returns {boolean} True if any of the shape defines changed, requiring a shader rebuild * @private */ function checkShapeDefines(primitive, shape) { const shapeDefines = shape.shaderDefines; const shapeDefinesChanged = Object.keys(shapeDefines).some( (key) => shapeDefines[key] !== primitive._shapeDefinesOld[key], ); if (shapeDefinesChanged) { primitive._shapeDefinesOld = clone(shapeDefines, true); } return shapeDefinesChanged; } /** * Find the keyframe location to render at. Doesn't need to be a whole number. * @param {TimeIntervalCollection} timeIntervalCollection * @param {Clock} clock * @returns {number} * * @private */ function getKeyframeLocation(timeIntervalCollection, clock) { if (!defined(timeIntervalCollection) || !defined(clock)) { return 0.0; } let date = clock.currentTime; let timeInterval; let timeIntervalIndex = timeIntervalCollection.indexOf(date); if (timeIntervalIndex >= 0) { timeInterval = timeIntervalCollection.get(timeIntervalIndex); } else { // Date fell outside the range timeIntervalIndex = ~timeIntervalIndex; if (timeIntervalIndex === timeIntervalCollection.length) { // Date past range timeIntervalIndex = timeIntervalCollection.length - 1; timeInterval = timeIntervalCollection.get(timeIntervalIndex); date = timeInterval.stop; } else { // Date before range timeInterval = timeIntervalCollection.get(timeIntervalIndex); date = timeInterval.start; } } // De-lerp between the start and end of the interval const totalSeconds = JulianDate.secondsDifference( timeInterval.stop, timeInterval.start, ); const secondsDifferenceStart = JulianDate.secondsDifference( date, timeInterval.start, ); const t = secondsDifferenceStart / totalSeconds; return timeIntervalIndex + t; } /** * Update the clipping planes state and associated uniforms * * @param {VoxelPrimitive} primitive * @param {FrameState} frameState * @returns {boolean} Whether the clipping planes changed, requiring a shader rebuild * @private */ function updateClippingPlanes(primitive, frameState) { const clippingPlanes = primitive.clippingPlanes; if (!defined(clippingPlanes)) { return false; } clippingPlanes.update(frameState); const { clippingPlanesState, enabled } = clippingPlanes; if (enabled) { const uniforms = primitive._uniforms; uniforms.clippingPlanesTexture = clippingPlanes.texture; // Compute the clipping plane's transformation to uv space and then take the inverse // transpose to properly transform the hessian normal form of the plane. // transpose(inverse(worldToUv * clippingPlaneLocalToWorld)) // transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToUv)) // transpose(inverse(clippingPlaneLocalToWorld) * uvToWorld) uniforms.clippingPlanesMatrix = Matrix4.transpose( Matrix4.multiplyTransformation( Matrix4.inverse( clippingPlanes.modelMatrix, uniforms.clippingPlanesMatrix, ), primitive._transformPositionUvToWorld, uniforms.clippingPlanesMatrix, ), uniforms.clippingPlanesMatrix, ); } if ( primitive._clippingPlanesState === clippingPlanesState && primitive._clippingPlanesEnabled === enabled ) { return false; } primitive._clippingPlanesState = clippingPlanesState; primitive._clippingPlanesEnabled = enabled; return true; } /** * Returns true if this object was destroyed; otherwise, false. * <br /><br /> * If this object was destroyed, it should not be used; calling any function other than * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. * * @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>. * * @see VoxelPrimitive#destroy */ VoxelPrimitive.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 o