1993 lines
54 KiB
JavaScript
1993 lines
54 KiB
JavaScript
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 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";
|
|
import Cartographic from "../Core/Cartographic.js";
|
|
import Ellipsoid from "../Core/Ellipsoid.js";
|
|
import VerticalExaggeration from "../Core/VerticalExaggeration.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();
|
|
|
|
/**
|
|
* Minimum bounds with vertical exaggeration applied
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._exaggeratedMinBounds = new Cartesian3();
|
|
|
|
/**
|
|
* Used to detect if the shape is dirty.
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._exaggeratedMinBoundsOld = new Cartesian3();
|
|
|
|
/**
|
|
* Maximum bounds with vertical exaggeration applied
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._exaggeratedMaxBounds = new Cartesian3();
|
|
|
|
/**
|
|
* Used to detect if the shape is dirty.
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._exaggeratedMaxBoundsOld = new Cartesian3();
|
|
|
|
/**
|
|
* This member is not known until the provider is ready.
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._minClippingBounds = new Cartesian3();
|
|
|
|
/**
|
|
* Used to detect if the clipping is dirty.
|
|
* This member is not known until the provider is ready.
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._minClippingBoundsOld = new Cartesian3();
|
|
|
|
/**
|
|
* This member is not known until the provider is ready.
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._maxClippingBounds = new Cartesian3();
|
|
|
|
/**
|
|
* Used to detect if the clipping is dirty.
|
|
* This member is not known until the provider is ready.
|
|
*
|
|
* @type {Cartesian3}
|
|
* @private
|
|
*/
|
|
this._maxClippingBoundsOld = new Cartesian3();
|
|
|
|
/**
|
|
* Clipping planes on the primitive
|
|
*
|
|
* @type {ClippingPlaneCollection}
|
|
* @private
|
|
*/
|
|
this._clippingPlanes = undefined;
|
|
|
|
/**
|
|
* Keeps track of when the clipping planes change
|
|
*
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this._clippingPlanesState = 0;
|
|
|
|
/**
|
|
* Keeps track of when the clipping planes are enabled / disabled
|
|
*
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._clippingPlanesEnabled = false;
|
|
|
|
/**
|
|
* The primitive's model matrix.
|
|
*
|
|
* @type {Matrix4}
|
|
* @private
|
|
*/
|
|
this._modelMatrix = Matrix4.clone(
|
|
defaultValue(options.modelMatrix, Matrix4.IDENTITY)
|
|
);
|
|
|
|
/**
|
|
* Model matrix with vertical exaggeration applied. Only used for BOX shape type.
|
|
*
|
|
* @type {Matrix4}
|
|
* @private
|
|
*/
|
|
this._exaggeratedModelMatrix = Matrix4.clone(this._modelMatrix);
|
|
|
|
/**
|
|
* The primitive's model matrix multiplied by the provider's model matrix.
|
|
* This member is not known until the provider is ready.
|
|
*
|
|
* @type {Matrix4}
|
|
* @private
|
|
*/
|
|
this._compoundModelMatrix = new Matrix4();
|
|
|
|
/**
|
|
* Used to detect if the shape is dirty.
|
|
* This member is not known until the provider is ready.
|
|
*
|
|
* @type {Matrix4}
|
|
* @private
|
|
*/
|
|
this._compoundModelMatrixOld = new Matrix4();
|
|
|
|
/**
|
|
* @type {CustomShader}
|
|
* @private
|
|
*/
|
|
this._customShader = 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();
|
|
|
|
// Rendering
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._jitter = false;
|
|
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._nearestSampling = false;
|
|
|
|
/**
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this._levelBlendFactor = 0.0;
|
|
|
|
/**
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this._stepSizeMultiplier = 1.0;
|
|
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._depthTest = true;
|
|
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._useLogDepth = undefined;
|
|
|
|
/**
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
this._screenSpaceError = 4.0; // in pixels
|
|
|
|
// Debug / statistics
|
|
/**
|
|
* @type {PolylineCollection}
|
|
* @private
|
|
*/
|
|
this._debugPolylines = new PolylineCollection();
|
|
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._debugDraw = false;
|
|
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._disableRender = false;
|
|
|
|
/**
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this._disableUpdate = false;
|
|
|
|
/**
|
|
* @type {Object<string, any>}
|
|
* @private
|
|
*/
|
|
this._uniforms = {
|
|
octreeInternalNodeTexture: undefined,
|
|
octreeInternalNodeTilesPerRow: 0,
|
|
octreeInternalNodeTexelSizeUv: new Cartesian2(),
|
|
octreeLeafNodeTexture: undefined,
|
|
octreeLeafNodeTilesPerRow: 0,
|
|
octreeLeafNodeTexelSizeUv: new Cartesian2(),
|
|
megatextureTextures: [],
|
|
megatextureSliceDimensions: new Cartesian2(),
|
|
megatextureTileDimensions: new Cartesian2(),
|
|
megatextureVoxelSizeUv: new Cartesian2(),
|
|
megatextureSliceSizeUv: new Cartesian2(),
|
|
megatextureTileSizeUv: new Cartesian2(),
|
|
dimensions: new Cartesian3(),
|
|
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<string, any>}
|
|
* @private
|
|
*/
|
|
this._shapeDefinesOld = {};
|
|
|
|
/**
|
|
* Map uniform names to functions that return the uniform values.
|
|
* @type {Object<string, function():any>}
|
|
* @private
|
|
*/
|
|
this._uniformMap = {};
|
|
|
|
const uniforms = this._uniforms;
|
|
const uniformMap = this._uniformMap;
|
|
for (const key in uniforms) {
|
|
if (uniforms.hasOwnProperty(key)) {
|
|
const name = `u_${key}`;
|
|
uniformMap[name] = function () {
|
|
return uniforms[key];
|
|
};
|
|
}
|
|
}
|
|
|
|
// If the provider fails to initialize the primitive will fail too.
|
|
const provider = this._provider;
|
|
initialize(this, provider);
|
|
}
|
|
|
|
function initialize(primitive, provider) {
|
|
// Set the bounds
|
|
const {
|
|
shape: shapeType,
|
|
minBounds = VoxelShapeType.getMinBounds(shapeType),
|
|
maxBounds = VoxelShapeType.getMaxBounds(shapeType),
|
|
} = provider;
|
|
|
|
primitive.minBounds = minBounds;
|
|
primitive.maxBounds = maxBounds;
|
|
primitive.minClippingBounds = VoxelShapeType.getMinBounds(shapeType);
|
|
primitive.maxClippingBounds = VoxelShapeType.getMaxBounds(shapeType);
|
|
|
|
// Initialize the exaggerated versions of bounds and model matrix
|
|
primitive._exaggeratedMinBounds = Cartesian3.clone(
|
|
primitive._minBounds,
|
|
primitive._exaggeratedMinBounds
|
|
);
|
|
primitive._exaggeratedMaxBounds = Cartesian3.clone(
|
|
primitive._maxBounds,
|
|
primitive._exaggeratedMaxBounds
|
|
);
|
|
primitive._exaggeratedModelMatrix = Matrix4.clone(
|
|
primitive._modelMatrix,
|
|
primitive._exaggeratedModelMatrix
|
|
);
|
|
|
|
checkTransformAndBounds(primitive, provider);
|
|
|
|
// Create the shape object, and update it so it is valid for VoxelTraversal
|
|
const ShapeConstructor = VoxelShapeType.getShapeConstructor(shapeType);
|
|
primitive._shape = new ShapeConstructor();
|
|
primitive._shapeVisible = updateShapeAndTransforms(
|
|
primitive,
|
|
primitive._shape,
|
|
provider
|
|
);
|
|
}
|
|
|
|
Object.defineProperties(VoxelPrimitive.prototype, {
|
|
/**
|
|
* Gets a value indicating whether or not the primitive is ready for use.
|
|
*
|
|
* @memberof VoxelPrimitive.prototype
|
|
* @type {boolean}
|
|
* @readonly
|
|
*/
|
|
ready: {
|
|
get: function () {
|
|
return this._ready;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Gets the {@link VoxelProvider} associated with this primitive.
|
|
*
|
|
* @memberof VoxelPrimitive.prototype
|
|
* @type {VoxelProvider}
|
|
* @readonly
|
|
*/
|
|
provider: {
|
|
get: function () {
|
|
return this._provider;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Gets the bounding sphere.
|
|
*
|
|
* @memberof VoxelPrimitive.prototype
|
|
* @type {BoundingSphere}
|
|
* @readonly
|
|
*/
|
|
boundingSphere: {
|
|
get: function () {
|
|
return this._shape.boundingSphere;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Gets the oriented bounding box.
|
|
*
|
|
* @memberof VoxelPrimitive.prototype
|
|
* @type {OrientedBoundingBox}
|
|
* @readonly
|
|
*/
|
|
orientedBoundingBox: {
|
|
get: function () {
|
|
return this._shape.orientedBoundingBox;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Gets the model matrix.
|
|
*
|
|
* @memberof VoxelPrimitive.prototype
|
|
* @type {Matrix4}
|
|
* @readonly
|
|
*/
|
|
modelMatrix: {
|
|
get: function () {
|
|
return this._modelMatrix;
|
|
},
|
|
set: function (modelMatrix) {
|
|
//>>includeStart('debug', pragmas.debug);
|
|
Check.typeOf.object("modelMatrix", modelMatrix);
|
|
//>>includeEnd('debug');
|
|
|
|
this._modelMatrix = Matrix4.clone(modelMatrix, this._modelMatrix);
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Gets the shape type.
|
|
*
|
|
* @memberof VoxelPrimitive.prototype
|
|
* @type {VoxelShapeType}
|
|
* @readonly
|
|
*/
|
|
shape: {
|
|
get: function () {
|
|
return this._provider.shape;
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Gets the 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);
|
|
|
|
// Initialize from the ready provider. This only happens once.
|
|
const context = frameState.context;
|
|
if (!this._ready) {
|
|
initFromProvider(this, provider, context);
|
|
// Set the primitive as ready after the first frame render since the user might set up events subscribed to
|
|
// the post render event, and the primitive may not be ready for those past the first frame.
|
|
frameState.afterRender.push(() => {
|
|
this._ready = true;
|
|
return true;
|
|
});
|
|
|
|
// Don't render until the next frame after ready is set to true
|
|
return;
|
|
}
|
|
|
|
updateVerticalExaggeration(this, frameState);
|
|
|
|
// Check if the shape is dirty before updating it. This needs to happen every
|
|
// frame because the member variables can be modified externally via the
|
|
// getters.
|
|
const shapeDirty = checkTransformAndBounds(this, provider);
|
|
const shape = this._shape;
|
|
if (shapeDirty) {
|
|
this._shapeVisible = updateShapeAndTransforms(this, shape, provider);
|
|
if (checkShapeDefines(this, shape)) {
|
|
this._shaderDirty = true;
|
|
}
|
|
}
|
|
if (!this._shapeVisible) {
|
|
return;
|
|
}
|
|
|
|
// Update the traversal and prepare for rendering.
|
|
const keyframeLocation = getKeyframeLocation(
|
|
provider.timeIntervalCollection,
|
|
this._clock
|
|
);
|
|
|
|
const traversal = this._traversal;
|
|
const sampleCountOld = traversal._sampleCount;
|
|
|
|
traversal.update(
|
|
frameState,
|
|
keyframeLocation,
|
|
shapeDirty, // recomputeBoundingVolumes
|
|
this._disableUpdate // pauseUpdate
|
|
);
|
|
|
|
if (sampleCountOld !== traversal._sampleCount) {
|
|
this._shaderDirty = true;
|
|
}
|
|
|
|
if (!traversal.isRenderable(traversal.rootNode)) {
|
|
return;
|
|
}
|
|
|
|
if (this._debugDraw) {
|
|
// Debug draw bounding boxes and other things. Must go after traversal update
|
|
// because that's what updates the tile bounding boxes.
|
|
debugDraw(this, frameState);
|
|
}
|
|
|
|
if (this._disableRender) {
|
|
return;
|
|
}
|
|
|
|
// Check if log depth changed
|
|
if (this._useLogDepth !== frameState.useLogDepth) {
|
|
this._useLogDepth = frameState.useLogDepth;
|
|
this._shaderDirty = true;
|
|
}
|
|
|
|
// Check if clipping planes changed
|
|
const clippingPlanesChanged = updateClippingPlanes(this, frameState);
|
|
if (clippingPlanesChanged) {
|
|
this._shaderDirty = true;
|
|
}
|
|
|
|
const leafNodeTexture = traversal.leafNodeTexture;
|
|
const uniforms = this._uniforms;
|
|
if (defined(leafNodeTexture)) {
|
|
uniforms.octreeLeafNodeTexture = traversal.leafNodeTexture;
|
|
uniforms.octreeLeafNodeTexelSizeUv = Cartesian2.clone(
|
|
traversal.leafNodeTexelSizeUv,
|
|
uniforms.octreeLeafNodeTexelSizeUv
|
|
);
|
|
uniforms.octreeLeafNodeTilesPerRow = traversal.leafNodeTilesPerRow;
|
|
}
|
|
|
|
// Rebuild shaders
|
|
if (this._shaderDirty) {
|
|
buildVoxelDrawCommands(this, context);
|
|
this._shaderDirty = false;
|
|
}
|
|
|
|
// Calculate the NDC-space AABB to "scissor" the fullscreen quad
|
|
const transformPositionWorldToProjection =
|
|
context.uniformState.viewProjection;
|
|
const orientedBoundingBox = shape.orientedBoundingBox;
|
|
const ndcAabb = orientedBoundingBoxToNdcAabb(
|
|
orientedBoundingBox,
|
|
transformPositionWorldToProjection,
|
|
scratchNdcAabb
|
|
);
|
|
|
|
// If the object is offscreen, don't render it.
|
|
const offscreen =
|
|
ndcAabb.x === +1.0 ||
|
|
ndcAabb.y === +1.0 ||
|
|
ndcAabb.z === -1.0 ||
|
|
ndcAabb.w === -1.0;
|
|
if (offscreen) {
|
|
return;
|
|
}
|
|
|
|
// Prepare to render: update uniforms that can change every frame
|
|
// Using a uniform instead of going through RenderState's scissor because the viewport is not accessible here, and the scissor command needs pixel coordinates.
|
|
uniforms.ndcSpaceAxisAlignedBoundingBox = Cartesian4.clone(
|
|
ndcAabb,
|
|
uniforms.ndcSpaceAxisAlignedBoundingBox
|
|
);
|
|
const transformPositionViewToWorld = context.uniformState.inverseView;
|
|
uniforms.transformPositionViewToUv = Matrix4.multiplyTransformation(
|
|
this._transformPositionWorldToUv,
|
|
transformPositionViewToWorld,
|
|
uniforms.transformPositionViewToUv
|
|
);
|
|
const transformPositionWorldToView = context.uniformState.view;
|
|
uniforms.transformPositionUvToView = Matrix4.multiplyTransformation(
|
|
transformPositionWorldToView,
|
|
this._transformPositionUvToWorld,
|
|
uniforms.transformPositionUvToView
|
|
);
|
|
const transformDirectionViewToWorld =
|
|
context.uniformState.inverseViewRotation;
|
|
uniforms.transformDirectionViewToLocal = Matrix3.multiply(
|
|
this._transformDirectionWorldToLocal,
|
|
transformDirectionViewToWorld,
|
|
uniforms.transformDirectionViewToLocal
|
|
);
|
|
uniforms.transformNormalLocalToWorld = Matrix3.clone(
|
|
this._transformNormalLocalToWorld,
|
|
uniforms.transformNormalLocalToWorld
|
|
);
|
|
const cameraPositionWorld = frameState.camera.positionWC;
|
|
uniforms.cameraPositionUv = Matrix4.multiplyByPoint(
|
|
this._transformPositionWorldToUv,
|
|
cameraPositionWorld,
|
|
uniforms.cameraPositionUv
|
|
);
|
|
uniforms.stepSize = this._stepSizeMultiplier;
|
|
|
|
// Render the primitive
|
|
const command = frameState.passes.pick
|
|
? this._drawCommandPick
|
|
: frameState.passes.pickVoxel
|
|
? this._drawCommandPickVoxel
|
|
: this._drawCommand;
|
|
command.boundingVolume = shape.boundingSphere;
|
|
frameState.commandList.push(command);
|
|
};
|
|
|
|
const scratchExaggerationScale = new Cartesian3();
|
|
const scratchExaggerationCenter = new Cartesian3();
|
|
const scratchCartographicCenter = new Cartographic();
|
|
const scratchExaggerationTranslation = new Cartesian3();
|
|
|
|
/**
|
|
* Update the exaggerated bounds of a primitive to account for vertical exaggeration
|
|
* Currently only applies to Ellipsoid shape type
|
|
* @param {VoxelPrimitive} primitive
|
|
* @param {FrameState} frameState
|
|
* @private
|
|
*/
|
|
function updateVerticalExaggeration(primitive, frameState) {
|
|
primitive._exaggeratedMinBounds = Cartesian3.clone(
|
|
primitive._minBounds,
|
|
primitive._exaggeratedMinBounds
|
|
);
|
|
primitive._exaggeratedMaxBounds = Cartesian3.clone(
|
|
primitive._maxBounds,
|
|
primitive._exaggeratedMaxBounds
|
|
);
|
|
|
|
if (primitive.shape === VoxelShapeType.ELLIPSOID) {
|
|
// Apply the exaggeration by stretching the height bounds
|
|
const relativeHeight = frameState.verticalExaggerationRelativeHeight;
|
|
const exaggeration = frameState.verticalExaggeration;
|
|
primitive._exaggeratedMinBounds.z =
|
|
(primitive._minBounds.z - relativeHeight) * exaggeration + relativeHeight;
|
|
primitive._exaggeratedMaxBounds.z =
|
|
(primitive._maxBounds.z - relativeHeight) * exaggeration + relativeHeight;
|
|
} else if (primitive.shape === VoxelShapeType.BOX) {
|
|
// Apply the exaggeration via the model matrix
|
|
const exaggerationScale = Cartesian3.fromElements(
|
|
1.0,
|
|
1.0,
|
|
frameState.verticalExaggeration,
|
|
scratchExaggerationScale
|
|
);
|
|
primitive._exaggeratedModelMatrix = Matrix4.multiplyByScale(
|
|
primitive._modelMatrix,
|
|
exaggerationScale,
|
|
primitive._exaggeratedModelMatrix
|
|
);
|
|
primitive._exaggeratedModelMatrix = Matrix4.multiplyByTranslation(
|
|
primitive._exaggeratedModelMatrix,
|
|
computeBoxExaggerationTranslation(primitive, frameState),
|
|
primitive._exaggeratedModelMatrix
|
|
);
|
|
}
|
|
}
|
|
|
|
function computeBoxExaggerationTranslation(primitive, frameState) {
|
|
// Compute translation based on box center, relative height, and exaggeration
|
|
const {
|
|
shapeTransform = Matrix4.IDENTITY,
|
|
globalTransform = Matrix4.IDENTITY,
|
|
} = primitive._provider;
|
|
|
|
// Find the Cartesian position of the center of the OBB
|
|
const initialCenter = Matrix4.getTranslation(
|
|
shapeTransform,
|
|
scratchExaggerationCenter
|
|
);
|
|
const intermediateCenter = Matrix4.multiplyByPoint(
|
|
primitive._modelMatrix,
|
|
initialCenter,
|
|
scratchExaggerationCenter
|
|
);
|
|
const transformedCenter = Matrix4.multiplyByPoint(
|
|
globalTransform,
|
|
intermediateCenter,
|
|
scratchExaggerationCenter
|
|
);
|
|
|
|
// Find the cartographic height
|
|
const ellipsoid = Ellipsoid.WGS84;
|
|
const centerCartographic = ellipsoid.cartesianToCartographic(
|
|
transformedCenter,
|
|
scratchCartographicCenter
|
|
);
|
|
|
|
let centerHeight = 0.0;
|
|
if (defined(centerCartographic)) {
|
|
centerHeight = centerCartographic.height;
|
|
}
|
|
|
|
// Find the shift that will put the center in the right position relative
|
|
// to relativeHeight, after it is scaled by verticalExaggeration
|
|
const exaggeratedHeight = VerticalExaggeration.getHeight(
|
|
centerHeight,
|
|
frameState.verticalExaggeration,
|
|
frameState.verticalExaggerationRelativeHeight
|
|
);
|
|
|
|
return Cartesian3.fromElements(
|
|
0.0,
|
|
0.0,
|
|
(exaggeratedHeight - centerHeight) / frameState.verticalExaggeration,
|
|
scratchExaggerationTranslation
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize primitive properties that are derived from the voxel provider
|
|
* @param {VoxelPrimitive} primitive
|
|
* @param {VoxelProvider} provider
|
|
* @param {Context} context
|
|
* @private
|
|
*/
|
|
function initFromProvider(primitive, provider, context) {
|
|
const uniforms = primitive._uniforms;
|
|
|
|
primitive._pickId = context.createPickId({ primitive });
|
|
uniforms.pickColor = Color.clone(primitive._pickId.color, uniforms.pickColor);
|
|
|
|
const { shaderDefines, shaderUniforms: shapeUniforms } = primitive._shape;
|
|
primitive._shapeDefinesOld = clone(shaderDefines, true);
|
|
|
|
// Add shape uniforms to the uniform map
|
|
const uniformMap = primitive._uniformMap;
|
|
for (const key in shapeUniforms) {
|
|
if (shapeUniforms.hasOwnProperty(key)) {
|
|
const name = `u_${key}`;
|
|
|
|
//>>includeStart('debug', pragmas.debug);
|
|
if (defined(uniformMap[name])) {
|
|
oneTimeWarning(
|
|
`VoxelPrimitive: Uniform name "${name}" is already defined`
|
|
);
|
|
}
|
|
//>>includeEnd('debug');
|
|
|
|
uniformMap[name] = function () {
|
|
return shapeUniforms[key];
|
|
};
|
|
}
|
|
}
|
|
|
|
// Set uniforms that come from the provider.
|
|
// Note that minBounds and maxBounds can be set dynamically, so their uniforms aren't set here.
|
|
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._exaggeratedModelMatrix,
|
|
primitive._compoundModelMatrix
|
|
);
|
|
Matrix4.multiplyTransformation(
|
|
primitive._compoundModelMatrix,
|
|
shapeTransform,
|
|
primitive._compoundModelMatrix
|
|
);
|
|
const numChanges =
|
|
updateBound(primitive, "_compoundModelMatrix", "_compoundModelMatrixOld") +
|
|
updateBound(primitive, "_minBounds", "_minBoundsOld") +
|
|
updateBound(primitive, "_maxBounds", "_maxBoundsOld") +
|
|
updateBound(
|
|
primitive,
|
|
"_exaggeratedMinBounds",
|
|
"_exaggeratedMinBoundsOld"
|
|
) +
|
|
updateBound(
|
|
primitive,
|
|
"_exaggeratedMaxBounds",
|
|
"_exaggeratedMaxBoundsOld"
|
|
) +
|
|
updateBound(primitive, "_minClippingBounds", "_minClippingBoundsOld") +
|
|
updateBound(primitive, "_maxClippingBounds", "_maxClippingBoundsOld");
|
|
return numChanges > 0;
|
|
}
|
|
|
|
/**
|
|
* Compare old and new values of a bound and update the old if it is different.
|
|
* @param {VoxelPrimitive} primitive The primitive with bounds properties
|
|
* @param {string} newBoundKey A key pointing to a bounds property of type Cartesian3 or Matrix4
|
|
* @param {string} oldBoundKey A key pointing to a bounds property of the same type as the property at newBoundKey
|
|
* @returns {number} 1 if the bound value changed, 0 otherwise
|
|
*
|
|
* @private
|
|
*/
|
|
function updateBound(primitive, newBoundKey, oldBoundKey) {
|
|
const newBound = primitive[newBoundKey];
|
|
const oldBound = primitive[oldBoundKey];
|
|
|
|
const changed = !newBound.equals(oldBound);
|
|
if (changed) {
|
|
newBound.clone(oldBound);
|
|
}
|
|
return changed ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Update the shape and related transforms
|
|
* @param {VoxelPrimitive} primitive
|
|
* @param {VoxelShape} shape
|
|
* @param {VoxelProvider} provider
|
|
* @returns {boolean} True if the shape is visible
|
|
* @private
|
|
*/
|
|
function updateShapeAndTransforms(primitive, shape, provider) {
|
|
const visible = shape.update(
|
|
primitive._compoundModelMatrix,
|
|
primitive._exaggeratedMinBounds,
|
|
primitive._exaggeratedMaxBounds,
|
|
primitive.minClippingBounds,
|
|
primitive.maxClippingBounds
|
|
);
|
|
if (!visible) {
|
|
return false;
|
|
}
|
|
|
|
const transformPositionLocalToWorld = shape.shapeTransform;
|
|
const transformPositionWorldToLocal = Matrix4.inverse(
|
|
transformPositionLocalToWorld,
|
|
scratchTransformPositionWorldToLocal
|
|
);
|
|
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
|
|
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.
|
|
* <br /><br />
|
|
* If this object was destroyed, it should not be used; calling any function other than
|
|
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
|
|
*
|
|
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
|
|
*
|
|
* @see VoxelPrimitive#destroy
|
|
*/
|
|
VoxelPrimitive.prototype.isDestroyed = function () {
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
|
|
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
|
|
* <br /><br />
|
|
* Once an object is destroyed, it should not be used; calling any function other than
|
|
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
|
|
* assign the return value (<code>undefined</code>) 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;
|