import buildVoxelCustomShader from "./buildVoxelCustomShader.js"; 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; const { provider = VoxelPrimitive.DefaultProvider, modelMatrix = Matrix4.IDENTITY, customShader = buildVoxelCustomShader(provider), clock, calculateStatistics = false, } = options; /** * @type {boolean} * @private */ this._ready = false; /** * @type {VoxelProvider} * @private */ this._provider = provider; /** * This member is not created until the first update loop. * * @type {VoxelTraversal} * @private */ this._traversal = undefined; /** * @type {Cesium3DTilesetStatistics} * @private */ this._statistics = new Cesium3DTilesetStatistics(); /** * @type {boolean} * @private */ this._calculateStatistics = calculateStatistics; const { shape: shapeType, minBounds = VoxelShapeType.getMinBounds(shapeType), maxBounds = VoxelShapeType.getMaxBounds(shapeType), dimensions, paddingBefore = Cartesian3.ZERO, paddingAfter = Cartesian3.ZERO, metadataOrder, availableLevels = 1, } = provider; /** * @type {Cartesian3} * @readonly * @constant * @private */ this._dimensions = Cartesian3.clone(dimensions); /** * @type {Cartesian3} * @private */ this._paddingBefore = Cartesian3.clone(paddingBefore); /** * @type {Cartesian3} * @private */ this._paddingAfter = Cartesian3.clone(paddingAfter); /** * @type {Cartesian3} * @private */ this._inputDimensions = computeInputDimensions( dimensions, paddingBefore, paddingAfter, metadataOrder, ); /** * @type {number} * @private */ this._availableLevels = availableLevels; /** * @type {Cartesian3} * @private */ this._minBounds = minBounds.clone(); /** * Used to detect if the shape is dirty. * * @type {Cartesian3} * @private */ this._minBoundsOld = new Cartesian3(); /** * @type {Cartesian3} * @private */ this._maxBounds = maxBounds.clone(); /** * Used to detect if the shape is dirty. * * @type {Cartesian3} * @private */ this._maxBoundsOld = new Cartesian3(); /** * @type {Cartesian3} * @private */ this._minClippingBounds = minBounds.clone(); /** * Used to detect if the clipping is dirty. * * @type {Cartesian3} * @private */ this._minClippingBoundsOld = new Cartesian3(); /** * @type {Cartesian3} * @private */ this._maxClippingBounds = maxBounds.clone(); /** * Used to detect if the clipping is dirty. * * @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(modelMatrix); /** * Used to detect if the model matrix is dirty. * * @type {Matrix4} * @private */ this._modelMatrixOld = Matrix4.clone(this._modelMatrix); /** * @type {CustomShader} * @private */ this._customShader = 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 = 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; const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType); /** * @type {VoxelShape} * @private */ this._shape = new ShapeConstructor(); checkTransformAndBounds(this); /** * @type {boolean} * @private */ this._shapeVisible = updateShapeAndTransforms(this); /** * @type {Object} * @private */ this._uniforms = { octreeInternalNodeTexture: undefined, octreeInternalNodeTilesPerRow: 0, octreeInternalNodeTexelSizeUv: new Cartesian2(), octreeLeafNodeTexture: undefined, octreeLeafNodeTilesPerRow: 0, octreeLeafNodeTexelSizeUv: new Cartesian2(), megatextureTextures: [], megatextureTileCounts: new Cartesian3(), dimensions: this._dimensions, inputDimensions: this._inputDimensions, paddingBefore: this._paddingBefore, paddingAfter: this._paddingAfter, 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: this._stepSizeMultiplier, pickColor: new Color(), }; /** * Shape specific shader defines from the previous shape update. Used to detect if the shader needs to be rebuilt. * @type {Object} * @private */ this._shapeDefinesOld = {}; /** * Map uniform names to functions that return the uniform values. * @type {Object} * @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]; }; } } setupShapeUniformsAndDefines(this, this._shape); /** * The event fired to indicate that a tile's content was loaded. *

* 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. *

* * @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. *

* 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. *

* The number of pending tile requests, numberOfPendingRequests, and number of tiles * processing, numberOfTilesProcessing are passed to the event listener. *

*

* This event is fired at the end of the frame after the scene is rendered. *

* * @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. *

* This event is fired at the end of the frame after the scene is rendered. *

* * @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. *

* This event is fired at the end of the frame after the scene is rendered. *

* * @type {Event} * * @example * voxelPrimitive.initialTilesLoaded.addEventListener(function() { * console.log('Initial tiles are loaded'); * }); * * @see Cesium3DTileset#allTilesLoaded */ this.initialTilesLoaded = new Event(); } /** * Computes the dimensions of the input voxel data, including padding and in the input orientation. * * @param {Cartesian3} dimensions The dimensions of the voxel data, not including padding, in z-up orientation. * @param {Cartesian3} paddingBefore The padding before the voxel data. * @param {Cartesian3} paddingAfter The padding after the voxel data. * @param {VoxelMetadataOrder} metadataOrder The ordering of the input metadata dimensions. * * @private */ function computeInputDimensions( dimensions, paddingBefore, paddingAfter, metadataOrder, ) { const inputDimensions = Cartesian3.add( dimensions, paddingBefore, new Cartesian3(), ); Cartesian3.add(inputDimensions, paddingAfter, inputDimensions); if (metadataOrder === VoxelMetadataOrder.Y_UP) { const inputDimensionsY = inputDimensions.y; inputDimensions.y = inputDimensions.z; inputDimensions.z = inputDimensionsY; } return inputDimensions; } /** * Combine uniforms from the shape with the primitive uniform map, and * setup change tracking for shape defines to know when to rebuild the shader. * * @param {VoxelPrimitive} primitive The primitive with which the shape uniforms are associated. * @param {VoxelShape} shape The shape from which to pull the shader uniforms and defines. * * @private */ function setupShapeUniformsAndDefines(primitive, shape) { const uniformMap = primitive._uniformMap; const { shaderUniforms, shaderDefines } = shape; for (const uniformName in shaderUniforms) { if (shaderUniforms.hasOwnProperty(uniformName)) { const name = `u_${uniformName}`; //>>includeStart('debug', pragmas.debug); if (defined(uniformMap[name])) { oneTimeWarning( `VoxelPrimitive: Uniform name "${name}" is already defined`, ); } //>>includeEnd('debug'); uniformMap[name] = function () { return shaderUniforms[uniformName]; }; } } primitive._shapeDefinesOld = clone(shaderDefines, true); } 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 * @constant */ 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 * @constant */ inputDimensions: { get: function () { return this._inputDimensions; }, }, /** * Gets the padding before the voxel data. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly * @constant */ paddingBefore: { get: function () { return this._paddingBefore; }, }, /** * Gets the padding after the voxel data. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly * @constant */ paddingAfter: { get: function () { return this._paddingAfter; }, }, /** * Gets the minimum value per channel of the voxel data. * * @memberof VoxelPrimitive.prototype * @type {number[][]} * @readonly * @constant */ minimumValues: { get: function () { return this._provider.minimumValues; }, }, /** * Gets the maximum value per channel of the voxel data. * * @memberof VoxelPrimitive.prototype * @type {number[][]} * @readonly * @constant */ 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'); this._nearestSampling = nearestSampling; }, }, /** * 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, attempt to build a default custom shader * appropriate to the metadata type. If that fails, use {@link VoxelPrimitive.DefaultCustomShader}. * * @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 (customShader === this._customShader) { return; } // Delete old custom shader entries from the uniform map // (they were added in VoxelRenderResources when the shader was built) const uniformMap = this._uniformMap; const oldCustomShader = this._customShader; const oldCustomShaderUniformMap = oldCustomShader.uniformMap; for (const uniformName in oldCustomShaderUniformMap) { if (oldCustomShaderUniformMap.hasOwnProperty(uniformName)) { delete uniformMap[uniformName]; } } if (!defined(customShader)) { const defaultShader = buildVoxelCustomShader(this._provider); this._customShader = defaultShader ?? 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 provider. This only happens once. const context = frameState.context; if (!this._ready) { initializeFromContext(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; updateNearestSampling(this); 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 updateNearestSampling(primitive) { const { megatextures } = primitive._traversal; for (let i = 0; i < megatextures.length; ++i) { megatextures[i].nearestSampling = primitive._nearestSampling; } } 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} true 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 initializeFromContext(primitive, provider, context) { const uniforms = primitive._uniforms; primitive._pickId = context.createPickId({ primitive }); uniforms.pickColor = Color.clone(primitive._pickId.color, uniforms.pickColor); // 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; const megatexture = megatextures[0]; uniforms.megatextureTextures = new Array(megatextures.length); for (let i = 0; i < megatextures.length; i++) { uniforms.megatextureTextures[i] = megatextures[i].texture; } uniforms.megatextureTileCounts = Cartesian3.clone( megatexture.tileCounts, uniforms.megatextureTileCounts, ); } /** * 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 plane's transformation to local space and then take the inverse // transpose to properly transform the hessian normal form of the plane. // transpose(inverse(worldToLocal * clippingPlaneLocalToWorld)) // transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToLocal)) // transpose(inverse(clippingPlaneLocalToWorld) * localToWorld) uniforms.clippingPlanesMatrix = Matrix4.transpose( Matrix4.multiplyTransformation( Matrix4.inverse( clippingPlanes.modelMatrix, uniforms.clippingPlanesMatrix, ), primitive._transformPositionLocalToWorld, 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. *

* If this object was destroyed, it should not be used; calling any function other than * isDestroyed will result in a {@link DeveloperError} exception. * * @returns {boolean} true if this object was destroyed; otherwise, false. * * @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 object. *

* Once an object is destroyed, it should not be used; calling any function other than * isDestroyed will result in a {@link DeveloperError} exception. Therefore, * assign the return value (undefined) to the object as done in the example. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * @see VoxelPrimitive#isDestroyed * * @example * voxelPrimitive = voxelPrimitive && voxelPrimitive.destroy(); */ VoxelPrimitive.prototype.destroy = function () { const drawCommand = this._drawCommand; if (defined(drawCommand)) { drawCommand.shaderProgram = drawCommand.shaderProgram && drawCommand.shaderProgram.destroy(); } const drawCommandPick = this._drawCommandPick; if (defined(drawCommandPick)) { drawCommandPick.shaderProgram = drawCommandPick.shaderProgram && drawCommandPick.shaderProgram.destroy(); } this._pickId = this._pickId && this._pickId.destroy(); this._traversal = this._traversal && this._traversal.destroy(); this.statistics.texturesByteLength = 0; this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy(); return destroyObject(this); }; const corners = new Array( new Cartesian4(-1.0, -1.0, -1.0, 1.0), new Cartesian4(+1.0, -1.0, -1.0, 1.0), new Cartesian4(-1.0, +1.0, -1.0, 1.0), new Cartesian4(+1.0, +1.0, -1.0, 1.0), new Cartesian4(-1.0, -1.0, +1.0, 1.0), new Cartesian4(+1.0, -1.0, +1.0, 1.0), new Cartesian4(-1.0, +1.0, +1.0, 1.0), new Cartesian4(+1.0, +1.0, +1.0, 1.0), ); const vertexNeighborIndices = new Array( 1, 2, 4, 0, 3, 5, 0, 3, 6, 1, 2, 7, 0, 5, 6, 1, 4, 7, 2, 4, 7, 3, 5, 6, ); const scratchCornersClipSpace = new Array( new Cartesian4(), new Cartesian4(), new Cartesian4(), new Cartesian4(), new Cartesian4(), new Cartesian4(), new Cartesian4(), new Cartesian4(), ); /** * Projects all 8 corners of the oriented bounding box to NDC space and finds the * resulting NDC axis aligned bounding box. To avoid projecting a vertex that is * behind the near plane, it uses the intersection point of each of the vertex's * edges against the near plane as part of the AABB calculation. This is done in * clip space prior to perspective division. * * @function * * @param {OrientedBoundingBox} orientedBoundingBox * @param {Matrix4} worldToProjection * @param {Cartesian4} result * @returns {Cartesian4} * * @private */ function orientedBoundingBoxToNdcAabb( orientedBoundingBox, worldToProjection, result, ) { const transformPositionLocalToWorld = Matrix4.fromRotationTranslation( orientedBoundingBox.halfAxes, orientedBoundingBox.center, scratchTransformPositionLocalToWorld, ); const transformPositionLocalToProjection = Matrix4.multiply( worldToProjection, transformPositionLocalToWorld, scratchTransformPositionLocalToProjection, ); let ndcMinX = +Number.MAX_VALUE; let ndcMaxX = -Number.MAX_VALUE; let ndcMinY = +Number.MAX_VALUE; let ndcMaxY = -Number.MAX_VALUE; let cornerIndex; // Convert all points to clip space const cornersClipSpace = scratchCornersClipSpace; const cornersLength = corners.length; for (cornerIndex = 0; cornerIndex < cornersLength; cornerIndex++) { Matrix4.multiplyByVector( transformPositionLocalToProjection, corners[cornerIndex], cornersClipSpace[cornerIndex], ); } for (cornerIndex = 0; cornerIndex < cornersLength; cornerIndex++) { const position = cornersClipSpace[cornerIndex]; if (position.z >= -position.w) { // Position is past near plane, so there's no need to clip. const ndcX = position.x / position.w; const ndcY = position.y / position.w; ndcMinX = Math.min(ndcMinX, ndcX); ndcMaxX = Math.max(ndcMaxX, ndcX); ndcMinY = Math.min(ndcMinY, ndcY); ndcMaxY = Math.max(ndcMaxY, ndcY); } else { for (let neighborIndex = 0; neighborIndex < 3; neighborIndex++) { const neighborVertexIndex = vertexNeighborIndices[cornerIndex * 3 + neighborIndex]; const neighborPosition = cornersClipSpace[neighborVertexIndex]; if (neighborPosition.z >= -neighborPosition.w) { // Position is behind the near plane and neighbor is after, so get intersection point on the near plane. const distanceToPlaneFromPosition = position.z + position.w; const distanceToPlaneFromNeighbor = neighborPosition.z + neighborPosition.w; const t = distanceToPlaneFromPosition / (distanceToPlaneFromPosition - distanceToPlaneFromNeighbor); const intersect = Cartesian4.lerp( position, neighborPosition, t, scratchIntersect, ); const intersectNdcX = intersect.x / intersect.w; const intersectNdcY = intersect.y / intersect.w; ndcMinX = Math.min(ndcMinX, intersectNdcX); ndcMaxX = Math.max(ndcMaxX, intersectNdcX); ndcMinY = Math.min(ndcMinY, intersectNdcY); ndcMaxY = Math.max(ndcMaxY, intersectNdcY); } } } } // Clamp the NDC values to -1 to +1 range even if they extend much further. ndcMinX = CesiumMath.clamp(ndcMinX, -1.0, +1.0); ndcMinY = CesiumMath.clamp(ndcMinY, -1.0, +1.0); ndcMaxX = CesiumMath.clamp(ndcMaxX, -1.0, +1.0); ndcMaxY = CesiumMath.clamp(ndcMaxY, -1.0, +1.0); result = Cartesian4.fromElements(ndcMinX, ndcMinY, ndcMaxX, ndcMaxY, result); return result; } const polylineAxisDistance = 30000000.0; const polylineXAxis = new Cartesian3(polylineAxisDistance, 0.0, 0.0); const polylineYAxis = new Cartesian3(0.0, polylineAxisDistance, 0.0); const polylineZAxis = new Cartesian3(0.0, 0.0, polylineAxisDistance); /** * Draws the tile bounding boxes and axes. * * @function * * @param {VoxelPrimitive} that * @param {FrameState} frameState * * @private */ function debugDraw(that, frameState) { const traversal = that._traversal; const polylines = that._debugPolylines; polylines.removeAll(); function makePolylineLineSegment(startPos, endPos, color, thickness) { polylines.add({ positions: [startPos, endPos], width: thickness, material: Material.fromType("Color", { color: color, }), }); } function makePolylineBox(orientedBoundingBox, color, thickness) { // Normally would want to use a scratch variable to store the corners, but // polylines don't clone the positions. const corners = orientedBoundingBox.computeCorners(); makePolylineLineSegment(corners[0], corners[1], color, thickness); makePolylineLineSegment(corners[2], corners[3], color, thickness); makePolylineLineSegment(corners[4], corners[5], color, thickness); makePolylineLineSegment(corners[6], corners[7], color, thickness); makePolylineLineSegment(corners[0], corners[2], color, thickness); makePolylineLineSegment(corners[4], corners[6], color, thickness); makePolylineLineSegment(corners[1], corners[3], color, thickness); makePolylineLineSegment(corners[5], corners[7], color, thickness); makePolylineLineSegment(corners[0], corners[4], color, thickness); makePolylineLineSegment(corners[2], corners[6], color, thickness); makePolylineLineSegment(corners[1], corners[5], color, thickness); makePolylineLineSegment(corners[3], corners[7], color, thickness); } function drawTile(tile) { if (!traversal.isRenderable(tile)) { return; } const level = tile.level; const startThickness = 5.0; const thickness = Math.max(1.0, startThickness / Math.pow(2.0, level)); const colors = [Color.RED, Color.LIME, Color.BLUE]; const color = colors[level % 3]; makePolylineBox(tile.orientedBoundingBox, color, thickness); if (defined(tile.children)) { for (let i = 0; i < 8; i++) { drawTile(tile.children[i]); } } } makePolylineBox(that._shape.orientedBoundingBox, Color.WHITE, 5.0); drawTile(traversal.rootNode); const axisThickness = 10.0; makePolylineLineSegment( Cartesian3.ZERO, polylineXAxis, Color.RED, axisThickness, ); makePolylineLineSegment( Cartesian3.ZERO, polylineYAxis, Color.LIME, axisThickness, ); makePolylineLineSegment( Cartesian3.ZERO, polylineZAxis, Color.BLUE, axisThickness, ); polylines.update(frameState); } /** * The default custom shader used by the primitive. * * @type {CustomShader} * @constant * @readonly * * @private */ VoxelPrimitive.DefaultCustomShader = new CustomShader({ fragmentShaderText: `void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { vec3 voxelNormal = fsInput.attributes.normalEC; float diffuse = max(0.0, dot(voxelNormal, czm_lightDirectionEC)); float lighting = 0.5 + 0.5 * diffuse; material.diffuse = vec3(lighting); material.alpha = 1.0; }`, }); function DefaultVoxelProvider() { this.shape = VoxelShapeType.BOX; this.dimensions = new Cartesian3(1, 1, 1); this.names = ["data"]; this.types = [MetadataType.SCALAR]; this.componentTypes = [MetadataComponentType.FLOAT32]; this.maximumTileCount = 1; } DefaultVoxelProvider.prototype.requestData = function (options) { const tileLevel = defined(options) ? (options.tileLevel ?? 0) : 0; if (tileLevel >= 1) { return undefined; } const content = new VoxelContent({ metadata: [new Float32Array(1)] }); return Promise.resolve(content); }; VoxelPrimitive.DefaultProvider = new DefaultVoxelProvider(); export default VoxelPrimitive;