UNPKG

@cesium/engine

Version:

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

1,806 lines (1,628 loc) 61.2 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 {number} * @private */ this._availableLevels = 1; /** * 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(); /** * 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(); /** * Vertical exaggeration applied to the voxel shape * * @type {number} * @private */ this._verticalExaggeration = 1.0; /** * The height relative to which the shape is exaggerated. * * @type {number} * @private */ this._verticalExaggerationRelativeHeight = 0.0; /** * 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); /** * Used to detect if the model matrix is dirty. * * @type {Matrix4} * @private */ this._modelMatrixOld = Matrix4.clone(this._modelMatrix); /** * @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._transformPositionLocalToWorld = new Matrix4(); /** * @type {Matrix4} * @private */ this._transformPositionWorldToLocal = new Matrix4(); /** * Transforms a plane in Hessian normal form from local space to view space. * @type {Matrix4} * @private */ this._transformPlaneLocalToView = 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(), transformPositionViewToLocal: new Matrix4(), transformDirectionViewToLocal: new Matrix3(), cameraPositionLocal: new Cartesian3(), cameraDirectionLocal: new Cartesian3(), cameraTileCoordinates: new Cartesian4(), cameraTileUv: new Cartesian3(), ndcSpaceAxisAlignedBoundingBox: new Cartesian4(), clippingPlanesTexture: undefined, clippingPlanesMatrix: new Matrix4(), renderBoundPlanesTexture: undefined, 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(); checkTransformAndBounds(primitive); // 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); } 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 scratchTransformPositionLocalToWorld = new Matrix4(); const scratchTransformPositionLocalToProjection = new Matrix4(); const scratchCameraPositionShapeUv = new Cartesian3(); const scratchCameraTileCoordinates = new Cartesian4(); /** * Updates the voxel primitive. * * @param {FrameState} frameState * @private */ VoxelPrimitive.prototype.update = function (frameState) { const provider = this._provider; const uniforms = this._uniforms; // 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; } // 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); const exaggerationChanged = updateVerticalExaggeration(this, frameState); if (shapeDirty || exaggerationChanged) { this._shapeVisible = updateShapeAndTransforms(this); if (checkShapeDefines(this)) { this._shaderDirty = true; } } if (!this._shapeVisible) { return; } this._shape.updateViewTransforms(frameState); // 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; 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 } = this._shape; 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; const transformPositionViewToLocal = Matrix4.multiplyTransformation( this._transformPositionWorldToLocal, transformPositionViewToWorld, uniforms.transformPositionViewToLocal, ); this._transformPlaneLocalToView = Matrix4.transpose( transformPositionViewToLocal, this._transformPlaneLocalToView, ); const transformDirectionViewToWorld = context.uniformState.inverseViewRotation; uniforms.transformDirectionViewToLocal = Matrix3.multiply( this._transformDirectionWorldToLocal, transformDirectionViewToWorld, uniforms.transformDirectionViewToLocal, ); uniforms.cameraPositionLocal = Matrix4.multiplyByPoint( this._transformPositionWorldToLocal, frameState.camera.positionWC, uniforms.cameraPositionLocal, ); uniforms.cameraDirectionLocal = Matrix3.multiplyByVector( this._transformDirectionWorldToLocal, frameState.camera.directionWC, uniforms.cameraDirectionLocal, ); const cameraTileCoordinates = getTileCoordinates( this, uniforms.cameraPositionLocal, scratchCameraTileCoordinates, ); uniforms.cameraTileCoordinates = Cartesian4.fromElements( Math.floor(cameraTileCoordinates.x), Math.floor(cameraTileCoordinates.y), Math.floor(cameraTileCoordinates.z), cameraTileCoordinates.w, uniforms.cameraTileCoordinates, ); uniforms.cameraTileUv = Cartesian3.fromElements( cameraTileCoordinates.x - Math.floor(cameraTileCoordinates.x), cameraTileCoordinates.y - Math.floor(cameraTileCoordinates.y), cameraTileCoordinates.z - Math.floor(cameraTileCoordinates.z), uniforms.cameraTileUv, ); uniforms.stepSize = this._stepSizeMultiplier; updateRenderBoundPlanes(this, frameState); // Render the primitive const command = frameState.passes.pick ? this._drawCommandPick : frameState.passes.pickVoxel ? this._drawCommandPickVoxel : this._drawCommand; command.boundingVolume = this._shape.boundingSphere; frameState.commandList.push(command); }; function updateRenderBoundPlanes(primitive, frameState) { const uniforms = primitive._uniforms; const { renderBoundPlanes } = primitive._shape; if (!defined(renderBoundPlanes)) { return; } renderBoundPlanes.update(frameState, primitive._transformPlaneLocalToView); uniforms.renderBoundPlanesTexture = renderBoundPlanes.texture; } /** * Converts a position in local space to tile coordinates. * * @param {VoxelPrimitive} primitive The primitive to get the tile coordinates for. * @param {Cartesian3} positionLocal The position in local space to convert to tile coordinates. * @param {Cartesian4} result The result object to store the tile coordinates. * @returns {Cartesian4} The tile coordinates of the supplied position. * @private */ function getTileCoordinates(primitive, positionLocal, result) { const shapeUv = primitive._shape.convertLocalToShapeUvSpace( positionLocal, scratchCameraPositionShapeUv, ); const availableLevels = primitive._availableLevels; const numTiles = 2 ** (availableLevels - 1); return Cartesian4.fromElements( shapeUv.x * numTiles, shapeUv.y * numTiles, shapeUv.z * numTiles, availableLevels - 1, result, ); } const scratchExaggerationScale = new Cartesian3(); const scratchExaggerationCenter = new Cartesian3(); const scratchCartographicCenter = new Cartographic(); /** * Check for changes in the vertical exaggeration of the primitive * @param {VoxelPrimitive} primitive The primitive to update * @param {FrameState} frameState The current frame state * @returns {boolean} <code>true</code> if the exaggeration was changed * @private */ function updateVerticalExaggeration(primitive, frameState) { const { verticalExaggeration, verticalExaggerationRelativeHeight } = frameState; if ( primitive._verticalExaggeration === verticalExaggeration && primitive._verticalExaggerationRelativeHeight === verticalExaggerationRelativeHeight ) { return false; } primitive._verticalExaggeration = verticalExaggeration; primitive._verticalExaggerationRelativeHeight = verticalExaggerationRelativeHeight; return true; } /** * 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, ); primitive._availableLevels = provider.availableLevels ?? 1; // 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 * @returns {boolean} Whether any of the transform or bounds changed * @private */ function checkTransformAndBounds(primitive) { const numChanges = updateBound(primitive, "_modelMatrix", "_modelMatrixOld") + updateBound(primitive, "_minBounds", "_minBoundsOld") + updateBound(primitive, "_maxBounds", "_maxBoundsOld") + 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; } const scratchExaggeratedMinBounds = new Cartesian3(); const scratchExaggeratedMaxBounds = new Cartesian3(); const scratchExaggeratedMinClippingBounds = new Cartesian3(); const scratchExaggeratedMaxClippingBounds = new Cartesian3(); const scratchExaggeratedModelMatrix = new Matrix4(); const scratchCompoundModelMatrix = new Matrix4(); /** * Update the shape and related transforms * @param {VoxelPrimitive} primitive * @returns {boolean} True if the shape is visible * @private */ function updateShapeAndTransforms(primitive) { const verticalExaggeration = primitive._verticalExaggeration; const verticalExaggerationRelativeHeight = primitive._verticalExaggerationRelativeHeight; const exaggeratedMinBounds = Cartesian3.clone( primitive._minBounds, scratchExaggeratedMinBounds, ); const exaggeratedMaxBounds = Cartesian3.clone( primitive._maxBounds, scratchExaggeratedMaxBounds, ); const exaggeratedMinClippingBounds = Cartesian3.clone( primitive._minClippingBounds, scratchExaggeratedMinClippingBounds, ); const exaggeratedMaxClippingBounds = Cartesian3.clone( primitive._maxClippingBounds, scratchExaggeratedMaxClippingBounds, ); const exaggeratedModelMatrix = Matrix4.clone( primitive._modelMatrix, scratchExaggeratedModelMatrix, ); if (primitive.shape === VoxelShapeType.ELLIPSOID) { // Apply the exaggeration by stretching the height bounds exaggeratedMinBounds.z = VerticalExaggeration.getHeight( primitive._minBounds.z, verticalExaggeration, verticalExaggerationRelativeHeight, ); exaggeratedMaxBounds.z = VerticalExaggeration.getHeight( primitive._maxBounds.z, verticalExaggeration, verticalExaggerationRelativeHeight, ); exaggeratedMinClippingBounds.z = VerticalExaggeration.getHeight( primitive._minClippingBounds.z, verticalExaggeration, verticalExaggerationRelativeHeight, ); exaggeratedMaxClippingBounds.z = VerticalExaggeration.getHeight( primitive._maxClippingBounds.z, verticalExaggeration, verticalExaggerationRelativeHeight, ); } else { // Apply the exaggeration via the model matrix const exaggerationScale = Cartesian3.fromElements( 1.0, 1.0, verticalExaggeration, scratchExaggerationScale, ); Matrix4.multiplyByScale( exaggeratedModelMatrix, exaggerationScale, exaggeratedModelMatrix, ); Matrix4.multiplyByTranslation( exaggeratedModelMatrix, computeBoxExaggerationTranslation(primitive), exaggeratedModelMatrix, ); } const provider = primitive._provider; const shapeTransform = provider.shapeTransform ?? Matrix4.IDENTITY; const globalTransform = provider.globalTransform ?? Matrix4.IDENTITY; // Compound model matrix = global transform * model matrix * shape transform const compoundModelMatrix = Matrix4.multiplyTransformation( globalTransform, exaggeratedModelMatrix, scratchCompoundModelMatrix, ); Matrix4.multiplyTransformation( compoundModelMatrix, shapeTransform, compoundModelMatrix, ); const shape = primitive._shape; const visible = shape.update( compoundModelMatrix, exaggeratedMinBounds, exaggeratedMaxBounds, exaggeratedMinClippingBounds, exaggeratedMaxClippingBounds, ); if (!visible) { return false; } primitive._transformPositionLocalToWorld = Matrix4.clone( shape.shapeTransform, primitive._transformPositionLocalToWorld, ); primitive._transformPositionWorldToLocal = Matrix4.inverse( primitive._transformPositionLocalToWorld, primitive._transformPositionWorldToLocal, ); primitive._transformDirectionWorldToLocal = Matrix4.getMatrix3( primitive._transformPositionWorldToLocal, primitive._transformDirectionWorldToLocal, ); return true; } const scratchExaggerationTranslation = new Cartesian3(); /** * Compute the translation to apply to box shapes to account for vertical exaggeration * * @param {VoxelPrimitive} primitive * @returns {Cartesian3} The translation to apply to the box to account for vertical exaggeration * @private */ function computeBoxExaggerationTranslation(primitive) { const verticalExaggeration = primitive._verticalExaggeration; const verticalExaggerationRelativeHeight = primitive._verticalExaggerationRelativeHeight; // 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, verticalExaggeration, verticalExaggerationRelativeHeight, ); return Cartesian3.fromElements( 0.0, 0.0, (exaggeratedHeight - centerHeight) / verticalExaggeration, scratchExaggerationTranslation, ); } /** * 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 * @returns {boolean} True if any of the shape defines changed, requiring a shader rebuild * @private */ function checkShapeDefines(primitive) { const { shaderDefines } = primitive._shape; const shapeDefinesChanged = Object.keys(shaderDefines).some( (key) => shaderDefines[key] !== primitive._shapeDefinesOld[key], ); if (shapeDefinesChanged) { primitive._shapeDefinesOld = clone(shaderDefines, 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