@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
596 lines (535 loc) • 17 kB
JavaScript
import BoundingSphere from "../Core/BoundingSphere.js";
import Cartesian3 from "../Core/Cartesian3.js";
import CesiumMath from "../Core/Math.js";
import Check from "../Core/Check.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import VoxelBoundsCollection from "./VoxelBoundsCollection.js";
import ClippingPlane from "./ClippingPlane.js";
/**
* A box {@link VoxelShape}.
*
* @alias VoxelBoxShape
* @constructor
*
* @see VoxelShape
* @see VoxelEllipsoidShape
* @see VoxelCylinderShape
* @see VoxelShapeType
*
* @private
*/
function VoxelBoxShape() {
this._orientedBoundingBox = new OrientedBoundingBox();
this._boundingSphere = new BoundingSphere();
this._boundTransform = new Matrix4();
this._shapeTransform = new Matrix4();
/**
* The minimum bounds of the shape.
* @type {Cartesian3}
* @private
*/
this._minBounds = VoxelBoxShape.DefaultMinBounds.clone();
/**
* The maximum bounds of the shape.
* @type {Cartesian3}
* @private
*/
this._maxBounds = VoxelBoxShape.DefaultMaxBounds.clone();
/**
* The minimum render bounds of the shape.
* @type {Cartesian3}
* @private
*/
this._renderMinBounds = VoxelBoxShape.DefaultMinBounds.clone();
/**
* The maximum render bounds of the shape.
* @type {Cartesian3}
* @private
*/
this._renderMaxBounds = VoxelBoxShape.DefaultMaxBounds.clone();
const { DefaultMinBounds, DefaultMaxBounds } = VoxelBoxShape;
const boundPlanes = [
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3()),
DefaultMinBounds.x,
),
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_Y, new Cartesian3()),
DefaultMinBounds.y,
),
new ClippingPlane(
Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()),
DefaultMinBounds.z,
),
new ClippingPlane(Cartesian3.UNIT_X, -DefaultMaxBounds.x),
new ClippingPlane(Cartesian3.UNIT_Y, -DefaultMaxBounds.y),
new ClippingPlane(Cartesian3.UNIT_Z, -DefaultMaxBounds.z),
];
this._renderBoundPlanes = new VoxelBoundsCollection({ planes: boundPlanes });
this._shaderUniforms = {
boxEcToXyz: new Matrix3(),
boxLocalToShapeUvScale: new Cartesian3(),
boxLocalToShapeUvTranslate: new Cartesian3(),
};
this._shaderDefines = {
BOX_INTERSECTION_INDEX: undefined,
};
this._shaderMaximumIntersectionsLength = 0; // not known until update
}
Object.defineProperties(VoxelBoxShape.prototype, {
/**
* An oriented bounding box containing the bounded shape.
*
* @memberof VoxelBoxShape.prototype
* @type {OrientedBoundingBox}
* @readonly
* @private
*/
orientedBoundingBox: {
get: function () {
return this._orientedBoundingBox;
},
},
/**
* A collection of planes used for the render bounds
* @memberof VoxelBoxShape.prototype
* @type {VoxelBoundsCollection}
* @readonly
* @private
*/
renderBoundPlanes: {
get: function () {
return this._renderBoundPlanes;
},
},
/**
* A bounding sphere containing the bounded shape.
*
* @memberof VoxelBoxShape.prototype
* @type {BoundingSphere}
* @readonly
* @private
*/
boundingSphere: {
get: function () {
return this._boundingSphere;
},
},
/**
* A transformation matrix containing the bounded shape.
*
* @memberof VoxelBoxShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
boundTransform: {
get: function () {
return this._boundTransform;
},
},
/**
* A transformation matrix containing the shape, ignoring the bounds.
*
* @memberof VoxelBoxShape.prototype
* @type {Matrix4}
* @readonly
* @private
*/
shapeTransform: {
get: function () {
return this._shapeTransform;
},
},
/**
* @memberof VoxelBoxShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderUniforms: {
get: function () {
return this._shaderUniforms;
},
},
/**
* @memberof VoxelBoxShape.prototype
* @type {Object<string, any>}
* @readonly
* @private
*/
shaderDefines: {
get: function () {
return this._shaderDefines;
},
},
/**
* The maximum number of intersections against the shape for any ray direction.
* @memberof VoxelBoxShape.prototype
* @type {number}
* @readonly
* @private
*/
shaderMaximumIntersectionsLength: {
get: function () {
return this._shaderMaximumIntersectionsLength;
},
},
});
const scratchCenter = new Cartesian3();
const scratchScale = new Cartesian3();
const scratchRotation = new Matrix3();
const scratchClipMinBounds = new Cartesian3();
const scratchClipMaxBounds = new Cartesian3();
/**
* Update the shape's state.
* @private
* @param {Matrix4} modelMatrix The model matrix.
* @param {Cartesian3} minBounds The minimum bounds.
* @param {Cartesian3} maxBounds The maximum bounds.
* @param {Cartesian3} [clipMinBounds] The minimum clip bounds.
* @param {Cartesian3} [clipMaxBounds] The maximum clip bounds.
* @returns {boolean} Whether the shape is visible.
*/
VoxelBoxShape.prototype.update = function (
modelMatrix,
minBounds,
maxBounds,
clipMinBounds,
clipMaxBounds,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("modelMatrix", modelMatrix);
Check.typeOf.object("minBounds", minBounds);
Check.typeOf.object("maxBounds", maxBounds);
//>>includeEnd('debug');
clipMinBounds = clipMinBounds ?? minBounds.clone(scratchClipMinBounds);
clipMaxBounds = clipMaxBounds ?? maxBounds.clone(scratchClipMaxBounds);
minBounds = Cartesian3.clone(minBounds, this._minBounds);
maxBounds = Cartesian3.clone(maxBounds, this._maxBounds);
const renderMinBounds = Cartesian3.clamp(
minBounds,
clipMinBounds,
clipMaxBounds,
this._renderMinBounds,
);
const renderMaxBounds = Cartesian3.clamp(
maxBounds,
clipMinBounds,
clipMaxBounds,
this._renderMaxBounds,
);
// Box is not visible if:
// - any of the min render bounds exceed the max render bounds
// - two or more of the min bounds equal the max bounds (line / point)
// - scale is 0 for any component (too annoying to reconstruct rotation matrix)
const scale = Matrix4.getScale(modelMatrix, scratchScale);
if (
renderMinBounds.x > renderMaxBounds.x ||
renderMinBounds.y > renderMaxBounds.y ||
renderMinBounds.z > renderMaxBounds.z ||
(renderMinBounds.x === renderMaxBounds.x) +
(renderMinBounds.y === renderMaxBounds.y) +
(renderMinBounds.z === renderMaxBounds.z) >=
2 ||
scale.x === 0.0 ||
scale.y === 0.0 ||
scale.z === 0.0
) {
return false;
}
// Update the render bounds planes
const renderBoundPlanes = this._renderBoundPlanes;
renderBoundPlanes.get(0).distance = renderMinBounds.x;
renderBoundPlanes.get(1).distance = renderMinBounds.y;
renderBoundPlanes.get(2).distance = renderMinBounds.z;
renderBoundPlanes.get(3).distance = -renderMaxBounds.x;
renderBoundPlanes.get(4).distance = -renderMaxBounds.y;
renderBoundPlanes.get(5).distance = -renderMaxBounds.z;
this._shapeTransform = Matrix4.clone(modelMatrix, this._shapeTransform);
this._orientedBoundingBox = getBoxChunkObb(
renderMinBounds,
renderMaxBounds,
this._shapeTransform,
this._orientedBoundingBox,
);
// All of the box bounds go from -1 to +1, so the model matrix scale can be
// used as the oriented bounding box half axes.
this._boundTransform = Matrix4.fromRotationTranslation(
this._orientedBoundingBox.halfAxes,
this._orientedBoundingBox.center,
this._boundTransform,
);
this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(
this._orientedBoundingBox,
this._boundingSphere,
);
const shaderUniforms = this._shaderUniforms;
const shaderDefines = this._shaderDefines;
// To keep things simple, clear the defines every time
for (const key in shaderDefines) {
if (shaderDefines.hasOwnProperty(key)) {
shaderDefines[key] = undefined;
}
}
// Keep track of how many intersections there are going to be.
let intersectionCount = 0;
shaderDefines["BOX_INTERSECTION_INDEX"] = intersectionCount;
intersectionCount += 1;
// Compute scale and translation to transform from UV space to bounded UV space
const min = minBounds;
const max = maxBounds;
const boxLocalToShapeUvScale = Cartesian3.fromElements(
boundScale(min.x, max.x),
boundScale(min.y, max.y),
boundScale(min.z, max.z),
shaderUniforms.boxLocalToShapeUvScale,
);
shaderUniforms.boxLocalToShapeUvTranslate = Cartesian3.negate(
Cartesian3.multiplyComponents(
boxLocalToShapeUvScale,
min,
shaderUniforms.boxLocalToShapeUvTranslate,
),
shaderUniforms.boxLocalToShapeUvTranslate,
);
this._shaderMaximumIntersectionsLength = intersectionCount;
return true;
};
function boundScale(minBound, maxBound) {
return CesiumMath.equalsEpsilon(minBound, maxBound, CesiumMath.EPSILON7)
? 1.0
: 1.0 / (maxBound - minBound);
}
const scratchTransformPositionWorldToLocal = new Matrix4();
/**
* Update any view-dependent transforms.
* @private
* @param {FrameState} frameState The frame state.
*/
VoxelBoxShape.prototype.updateViewTransforms = function (frameState) {
const shaderUniforms = this._shaderUniforms;
const transformPositionWorldToLocal = Matrix4.inverse(
this._shapeTransform,
scratchTransformPositionWorldToLocal,
);
const transformDirectionWorldToLocal = Matrix4.getMatrix3(
transformPositionWorldToLocal,
shaderUniforms.boxEcToXyz,
);
const rotateViewToWorld = frameState.context.uniformState.inverseViewRotation;
Matrix3.multiply(
transformDirectionWorldToLocal,
rotateViewToWorld,
shaderUniforms.boxEcToXyz,
);
};
/**
* Convert a local coordinate to the shape's UV space.
* @private
* @param {Cartesian3} positionLocal The local coordinate to convert.
* @param {Cartesian3} result The Cartesian3 to store the result in.
* @returns {Cartesian3} The converted UV coordinate.
*/
VoxelBoxShape.prototype.convertLocalToShapeUvSpace = function (
positionLocal,
result,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("positionLocal", positionLocal);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
const { boxLocalToShapeUvScale, boxLocalToShapeUvTranslate } =
this._shaderUniforms;
return Cartesian3.add(
Cartesian3.multiplyComponents(
positionLocal,
boxLocalToShapeUvScale,
result,
),
boxLocalToShapeUvTranslate,
result,
);
};
const scratchTileMinBounds = new Cartesian3();
const scratchTileMaxBounds = new Cartesian3();
/**
* Computes an oriented bounding box for a specified tile.
* @private
* @param {number} tileLevel The tile's level.
* @param {number} tileX The tile's x coordinate.
* @param {number} tileY The tile's y coordinate.
* @param {number} tileZ The tile's z coordinate.
* @param {OrientedBoundingBox} result The oriented bounding box that will be set to enclose the specified tile
* @returns {OrientedBoundingBox} The oriented bounding box.
*/
VoxelBoxShape.prototype.computeOrientedBoundingBoxForTile = function (
tileLevel,
tileX,
tileY,
tileZ,
result,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.number("tileLevel", tileLevel);
Check.typeOf.number("tileX", tileX);
Check.typeOf.number("tileY", tileY);
Check.typeOf.number("tileZ", tileZ);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
const minBounds = this._minBounds;
const maxBounds = this._maxBounds;
const sizeAtLevel = 1.0 / Math.pow(2, tileLevel);
const tileMinBounds = Cartesian3.fromElements(
CesiumMath.lerp(minBounds.x, maxBounds.x, sizeAtLevel * tileX),
CesiumMath.lerp(minBounds.y, maxBounds.y, sizeAtLevel * tileY),
CesiumMath.lerp(minBounds.z, maxBounds.z, sizeAtLevel * tileZ),
scratchTileMinBounds,
);
const tileMaxBounds = Cartesian3.fromElements(
CesiumMath.lerp(minBounds.x, maxBounds.x, sizeAtLevel * (tileX + 1)),
CesiumMath.lerp(minBounds.y, maxBounds.y, sizeAtLevel * (tileY + 1)),
CesiumMath.lerp(minBounds.z, maxBounds.z, sizeAtLevel * (tileZ + 1)),
scratchTileMaxBounds,
);
return getBoxChunkObb(
tileMinBounds,
tileMaxBounds,
this._shapeTransform,
result,
);
};
const sampleSizeScratch = new Cartesian3();
/**
* Computes an oriented bounding box for a specified sample within a specified tile.
* @private
* @param {SpatialNode} spatialNode The spatial node containing the sample
* @param {Cartesian3} tileDimensions The size of the tile in number of samples, before padding
* @param {Cartesian3} tileUv The sample coordinate within the tile
* @param {OrientedBoundingBox} result The oriented bounding box that will be set to enclose the specified sample
* @returns {OrientedBoundingBox} The oriented bounding box.
*/
VoxelBoxShape.prototype.computeOrientedBoundingBoxForSample = function (
spatialNode,
tileDimensions,
tileUv,
result,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("spatialNode", spatialNode);
Check.typeOf.object("tileDimensions", tileDimensions);
Check.typeOf.object("tileUv", tileUv);
Check.typeOf.object("result", result);
//>>includeEnd('debug');
const tileSizeAtLevel = 1.0 / Math.pow(2, spatialNode.level);
const sampleSize = Cartesian3.divideComponents(
Cartesian3.ONE,
tileDimensions,
sampleSizeScratch,
);
const sampleSizeAtLevel = Cartesian3.multiplyByScalar(
sampleSize,
tileSizeAtLevel,
sampleSizeScratch,
);
const minLerp = Cartesian3.multiplyByScalar(
Cartesian3.fromElements(
spatialNode.x + tileUv.x,
spatialNode.y + tileUv.y,
spatialNode.z + tileUv.z,
scratchTileMinBounds,
),
tileSizeAtLevel,
scratchTileMinBounds,
);
const maxLerp = Cartesian3.add(
minLerp,
sampleSizeAtLevel,
scratchTileMaxBounds,
);
const minBounds = this._minBounds;
const maxBounds = this._maxBounds;
const sampleMinBounds = Cartesian3.fromElements(
CesiumMath.lerp(minBounds.x, maxBounds.x, minLerp.x),
CesiumMath.lerp(minBounds.y, maxBounds.y, minLerp.y),
CesiumMath.lerp(minBounds.z, maxBounds.z, minLerp.z),
scratchTileMinBounds,
);
const sampleMaxBounds = Cartesian3.fromElements(
CesiumMath.lerp(minBounds.x, maxBounds.x, maxLerp.x),
CesiumMath.lerp(minBounds.y, maxBounds.y, maxLerp.y),
CesiumMath.lerp(minBounds.z, maxBounds.z, maxLerp.z),
scratchTileMaxBounds,
);
return getBoxChunkObb(
sampleMinBounds,
sampleMaxBounds,
this._shapeTransform,
result,
);
};
/**
* Defines the minimum bounds of the shape. Corresponds to minimum X, Y, Z.
* @private
* @type {Cartesian3}
* @constant
* @readonly
*/
VoxelBoxShape.DefaultMinBounds = Object.freeze(
new Cartesian3(-1.0, -1.0, -1.0),
);
/**
* Defines the maximum bounds of the shape. Corresponds to maximum X, Y, Z.
* @private
* @type {Cartesian3}
* @constant
* @readonly
*/
VoxelBoxShape.DefaultMaxBounds = Object.freeze(
new Cartesian3(+1.0, +1.0, +1.0),
);
const scratchBoxScale = new Cartesian3();
/**
* Computes an {@link OrientedBoundingBox} for a subregion of the shape.
*
* @function
*
* @param {Cartesian3} minimumBounds The minimum bounds, in the local coordinates of the shape.
* @param {Cartesian3} maximumBounds The maximum bounds, in the local coordinates of the shape.
* @param {Matrix4} matrix The matrix to transform the points.
* @param {OrientedBoundingBox} result The object onto which to store the result.
* @returns {OrientedBoundingBox} The oriented bounding box that contains this subregion.
*
* @private
*/
function getBoxChunkObb(minimumBounds, maximumBounds, matrix, result) {
const defaultMinBounds = VoxelBoxShape.DefaultMinBounds;
const defaultMaxBounds = VoxelBoxShape.DefaultMaxBounds;
const isDefaultBounds =
Cartesian3.equals(minimumBounds, defaultMinBounds) &&
Cartesian3.equals(maximumBounds, defaultMaxBounds);
if (isDefaultBounds) {
result.center = Matrix4.getTranslation(matrix, result.center);
result.halfAxes = Matrix4.getMatrix3(matrix, result.halfAxes);
} else {
let scale = Matrix4.getScale(matrix, scratchBoxScale);
const localCenter = Cartesian3.midpoint(
minimumBounds,
maximumBounds,
scratchCenter,
);
result.center = Matrix4.multiplyByPoint(matrix, localCenter, result.center);
scale = Cartesian3.fromElements(
scale.x * 0.5 * (maximumBounds.x - minimumBounds.x),
scale.y * 0.5 * (maximumBounds.y - minimumBounds.y),
scale.z * 0.5 * (maximumBounds.z - minimumBounds.z),
scratchBoxScale,
);
const rotation = Matrix4.getRotation(matrix, scratchRotation);
result.halfAxes = Matrix3.setScale(rotation, scale, result.halfAxes);
}
return result;
}
export default VoxelBoxShape;