385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
import binarySearch from "../Core/binarySearch.js";
|
|
import Cartesian3 from "../Core/Cartesian3.js";
|
|
import CesiumMath from "../Core/Math.js";
|
|
import defined from "../Core/defined.js";
|
|
import DeveloperError from "../Core/DeveloperError.js";
|
|
import KeyframeNode from "./KeyframeNode.js";
|
|
import Matrix3 from "../Core/Matrix3.js";
|
|
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
|
|
|
|
/**
|
|
* @alias SpatialNode
|
|
* @constructor
|
|
*
|
|
* @param {number} level
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {number} z
|
|
* @param {SpatialNode} parent
|
|
* @param {VoxelShape} shape
|
|
* @param {Cartesian3} voxelDimensions
|
|
*
|
|
* @private
|
|
*/
|
|
function SpatialNode(level, x, y, z, parent, shape, voxelDimensions) {
|
|
/**
|
|
* @type {SpatialNode[]}
|
|
*/
|
|
this.children = undefined;
|
|
this.parent = parent;
|
|
|
|
this.level = level;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
|
|
/**
|
|
* @type {Cartesian3}
|
|
*/
|
|
this.dimensions = Cartesian3.clone(voxelDimensions);
|
|
/**
|
|
* @type {KeyframeNode[]}
|
|
*/
|
|
this.keyframeNodes = [];
|
|
/**
|
|
* @type {KeyframeNode[]}
|
|
*/
|
|
this.renderableKeyframeNodes = [];
|
|
|
|
this.renderableKeyframeNodeLerp = 0.0;
|
|
/**
|
|
* @type {KeyframeNode}
|
|
*/
|
|
this.renderableKeyframeNodePrevious = undefined;
|
|
/**
|
|
* @type {KeyframeNode}
|
|
*/
|
|
this.renderableKeyframeNodeNext = undefined;
|
|
|
|
this.orientedBoundingBox = new OrientedBoundingBox();
|
|
this.approximateVoxelSize = 0.0;
|
|
this.screenSpaceError = 0.0;
|
|
this.visitedFrameNumber = -1;
|
|
|
|
this.computeBoundingVolumes(shape);
|
|
}
|
|
|
|
const scratchObbHalfScale = new Cartesian3();
|
|
|
|
/**
|
|
* @param {VoxelShape} shape
|
|
*/
|
|
SpatialNode.prototype.computeBoundingVolumes = function (shape) {
|
|
this.orientedBoundingBox = shape.computeOrientedBoundingBoxForTile(
|
|
this.level,
|
|
this.x,
|
|
this.y,
|
|
this.z,
|
|
this.orientedBoundingBox
|
|
);
|
|
|
|
const halfScale = Matrix3.getScale(
|
|
this.orientedBoundingBox.halfAxes,
|
|
scratchObbHalfScale
|
|
);
|
|
const maximumScale = 2.0 * Cartesian3.maximumComponent(halfScale);
|
|
this.approximateVoxelSize =
|
|
maximumScale / Cartesian3.minimumComponent(this.dimensions);
|
|
};
|
|
|
|
/**
|
|
* @param {VoxelShape} shape The shape of the parent VoxelPrimitive
|
|
* @private
|
|
*/
|
|
SpatialNode.prototype.constructChildNodes = function (shape) {
|
|
const { level, x, y, z } = this;
|
|
const xMin = x * 2;
|
|
const yMin = y * 2;
|
|
const zMin = z * 2;
|
|
const yMax = yMin + 1;
|
|
const xMax = xMin + 1;
|
|
const zMax = zMin + 1;
|
|
const childLevel = level + 1;
|
|
|
|
const childCoords = [
|
|
[childLevel, xMin, yMin, zMin],
|
|
[childLevel, xMax, yMin, zMin],
|
|
[childLevel, xMin, yMax, zMin],
|
|
[childLevel, xMax, yMax, zMin],
|
|
[childLevel, xMin, yMin, zMax],
|
|
[childLevel, xMax, yMin, zMax],
|
|
[childLevel, xMin, yMax, zMax],
|
|
[childLevel, xMax, yMax, zMax],
|
|
];
|
|
|
|
this.children = childCoords.map(([level, x, y, z]) => {
|
|
return new SpatialNode(level, x, y, z, this, shape, this.dimensions);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {FrameState} frameState
|
|
* @param {number} visibilityPlaneMask
|
|
* @returns {number} A plane mask as described in {@link CullingVolume#computeVisibilityWithPlaneMask}.
|
|
*/
|
|
SpatialNode.prototype.visibility = function (frameState, visibilityPlaneMask) {
|
|
const obb = this.orientedBoundingBox;
|
|
const cullingVolume = frameState.cullingVolume;
|
|
return cullingVolume.computeVisibilityWithPlaneMask(obb, visibilityPlaneMask);
|
|
};
|
|
|
|
/**
|
|
* @param {Cartesian3} cameraPosition
|
|
* @param {number} screenSpaceErrorMultiplier
|
|
*/
|
|
SpatialNode.prototype.computeScreenSpaceError = function (
|
|
cameraPosition,
|
|
screenSpaceErrorMultiplier
|
|
) {
|
|
const obb = this.orientedBoundingBox;
|
|
|
|
let distance = Math.sqrt(obb.distanceSquaredTo(cameraPosition));
|
|
// Avoid divide-by-zero when viewer is inside the tile.
|
|
distance = Math.max(distance, CesiumMath.EPSILON7);
|
|
const approximateVoxelSize = this.approximateVoxelSize;
|
|
const error = screenSpaceErrorMultiplier * (approximateVoxelSize / distance);
|
|
this.screenSpaceError = error;
|
|
};
|
|
|
|
// This object imitates a KeyframeNode. Only used for binary search function.
|
|
const scratchBinarySearchKeyframeNode = {
|
|
keyframe: 0,
|
|
};
|
|
|
|
/**
|
|
* Find the index of a given key frame position within an array of KeyframeNodes,
|
|
* or the complement (~) of the index where it would be in the sorted array.
|
|
* @param {number} keyframe
|
|
* @param {KeyframeNode[]} keyframeNodes
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
function findKeyframeIndex(keyframe, keyframeNodes) {
|
|
scratchBinarySearchKeyframeNode.keyframe = keyframe;
|
|
return binarySearch(
|
|
keyframeNodes,
|
|
scratchBinarySearchKeyframeNode,
|
|
KeyframeNode.searchComparator
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Computes the most suitable keyframes for rendering, balancing between temporal and visual quality.
|
|
*
|
|
* @param {number} keyframeLocation
|
|
*/
|
|
SpatialNode.prototype.computeSurroundingRenderableKeyframeNodes = function (
|
|
keyframeLocation
|
|
) {
|
|
let spatialNode = this;
|
|
const startLevel = spatialNode.level;
|
|
|
|
const targetKeyframePrev = Math.floor(keyframeLocation);
|
|
const targetKeyframeNext = Math.ceil(keyframeLocation);
|
|
|
|
let bestKeyframeNodePrev;
|
|
let bestKeyframeNodeNext;
|
|
let minimumDistancePrev = +Number.MAX_VALUE;
|
|
let minimumDistanceNext = +Number.MAX_VALUE;
|
|
|
|
while (defined(spatialNode)) {
|
|
const { renderableKeyframeNodes } = spatialNode;
|
|
|
|
if (renderableKeyframeNodes.length >= 1) {
|
|
const indexPrev = getKeyframeIndexPrev(
|
|
targetKeyframePrev,
|
|
renderableKeyframeNodes
|
|
);
|
|
const keyframeNodePrev = renderableKeyframeNodes[indexPrev];
|
|
|
|
const indexNext =
|
|
targetKeyframeNext === targetKeyframePrev ||
|
|
targetKeyframePrev < keyframeNodePrev.keyframe
|
|
? indexPrev
|
|
: Math.min(indexPrev + 1, renderableKeyframeNodes.length - 1);
|
|
const keyframeNodeNext = renderableKeyframeNodes[indexNext];
|
|
|
|
const distancePrev = targetKeyframePrev - keyframeNodePrev.keyframe;
|
|
const weightedDistancePrev = getWeightedKeyframeDistance(
|
|
startLevel - spatialNode.level,
|
|
distancePrev
|
|
);
|
|
if (weightedDistancePrev < minimumDistancePrev) {
|
|
minimumDistancePrev = weightedDistancePrev;
|
|
bestKeyframeNodePrev = keyframeNodePrev;
|
|
}
|
|
|
|
const distanceNext = keyframeNodeNext.keyframe - targetKeyframeNext;
|
|
const weightedDistanceNext = getWeightedKeyframeDistance(
|
|
startLevel - spatialNode.level,
|
|
distanceNext
|
|
);
|
|
if (weightedDistanceNext < minimumDistanceNext) {
|
|
minimumDistanceNext = weightedDistanceNext;
|
|
bestKeyframeNodeNext = keyframeNodeNext;
|
|
}
|
|
|
|
if (distancePrev === 0 && distanceNext === 0) {
|
|
// Nothing higher up will be better, so break early.
|
|
break;
|
|
}
|
|
}
|
|
|
|
spatialNode = spatialNode.parent;
|
|
}
|
|
|
|
this.renderableKeyframeNodePrevious = bestKeyframeNodePrev;
|
|
this.renderableKeyframeNodeNext = bestKeyframeNodeNext;
|
|
|
|
if (!defined(bestKeyframeNodePrev) || !defined(bestKeyframeNodeNext)) {
|
|
return;
|
|
}
|
|
|
|
const bestKeyframePrev = bestKeyframeNodePrev.keyframe;
|
|
const bestKeyframeNext = bestKeyframeNodeNext.keyframe;
|
|
this.renderableKeyframeNodeLerp =
|
|
bestKeyframePrev === bestKeyframeNext
|
|
? 0.0
|
|
: CesiumMath.clamp(
|
|
(keyframeLocation - bestKeyframePrev) /
|
|
(bestKeyframeNext - bestKeyframePrev),
|
|
0.0,
|
|
1.0
|
|
);
|
|
};
|
|
|
|
function getKeyframeIndexPrev(targetKeyframe, keyframeNodes) {
|
|
const keyframeIndex = findKeyframeIndex(targetKeyframe, keyframeNodes);
|
|
return keyframeIndex < 0
|
|
? CesiumMath.clamp(~keyframeIndex - 1, 0, keyframeNodes.length - 1)
|
|
: keyframeIndex;
|
|
}
|
|
|
|
function getWeightedKeyframeDistance(levelDistance, keyframeDistance) {
|
|
// Balance quality between visual (levelDistance) and temporal (keyframeDistance)
|
|
const levelWeight = Math.exp(levelDistance * 4.0);
|
|
// Keyframes on the opposite of the desired direction are deprioritized.
|
|
const keyframeWeight = keyframeDistance >= 0 ? 1.0 : -200.0;
|
|
return levelDistance * levelWeight + keyframeDistance * keyframeWeight;
|
|
}
|
|
|
|
/**
|
|
* @param {number} frameNumber
|
|
* @returns {boolean}
|
|
*/
|
|
SpatialNode.prototype.isVisited = function (frameNumber) {
|
|
return this.visitedFrameNumber === frameNumber;
|
|
};
|
|
|
|
/**
|
|
* @param {number} keyframe
|
|
*/
|
|
SpatialNode.prototype.createKeyframeNode = function (keyframe) {
|
|
let index = findKeyframeIndex(keyframe, this.keyframeNodes);
|
|
if (index < 0) {
|
|
index = ~index; // convert to insertion index
|
|
const keyframeNode = new KeyframeNode(this, keyframe);
|
|
this.keyframeNodes.splice(index, 0, keyframeNode);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {KeyframeNode} keyframeNode
|
|
* @param {Megatexture[]} megatextures
|
|
*/
|
|
SpatialNode.prototype.destroyKeyframeNode = function (
|
|
keyframeNode,
|
|
megatextures
|
|
) {
|
|
const keyframe = keyframeNode.keyframe;
|
|
const keyframeIndex = findKeyframeIndex(keyframe, this.keyframeNodes);
|
|
if (keyframeIndex < 0) {
|
|
throw new DeveloperError("Keyframe node does not exist.");
|
|
}
|
|
|
|
this.keyframeNodes.splice(keyframeIndex, 1);
|
|
|
|
if (keyframeNode.megatextureIndex !== -1) {
|
|
for (let i = 0; i < megatextures.length; i++) {
|
|
megatextures[i].remove(keyframeNode.megatextureIndex);
|
|
}
|
|
|
|
const renderableKeyframeNodeIndex = findKeyframeIndex(
|
|
keyframe,
|
|
this.renderableKeyframeNodes
|
|
);
|
|
if (renderableKeyframeNodeIndex < 0) {
|
|
throw new DeveloperError("Renderable keyframe node does not exist.");
|
|
}
|
|
|
|
this.renderableKeyframeNodes.splice(renderableKeyframeNodeIndex, 1);
|
|
}
|
|
|
|
keyframeNode.spatialNode = undefined;
|
|
keyframeNode.state = KeyframeNode.LoadState.UNLOADED;
|
|
keyframeNode.metadata = {};
|
|
keyframeNode.megatextureIndex = -1;
|
|
keyframeNode.priority = -Number.MAX_VALUE;
|
|
keyframeNode.highPriorityFrameNumber = -1;
|
|
};
|
|
|
|
/**
|
|
* @param {KeyframeNode} keyframeNode
|
|
* @param {Megatexture[]} megatextures
|
|
*/
|
|
SpatialNode.prototype.addKeyframeNodeToMegatextures = function (
|
|
keyframeNode,
|
|
megatextures
|
|
) {
|
|
if (
|
|
keyframeNode.state !== KeyframeNode.LoadState.RECEIVED ||
|
|
keyframeNode.megatextureIndex !== -1 ||
|
|
keyframeNode.metadata.length !== megatextures.length
|
|
) {
|
|
throw new DeveloperError("Keyframe node cannot be added to megatexture");
|
|
}
|
|
|
|
for (let i = 0; i < megatextures.length; i++) {
|
|
const megatexture = megatextures[i];
|
|
keyframeNode.megatextureIndex = megatexture.add(keyframeNode.metadata[i]);
|
|
}
|
|
|
|
keyframeNode.state = KeyframeNode.LoadState.LOADED;
|
|
|
|
const renderableKeyframeNodes = this.renderableKeyframeNodes;
|
|
let renderableKeyframeNodeIndex = findKeyframeIndex(
|
|
keyframeNode.keyframe,
|
|
renderableKeyframeNodes
|
|
);
|
|
if (renderableKeyframeNodeIndex >= 0) {
|
|
throw new DeveloperError("Keyframe already renderable");
|
|
}
|
|
renderableKeyframeNodeIndex = ~renderableKeyframeNodeIndex;
|
|
renderableKeyframeNodes.splice(renderableKeyframeNodeIndex, 0, keyframeNode);
|
|
};
|
|
|
|
/**
|
|
* @param {number} frameNumber
|
|
* @returns {boolean}
|
|
*/
|
|
SpatialNode.prototype.isRenderable = function (frameNumber) {
|
|
const previousNode = this.renderableKeyframeNodePrevious;
|
|
const nextNode = this.renderableKeyframeNodeNext;
|
|
const level = this.level;
|
|
|
|
return (
|
|
defined(previousNode) &&
|
|
defined(nextNode) &&
|
|
(previousNode.spatialNode.level === level ||
|
|
nextNode.spatialNode.level === level) &&
|
|
this.visitedFrameNumber === frameNumber
|
|
);
|
|
};
|
|
|
|
export default SpatialNode;
|