import buildVoxelDrawCommands from "./buildVoxelDrawCommands.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import CesiumMath from "../Core/Math.js"; import Check from "../Core/Check.js"; import clone from "../Core/clone.js"; import Color from "../Core/Color.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import deprecationWarning from "../Core/deprecationWarning.js"; import destroyObject from "../Core/destroyObject.js"; import Event from "../Core/Event.js"; import JulianDate from "../Core/JulianDate.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import ClippingPlaneCollection from "./ClippingPlaneCollection.js"; import Material from "./Material.js"; import MetadataComponentType from "./MetadataComponentType.js"; import MetadataType from "./MetadataType.js"; import PolylineCollection from "./PolylineCollection.js"; import VoxelShapeType from "./VoxelShapeType.js"; import VoxelTraversal from "./VoxelTraversal.js"; import CustomShader from "./Model/CustomShader.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. * * @see VoxelProvider * @see Cesium3DTilesVoxelProvider * @see VoxelShapeType * * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. */ function VoxelPrimitive(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); /** * @type {boolean} * @private */ this._ready = false; /** * @type {VoxelProvider} * @private */ this._provider = defaultValue( options.provider, VoxelPrimitive.DefaultProvider ); /** * This member is not created until the provider and shape are ready. * * @type {VoxelTraversal} * @private */ this._traversal = undefined; /** * 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._paddingBefore = new Cartesian3(); /** * This member is not created until the provider is ready. * * @type {Cartesian3} * @private */ this._paddingAfter = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minBounds = new Cartesian3(); /** * Used to detect if the shape is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minBoundsOld = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxBounds = new Cartesian3(); /** * Used to detect if the shape is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxBoundsOld = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minClippingBounds = new Cartesian3(); /** * Used to detect if the clipping is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._minClippingBoundsOld = new Cartesian3(); /** * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxClippingBounds = new Cartesian3(); /** * Used to detect if the clipping is dirty. * This member is not known until the provider is ready. * * @type {Cartesian3} * @private */ this._maxClippingBoundsOld = new Cartesian3(); /** * Clipping planes on the primitive * * @type {ClippingPlaneCollection} * @private */ this._clippingPlanes = undefined; /** * Keeps track of when the clipping planes change * * @type {number} * @private */ this._clippingPlanesState = 0; /** * Keeps track of when the clipping planes are enabled / disabled * * @type {boolean} * @private */ this._clippingPlanesEnabled = false; /** * The primitive's model matrix. * * @type {Matrix4} * @private */ this._modelMatrix = Matrix4.clone( defaultValue(options.modelMatrix, Matrix4.IDENTITY) ); /** * The primitive's model matrix multiplied by the provider's model matrix. * This member is not known until the provider is ready. * * @type {Matrix4} * @private */ this._compoundModelMatrix = new Matrix4(); /** * Used to detect if the shape is dirty. * This member is not known until the provider is ready. * * @type {Matrix4} * @private */ this._compoundModelMatrixOld = new Matrix4(); /** * @type {CustomShader} * @private */ this._customShader = defaultValue( options.customShader, VoxelPrimitive.DefaultCustomShader ); /** * @type {Event} * @private */ this._customShaderCompilationEvent = new Event(); /** * @type {boolean} * @private */ this._shaderDirty = true; /** * @type {DrawCommand} * @private */ this._drawCommand = undefined; /** * @type {DrawCommand} * @private */ this._drawCommandPick = undefined; /** * @type {object} * @private */ this._pickId = undefined; /** * @type {Clock} * @private */ this._clock = options.clock; // Transforms and other values that are computed when the shape changes /** * @type {Matrix4} * @private */ this._transformPositionWorldToUv = new Matrix4(); /** * @type {Matrix4} * @private */ this._transformPositionUvToWorld = new Matrix4(); /** * @type {Matrix3} * @private */ this._transformDirectionWorldToLocal = new Matrix3(); /** * @type {Matrix3} * @private */ this._transformNormalLocalToWorld = new Matrix3(); /** * @type {number} * @private */ this._stepSizeUv = 1.0; // Rendering /** * @type {boolean} * @private */ this._jitter = true; /** * @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} * @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(), paddingBefore: new Cartesian3(), paddingAfter: new Cartesian3(), transformPositionViewToUv: new Matrix4(), transformPositionUvToView: new Matrix4(), transformDirectionViewToLocal: new Matrix3(), transformNormalLocalToWorld: new Matrix3(), cameraPositionUv: new Cartesian3(), ndcSpaceAxisAlignedBoundingBox: new Cartesian4(), clippingPlanesTexture: undefined, clippingPlanesMatrix: new Matrix4(), stepSize: 0, pickColor: new Color(), }; /** * Shape specific shader defines from the previous shape update. Used to detect if the shader needs to be rebuilt. * @type {Object} * @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]; }; } } // If the provider fails to initialize the primitive will fail too. const provider = this._provider; this._completeLoad = function (primitive, frameState) {}; this._readyPromise = initialize(this, provider); } async function initialize(primitive, provider) { const promise = new Promise(function (resolve) { primitive._completeLoad = function (primitive, frameState) { // 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(function () { primitive._ready = true; resolve(primitive); return true; }); }; }); // This is here for backwards compatibility. It can be removed when readyPromise is removed. if (defined(provider._readyPromise) && !provider._ready) { await provider._readyPromise; } // Set the bounds const { shape: shapeType, minBounds = VoxelShapeType.getMinBounds(shapeType), maxBounds = VoxelShapeType.getMaxBounds(shapeType), } = provider; primitive.minBounds = minBounds; primitive.maxBounds = maxBounds; primitive.minClippingBounds = VoxelShapeType.getMinBounds(shapeType); primitive.maxClippingBounds = VoxelShapeType.getMaxBounds(shapeType); checkTransformAndBounds(primitive, provider); // Create the shape object, and update it so it is valid for VoxelTraversal const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType); primitive._shape = new ShapeConstructor(); primitive._shapeVisible = updateShapeAndTransforms( primitive, primitive._shape, provider ); return promise; } 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 promise that will be resolved when the primitive is ready for use. * * @memberof VoxelPrimitive.prototype * @type {Promise} * @readonly * @deprecated */ readyPromise: { get: function () { deprecationWarning( "VoxelPrimitive.readyPromise", "VoxelPrimitive.readyPromise was deprecated in CesiumJS 1.104. It will be removed in 1.107. Wait for VoxelPrimitive.ready to return true instead." ); return this._readyPromise; }, }, /** * 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 voxel dimensions. * * @memberof VoxelPrimitive.prototype * @type {Cartesian3} * @readonly */ dimensions: { get: function () { return this._provider.dimensions; }, }, /** * 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 whether or not to jitter the view ray during the raymarch. * This reduces stair-step artifacts but introduces noise. * * @memberof VoxelPrimitive.prototype * @type {boolean} */ jitter: { get: function () { return this._jitter; }, set: function (jitter) { //>>includeStart('debug', pragmas.debug); Check.typeOf.bool("jitter", jitter); //>>includeEnd('debug'); if (this._jitter !== jitter) { this._jitter = jitter; 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} */ 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; }, }, }); const scratchDimensions = new Cartesian3(); const scratchIntersect = new Cartesian4(); const scratchNdcAabb = new Cartesian4(); const scratchScale = new Cartesian3(); const scratchLocalScale = new Cartesian3(); const scratchRotation = new Matrix3(); const scratchRotationAndLocalScale = new Matrix3(); const scratchTransformPositionWorldToLocal = new Matrix4(); const scratchTransformPositionLocalToWorld = new Matrix4(); const scratchTransformPositionLocalToProjection = new Matrix4(); const transformPositionLocalToUv = Matrix4.fromRotationTranslation( Matrix3.fromUniformScale(0.5, new Matrix3()), new Cartesian3(0.5, 0.5, 0.5), new Matrix4() ); const transformPositionUvToLocal = Matrix4.fromRotationTranslation( Matrix3.fromUniformScale(2.0, new Matrix3()), new Cartesian3(-1.0, -1.0, -1.0), new Matrix4() ); /** * Updates the voxel primitive. * * @param {FrameState} frameState * @private */ VoxelPrimitive.prototype.update = function (frameState) { const provider = this._provider; // Update the custom shader in case it has texture uniforms. this._customShader.update(frameState); // Exit early if it's not ready yet. // This is here for backward compatibility. It can be removed when readyPromise is removed. if ((defined(provider._ready) && !provider._ready) || !defined(this._shape)) { return; } // Initialize from the ready provider. This only happens once. const context = frameState.context; if (!this._ready) { initFromProvider(this, provider, context); this._completeLoad(this, frameState); // Don't render until the next frame after the ready promise is resolved 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, provider); const shape = this._shape; if (shapeDirty) { this._shapeVisible = updateShapeAndTransforms(this, shape, provider); if (checkShapeDefines(this, shape)) { this._shaderDirty = true; } } if (!this._shapeVisible) { return; } // Update the traversal and prepare for rendering. const keyframeLocation = getKeyframeLocation( provider.timeIntervalCollection, this._clock ); const traversal = this._traversal; const sampleCountOld = traversal._sampleCount; traversal.update( frameState, keyframeLocation, shapeDirty, // recomputeBoundingVolumes this._disableUpdate // pauseUpdate ); if (sampleCountOld !== traversal._sampleCount) { this._shaderDirty = true; } if (!traversal.isRenderable(traversal.rootNode)) { return; } if (this._debugDraw) { // Debug draw bounding boxes and other things. Must go after traversal update // because that's what updates the tile bounding boxes. debugDraw(this, frameState); } if (this._disableRender) { return; } // Check if log depth changed if (this._useLogDepth !== frameState.useLogDepth) { this._useLogDepth = frameState.useLogDepth; this._shaderDirty = true; } // Check if clipping planes changed const clippingPlanesChanged = updateClippingPlanes(this, frameState); if (clippingPlanesChanged) { this._shaderDirty = true; } const leafNodeTexture = traversal.leafNodeTexture; const uniforms = this._uniforms; if (defined(leafNodeTexture)) { uniforms.octreeLeafNodeTexture = traversal.leafNodeTexture; uniforms.octreeLeafNodeTexelSizeUv = Cartesian2.clone( traversal.leafNodeTexelSizeUv, uniforms.octreeLeafNodeTexelSizeUv ); uniforms.octreeLeafNodeTilesPerRow = traversal.leafNodeTilesPerRow; } // Rebuild shaders if (this._shaderDirty) { buildVoxelDrawCommands(this, context); this._shaderDirty = false; } // Calculate the NDC-space AABB to "scissor" the fullscreen quad const transformPositionWorldToProjection = context.uniformState.viewProjection; const orientedBoundingBox = shape.orientedBoundingBox; const ndcAabb = orientedBoundingBoxToNdcAabb( orientedBoundingBox, transformPositionWorldToProjection, scratchNdcAabb ); // If the object is offscreen, don't render it. const offscreen = ndcAabb.x === +1.0 || ndcAabb.y === +1.0 || ndcAabb.z === -1.0 || ndcAabb.w === -1.0; if (offscreen) { return; } // Prepare to render: update uniforms that can change every frame // Using a uniform instead of going through RenderState's scissor because the viewport is not accessible here, and the scissor command needs pixel coordinates. uniforms.ndcSpaceAxisAlignedBoundingBox = Cartesian4.clone( ndcAabb, uniforms.ndcSpaceAxisAlignedBoundingBox ); const transformPositionViewToWorld = context.uniformState.inverseView; uniforms.transformPositionViewToUv = Matrix4.multiplyTransformation( this._transformPositionWorldToUv, transformPositionViewToWorld, uniforms.transformPositionViewToUv ); const transformPositionWorldToView = context.uniformState.view; uniforms.transformPositionUvToView = Matrix4.multiplyTransformation( transformPositionWorldToView, this._transformPositionUvToWorld, uniforms.transformPositionUvToView ); const transformDirectionViewToWorld = context.uniformState.inverseViewRotation; uniforms.transformDirectionViewToLocal = Matrix3.multiply( this._transformDirectionWorldToLocal, transformDirectionViewToWorld, uniforms.transformDirectionViewToLocal ); uniforms.transformNormalLocalToWorld = Matrix3.clone( this._transformNormalLocalToWorld, uniforms.transformNormalLocalToWorld ); const cameraPositionWorld = frameState.camera.positionWC; uniforms.cameraPositionUv = Matrix4.multiplyByPoint( this._transformPositionWorldToUv, cameraPositionWorld, uniforms.cameraPositionUv ); uniforms.stepSize = this._stepSizeUv * this._stepSizeMultiplier; // Render the primitive const command = frameState.passes.pick ? this._drawCommandPick : this._drawCommand; command.boundingVolume = shape.boundingSphere; frameState.commandList.push(command); }; /** * 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. uniforms.dimensions = Cartesian3.clone( provider.dimensions, uniforms.dimensions ); primitive._paddingBefore = Cartesian3.clone( defaultValue(provider.paddingBefore, Cartesian3.ZERO), primitive._paddingBefore ); uniforms.paddingBefore = Cartesian3.clone( primitive._paddingBefore, uniforms.paddingBefore ); primitive._paddingAfter = Cartesian3.clone( defaultValue(provider.paddingAfter, Cartesian3.ZERO), primitive._paddingBefore ); uniforms.paddingAfter = Cartesian3.clone( primitive._paddingAfter, uniforms.paddingAfter ); // Create the VoxelTraversal, and set related uniforms primitive._traversal = setupTraversal(primitive, provider, context); setTraversalUniforms(primitive._traversal, uniforms); } /** * Track changes in provider transform and primitive bounds * @param {VoxelPrimitive} primitive * @param {VoxelProvider} provider * @returns {boolean} Whether any of the transform or bounds changed * @private */ function checkTransformAndBounds(primitive, provider) { const shapeTransform = defaultValue( provider.shapeTransform, Matrix4.IDENTITY ); const globalTransform = defaultValue( provider.globalTransform, Matrix4.IDENTITY ); // Compound model matrix = global transform * model matrix * shape transform Matrix4.multiplyTransformation( globalTransform, primitive._modelMatrix, primitive._compoundModelMatrix ); Matrix4.multiplyTransformation( primitive._compoundModelMatrix, shapeTransform, primitive._compoundModelMatrix ); const numChanges = updateBound(primitive, "_compoundModelMatrix", "_compoundModelMatrixOld") + updateBound(primitive, "_minBounds", "_minBoundsOld") + updateBound(primitive, "_maxBounds", "_maxBoundsOld") + updateBound(primitive, "_minClippingBounds", "_minClippingBoundsOld") + updateBound(primitive, "_maxClippingBounds", "_maxClippingBoundsOld"); return numChanges > 0; } /** * Compare old and new values of a bound and update the old if it is different. * @param {VoxelPrimitive} primitive The primitive with bounds properties * @param {string} newBoundKey A key pointing to a bounds property of type Cartesian3 or Matrix4 * @param {string} oldBoundKey A key pointing to a bounds property of the same type as the property at newBoundKey * @returns {number} 1 if the bound value changed, 0 otherwise * * @private */ function updateBound(primitive, newBoundKey, oldBoundKey) { const newBound = primitive[newBoundKey]; const oldBound = primitive[oldBoundKey]; const changed = !newBound.equals(oldBound); if (changed) { newBound.clone(oldBound); } return changed ? 1 : 0; } /** * Update the shape and related transforms * @param {VoxelPrimitive} primitive * @param {VoxelShape} shape * @param {VoxelProvider} provider * @returns {boolean} True if the shape is visible * @private */ function updateShapeAndTransforms(primitive, shape, provider) { const visible = shape.update( primitive._compoundModelMatrix, primitive.minBounds, primitive.maxBounds, primitive.minClippingBounds, primitive.maxClippingBounds ); if (!visible) { return false; } const transformPositionLocalToWorld = shape.shapeTransform; const transformPositionWorldToLocal = Matrix4.inverse( transformPositionLocalToWorld, scratchTransformPositionWorldToLocal ); const rotation = Matrix4.getRotation( transformPositionLocalToWorld, scratchRotation ); // Note that inverse(rotation) is the same as transpose(rotation) const scale = Matrix4.getScale(transformPositionLocalToWorld, scratchScale); const maximumScaleComponent = Cartesian3.maximumComponent(scale); const localScale = Cartesian3.divideByScalar( scale, maximumScaleComponent, scratchLocalScale ); const rotationAndLocalScale = Matrix3.multiplyByScale( rotation, localScale, scratchRotationAndLocalScale ); // Set member variables when the shape is dirty const dimensions = provider.dimensions; primitive._stepSizeUv = shape.computeApproximateStepSize(dimensions); primitive._transformPositionWorldToUv = Matrix4.multiplyTransformation( transformPositionLocalToUv, transformPositionWorldToLocal, primitive._transformPositionWorldToUv ); primitive._transformPositionUvToWorld = Matrix4.multiplyTransformation( transformPositionLocalToWorld, transformPositionUvToLocal, primitive._transformPositionUvToWorld ); primitive._transformDirectionWorldToLocal = Matrix4.getMatrix3( transformPositionWorldToLocal, primitive._transformDirectionWorldToLocal ); primitive._transformNormalLocalToWorld = Matrix3.inverseTranspose( rotationAndLocalScale, primitive._transformNormalLocalToWorld ); return true; } /** * Set up a VoxelTraversal based on dimensions and types from the primitive and provider * @param {VoxelPrimitive} primitive * @param {VoxelProvider} provider * @param {Context} context * @returns {VoxelTraversal} * @private */ function setupTraversal(primitive, provider, context) { const dimensions = Cartesian3.clone(provider.dimensions, scratchDimensions); Cartesian3.add(dimensions, primitive._paddingBefore, dimensions); Cartesian3.add(dimensions, primitive._paddingAfter, dimensions); // It's ok for memory byte length to be undefined. // The system will choose a default memory size. const maximumTileCount = provider.maximumTileCount; const maximumTextureMemoryByteLength = defined(maximumTileCount) ? VoxelTraversal.getApproximateTextureMemoryByteLength( maximumTileCount, dimensions, provider.types, provider.componentTypes ) : undefined; const keyframeCount = defaultValue(provider.keyframeCount, 1); return new VoxelTraversal( primitive, context, dimensions, provider.types, provider.componentTypes, keyframeCount, maximumTextureMemoryByteLength ); } /** * Set uniforms that come from the traversal. * @param {VoxelTraversal} traversal * @param {object} uniforms * @private */ function setTraversalUniforms(traversal, uniforms) { uniforms.octreeInternalNodeTexture = traversal.internalNodeTexture; uniforms.octreeInternalNodeTexelSizeUv = Cartesian2.clone( traversal.internalNodeTexelSizeUv, uniforms.octreeInternalNodeTexelSizeUv ); uniforms.octreeInternalNodeTilesPerRow = traversal.internalNodeTilesPerRow; const megatextures = traversal.megatextures; const megatexture = megatextures[0]; const megatextureLength = megatextures.length; uniforms.megatextureTextures = new Array(megatextureLength); for (let i = 0; i < megatextureLength; i++) { uniforms.megatextureTextures[i] = megatextures[i].texture; } uniforms.megatextureSliceDimensions = Cartesian2.clone( megatexture.sliceCountPerRegion, uniforms.megatextureSliceDimensions ); uniforms.megatextureTileDimensions = Cartesian2.clone( megatexture.regionCountPerMegatexture, uniforms.megatextureTileDimensions ); uniforms.megatextureVoxelSizeUv = Cartesian2.clone( megatexture.voxelSizeUv, uniforms.megatextureVoxelSizeUv ); uniforms.megatextureSliceSizeUv = Cartesian2.clone( megatexture.sliceSizeUv, uniforms.megatextureSliceSizeUv ); uniforms.megatextureTileSizeUv = Cartesian2.clone( megatexture.regionSizeUv, uniforms.megatextureTileSizeUv ); } /** * Track changes in shape-related shader defines * @param {VoxelPrimitive} primitive * @param {VoxelShape} shape * @returns {boolean} True if any of the shape defines changed, requiring a shader rebuild * @private */ function checkShapeDefines(primitive, shape) { const shapeDefines = shape.shaderDefines; const shapeDefinesChanged = Object.keys(shapeDefines).some( (key) => shapeDefines[key] !== primitive._shapeDefinesOld[key] ); if (shapeDefinesChanged) { primitive._shapeDefinesOld = clone(shapeDefines, true); } return shapeDefinesChanged; } /** * Find the keyframe location to render at. Doesn't need to be a whole number. * @param {TimeIntervalCollection} timeIntervalCollection * @param {Clock} clock * @returns {number} * * @private */ function getKeyframeLocation(timeIntervalCollection, clock) { if (!defined(timeIntervalCollection) || !defined(clock)) { return 0.0; } let date = clock.currentTime; let timeInterval; let timeIntervalIndex = timeIntervalCollection.indexOf(date); if (timeIntervalIndex >= 0) { timeInterval = timeIntervalCollection.get(timeIntervalIndex); } else { // Date fell outside the range timeIntervalIndex = ~timeIntervalIndex; if (timeIntervalIndex === timeIntervalCollection.length) { // Date past range timeIntervalIndex = timeIntervalCollection.length - 1; timeInterval = timeIntervalCollection.get(timeIntervalIndex); date = timeInterval.stop; } else { // Date before range timeInterval = timeIntervalCollection.get(timeIntervalIndex); date = timeInterval.start; } } // De-lerp between the start and end of the interval const totalSeconds = JulianDate.secondsDifference( timeInterval.stop, timeInterval.start ); const secondsDifferenceStart = JulianDate.secondsDifference( date, timeInterval.start ); const t = secondsDifferenceStart / totalSeconds; return timeIntervalIndex + t; } /** * Update the clipping planes state and associated uniforms * * @param {VoxelPrimitive} primitive * @param {FrameState} frameState * @returns {boolean} Whether the clipping planes changed, requiring a shader rebuild * @private */ function updateClippingPlanes(primitive, frameState) { const clippingPlanes = primitive.clippingPlanes; if (!defined(clippingPlanes)) { return false; } clippingPlanes.update(frameState); const { clippingPlanesState, enabled } = clippingPlanes; if (enabled) { const uniforms = primitive._uniforms; uniforms.clippingPlanesTexture = clippingPlanes.texture; // Compute the clipping plane's transformation to uv space and then take the inverse // transpose to properly transform the hessian normal form of the plane. // transpose(inverse(worldToUv * clippingPlaneLocalToWorld)) // transpose(inverse(clippingPlaneLocalToWorld) * inverse(worldToUv)) // transpose(inverse(clippingPlaneLocalToWorld) * uvToWorld) uniforms.clippingPlanesMatrix = Matrix4.transpose( Matrix4.multiplyTransformation( Matrix4.inverse( clippingPlanes.modelMatrix, uniforms.clippingPlanesMatrix ), primitive._transformPositionUvToWorld, uniforms.clippingPlanesMatrix ), uniforms.clippingPlanesMatrix ); } if ( primitive._clippingPlanesState === clippingPlanesState && primitive._clippingPlanesEnabled === enabled ) { return false; } primitive._clippingPlanesState = clippingPlanesState; primitive._clippingPlanesEnabled = enabled; return true; } /** * Returns true if this object was destroyed; otherwise, false. *

* 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._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) { material.diffuse = vec3(1.0); material.alpha = 1.0; }`, }); function DefaultVoxelProvider() { this.ready = true; 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) ? defaultValue(options.tileLevel, 0) : 0; if (tileLevel >= 1) { return undefined; } return Promise.resolve([new Float32Array(1)]); }; VoxelPrimitive.DefaultProvider = new DefaultVoxelProvider(); export default VoxelPrimitive;