import Cartesian2 from "../Core/Cartesian2.js";
import CesiumMath from "../Core/Math.js";
import CullingVolume from "../Core/CullingVolume.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DoubleEndedPriorityQueue from "../Core/DoubleEndedPriorityQueue.js";
import getTimestamp from "../Core/getTimestamp.js";
import KeyframeNode from "./KeyframeNode.js";
import MetadataType from "./MetadataType.js";
import Megatexture from "./Megatexture.js";
import PixelFormat from "../Core/PixelFormat.js";
import PixelDatatype from "../Renderer/PixelDatatype.js";
import Sampler from "../Renderer/Sampler.js";
import SpatialNode from "./SpatialNode.js";
import Texture from "../Renderer/Texture.js";
import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";
import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";
/**
* Handles tileset traversal, tile requests, and GPU resources. Intended to be
* private and paired with a {@link VoxelPrimitive}, which has a user-facing API.
*
* @alias VoxelTraversal
* @constructor
*
* @param {VoxelPrimitive} primitive
* @param {Context} context
* @param {Cartesian3} dimensions
* @param {MetadataType[]} types
* @param {MetadataComponentType[]} componentTypes
* @param {number} keyframeCount
* @param {number} [maximumTextureMemoryByteLength]
*
* @private
*/
function VoxelTraversal(
primitive,
context,
dimensions,
types,
componentTypes,
keyframeCount,
maximumTextureMemoryByteLength
) {
/**
* TODO: maybe this shouldn't be stored or passed into update function?
* @type {VoxelPrimitive}
* @private
*/
this._primitive = primitive;
const length = types.length;
/**
* @type {Megatexture[]}
* @readonly
*/
this.megatextures = new Array(length);
// TODO make sure to split the maximumTextureMemoryByteLength across all the megatextures
for (let i = 0; i < length; i++) {
const type = types[i];
const componentCount = MetadataType.getComponentCount(type);
const componentType = componentTypes[i];
this.megatextures[i] = new Megatexture(
context,
dimensions,
componentCount,
componentType,
maximumTextureMemoryByteLength
);
}
const maximumTileCount = this.megatextures[0].maximumTileCount;
/**
* @type {number}
* @private
*/
this._simultaneousRequestCount = 0;
/**
* @type {boolean}
* @private
*/
this._debugPrint = false;
/**
* @type {number}
* @private
*/
this._frameNumber = 0;
const shape = primitive._shape;
/**
* @type {SpatialNode}
* @readonly
*/
this.rootNode = new SpatialNode(0, 0, 0, 0, undefined, shape, dimensions);
/**
* @type {DoubleEndedPriorityQueue}
* @private
*/
this._priorityQueue = new DoubleEndedPriorityQueue({
maximumLength: maximumTileCount,
comparator: KeyframeNode.priorityComparator,
});
/**
* @type {KeyframeNode[]}
* @private
*/
this._highPriorityKeyframeNodes = new Array(maximumTileCount);
/**
* @type {KeyframeNode[]}
* @private
*/
this._keyframeNodesInMegatexture = new Array(maximumTileCount);
/**
* @type {number}
* @private
*/
this._keyframeCount = keyframeCount;
/**
* @type {number}
* @private
*/
this._sampleCount = undefined;
/**
* @type {number}
* @private
*/
this._keyframeLocation = 0;
/**
* @type {number[]}
* @private
*/
this._binaryTreeKeyframeWeighting = new Array(keyframeCount);
const binaryTreeKeyframeWeighting = this._binaryTreeKeyframeWeighting;
binaryTreeKeyframeWeighting[0] = 0;
binaryTreeKeyframeWeighting[keyframeCount - 1] = 0;
binaryTreeWeightingRecursive(
binaryTreeKeyframeWeighting,
1,
keyframeCount - 2,
0
);
const internalNodeTexelCount = 9;
const internalNodeTextureDimensionX = 2048;
const internalNodeTilesPerRow = Math.floor(
internalNodeTextureDimensionX / internalNodeTexelCount
);
const internalNodeTextureDimensionY = Math.ceil(
maximumTileCount / internalNodeTilesPerRow
);
/**
* @type {Texture}
* @readonly
*/
this.internalNodeTexture = new Texture({
context: context,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
flipY: false,
width: internalNodeTextureDimensionX,
height: internalNodeTextureDimensionY,
sampler: new Sampler({
minificationFilter: TextureMinificationFilter.NEAREST,
magnificationFilter: TextureMagnificationFilter.NEAREST,
}),
});
/**
* @type {number}
* @readonly
*/
this.internalNodeTilesPerRow = internalNodeTilesPerRow;
/**
* @type {Cartesian2}
* @readonly
*/
this.internalNodeTexelSizeUv = new Cartesian2(
1.0 / internalNodeTextureDimensionX,
1.0 / internalNodeTextureDimensionY
);
/**
* Only generated when there are two or more samples.
* @type {Texture}
* @readonly
*/
this.leafNodeTexture = undefined;
/**
* Only generated when there are two or more samples.
* @type {number}
* @readonly
*/
this.leafNodeTilesPerRow = undefined;
/**
* Only generated when there are two or more samples.
* @type {Cartesian2}
* @readonly
*/
this.leafNodeTexelSizeUv = new Cartesian2();
}
function binaryTreeWeightingRecursive(arr, start, end, depth) {
if (start > end) {
return;
}
const mid = Math.floor((start + end) / 2);
arr[mid] = depth;
binaryTreeWeightingRecursive(arr, start, mid - 1, depth + 1);
binaryTreeWeightingRecursive(arr, mid + 1, end, depth + 1);
}
VoxelTraversal.simultaneousRequestCountMaximum = 50;
/**
* @param {FrameState} frameState
* @param {number} keyframeLocation
* @param {boolean} recomputeBoundingVolumes
* @param {boolean} pauseUpdate
*/
VoxelTraversal.prototype.update = function (
frameState,
keyframeLocation,
recomputeBoundingVolumes,
pauseUpdate
) {
const primitive = this._primitive;
const context = frameState.context;
const maximumTileCount = this.megatextures[0].maximumTileCount;
const keyframeCount = this._keyframeCount;
const levelBlendFactor = primitive._levelBlendFactor;
const hasLevelBlendFactor = levelBlendFactor > 0.0;
const hasKeyframes = keyframeCount > 1;
const sampleCount = (hasLevelBlendFactor ? 2 : 1) * (hasKeyframes ? 2 : 1);
this._sampleCount = sampleCount;
const useLeafNodes = sampleCount >= 2;
if (useLeafNodes && !defined(this.leafNodeTexture)) {
const leafNodeTexelCount = 2;
const leafNodeTextureDimensionX = 1024;
const leafNodeTilesPerRow = Math.floor(
leafNodeTextureDimensionX / leafNodeTexelCount
);
const leafNodeTextureDimensionY = Math.ceil(
maximumTileCount / leafNodeTilesPerRow
);
this.leafNodeTexture = new Texture({
context: context,
pixelFormat: PixelFormat.RGBA,
pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
flipY: false,
width: leafNodeTextureDimensionX,
height: leafNodeTextureDimensionY,
sampler: new Sampler({
minificationFilter: TextureMinificationFilter.NEAREST,
magnificationFilter: TextureMagnificationFilter.NEAREST,
}),
});
this.leafNodeTexelSizeUv = Cartesian2.fromElements(
1.0 / leafNodeTextureDimensionX,
1.0 / leafNodeTextureDimensionY,
this.leafNodeTexelSizeUv
);
this.leafNodeTilesPerRow = leafNodeTilesPerRow;
} else if (!useLeafNodes && defined(this.leafNodeTexture)) {
this.leafNodeTexture = this.leafNodeTexture.destroy();
}
this._keyframeLocation = CesiumMath.clamp(
keyframeLocation,
0.0,
keyframeCount - 1
);
if (recomputeBoundingVolumes) {
recomputeBoundingVolumesRecursive(this, this.rootNode);
}
if (pauseUpdate) {
return;
}
this._frameNumber = frameState.frameNumber;
const timestamp0 = getTimestamp();
loadAndUnload(this, frameState);
const timestamp1 = getTimestamp();
generateOctree(this, sampleCount, levelBlendFactor);
const timestamp2 = getTimestamp();
if (this._debugPrint) {
const loadAndUnloadTimeMs = timestamp1 - timestamp0;
const generateOctreeTimeMs = timestamp2 - timestamp1;
const totalTimeMs = timestamp2 - timestamp0;
printDebugInformation(
this,
loadAndUnloadTimeMs,
generateOctreeTimeMs,
totalTimeMs
);
}
};
/**
* Check if a node is renderable.
* @param {SpatialNode} tile
* @returns {boolean}
*/
VoxelTraversal.prototype.isRenderable = function (tile) {
return tile.isRenderable(this._frameNumber);
};
/**
* 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 VoxelTraversal#destroy
*/
VoxelTraversal.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 VoxelTraversal#isDestroyed
*
* @example
* voxelTraversal = voxelTraversal && voxelTraversal.destroy();
*/
VoxelTraversal.prototype.destroy = function () {
const megatextures = this.megatextures;
const megatextureLength = megatextures.length;
for (let i = 0; i < megatextureLength; i++) {
megatextures[i] = megatextures[i] && megatextures[i].destroy();
}
this.internalNodeTexture =
this.internalNodeTexture && this.internalNodeTexture.destroy();
this.leafNodeTexture = this.leafNodeTexture && this.leafNodeTexture.destroy();
return destroyObject(this);
};
/**
* @function
*
* @param {VoxelTraversal} that
* @param {SpatialNode} node
*
* @private
*/
function recomputeBoundingVolumesRecursive(that, node) {
const primitive = that._primitive;
const shape = primitive._shape;
const dimensions = primitive._provider.dimensions;
node.computeBoundingVolumes(shape, dimensions);
if (defined(node.children)) {
for (let i = 0; i < 8; i++) {
const child = node.children[i];
recomputeBoundingVolumesRecursive(that, child);
}
}
}
/**
* @function
*
* @param {VoxelTraversal} that
* @param {KeyframeNode} keyframeNode
*
* @private
*/
function requestData(that, keyframeNode) {
if (
that._simultaneousRequestCount >=
VoxelTraversal.simultaneousRequestCountMaximum
) {
return;
}
const primitive = that._primitive;
const provider = primitive._provider;
function postRequestSuccess(result) {
that._simultaneousRequestCount--;
const length = primitive._provider.types.length;
if (!defined(result)) {
keyframeNode.state = KeyframeNode.LoadState.UNAVAILABLE;
} else if (result === KeyframeNode.LoadState.FAILED) {
keyframeNode.state = KeyframeNode.LoadState.FAILED;
} else if (!Array.isArray(result) || result.length !== length) {
// TODO should this throw runtime error?
keyframeNode.state = KeyframeNode.LoadState.FAILED;
} else {
const megatextures = that.megatextures;
for (let i = 0; i < length; i++) {
const { voxelCountPerTile, channelCount } = megatextures[i];
const { x, y, z } = voxelCountPerTile;
const tileVoxelCount = x * y * z;
const data = result[i];
const expectedLength = tileVoxelCount * channelCount;
if (data.length === expectedLength) {
keyframeNode.metadatas[i] = data;
// State is received only when all metadata requests have been received
keyframeNode.state = KeyframeNode.LoadState.RECEIVED;
} else {
keyframeNode.state = KeyframeNode.LoadState.FAILED;
break;
}
}
}
}
function postRequestFailure() {
that._simultaneousRequestCount--;
keyframeNode.state = KeyframeNode.LoadState.FAILED;
}
const { keyframe, spatialNode } = keyframeNode;
const promise = provider.requestData({
tileLevel: spatialNode.level,
tileX: spatialNode.x,
tileY: spatialNode.y,
tileZ: spatialNode.z,
keyframe: keyframe,
});
if (defined(promise)) {
that._simultaneousRequestCount++;
keyframeNode.state = KeyframeNode.LoadState.RECEIVING;
promise.then(postRequestSuccess).catch(postRequestFailure);
} else {
keyframeNode.state = KeyframeNode.LoadState.FAILED;
}
}
/**
* @function
*
* @param {number} x
* @returns {number}
*
* @private
*/
function mapInfiniteRangeToZeroOne(x) {
return x / (1.0 + x);
}
/**
* @function
*
* @param {VoxelTraversal} that
* @param {FrameState} frameState
*
* @private
*/
function loadAndUnload(that, frameState) {
const frameNumber = that._frameNumber;
const primitive = that._primitive;
const shape = primitive._shape;
const { dimensions } = primitive;
const targetScreenSpaceError = primitive.screenSpaceError;
const priorityQueue = that._priorityQueue;
const keyframeLocation = that._keyframeLocation;
const keyframeCount = that._keyframeCount;
const rootNode = that.rootNode;
const { camera, context, pixelRatio } = frameState;
const { positionWC, frustum } = camera;
const screenHeight = context.drawingBufferHeight / pixelRatio;
const screenSpaceErrorMultiplier = screenHeight / frustum.sseDenominator;
function keyframePriority(previousKeyframe, keyframe, nextKeyframe) {
const keyframeDifference = Math.min(
Math.abs(keyframe - previousKeyframe),
Math.abs(keyframe - nextKeyframe)
);
const maxKeyframeDifference = Math.max(
previousKeyframe,
keyframeCount - nextKeyframe - 1,
1
);
const keyframeFactor = Math.pow(
1.0 - keyframeDifference / maxKeyframeDifference,
4.0
);
const binaryTreeFactor = Math.exp(
-that._binaryTreeKeyframeWeighting[keyframe]
);
return CesiumMath.lerp(
binaryTreeFactor,
keyframeFactor,
0.15 + 0.85 * keyframeFactor
);
}
/**
* @ignore
* @param {SpatialNode} spatialNode
* @param {number} visibilityPlaneMask
*/
function addToQueueRecursive(spatialNode, visibilityPlaneMask) {
spatialNode.computeScreenSpaceError(positionWC, screenSpaceErrorMultiplier);
visibilityPlaneMask = spatialNode.visibility(
frameState,
visibilityPlaneMask
);
if (visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) {
return;
}
spatialNode.visitedFrameNumber = frameNumber;
const previousKeyframe = CesiumMath.clamp(
Math.floor(keyframeLocation),
0,
keyframeCount - 2
);
const nextKeyframe = previousKeyframe + 1;
// Create keyframe nodes at the playhead.
// If they already exist, nothing will be created.
if (keyframeCount === 1) {
spatialNode.createKeyframeNode(0);
} else if (spatialNode.keyframeNodes.length !== keyframeCount) {
for (let k = 0; k < keyframeCount; k++) {
spatialNode.createKeyframeNode(k);
}
}
const ssePriority = mapInfiniteRangeToZeroOne(spatialNode.screenSpaceError);
let hasLoadedKeyframe = false;
const keyframeNodes = spatialNode.keyframeNodes;
for (let i = 0; i < keyframeNodes.length; i++) {
const keyframeNode = keyframeNodes[i];
keyframeNode.priority =
10.0 * ssePriority +
keyframePriority(previousKeyframe, keyframeNode.keyframe, nextKeyframe);
if (
keyframeNode.state !== KeyframeNode.LoadState.UNAVAILABLE &&
keyframeNode.state !== KeyframeNode.LoadState.FAILED &&
keyframeNode.priority !== -Number.MAX_VALUE
) {
priorityQueue.insert(keyframeNode);
}
if (keyframeNode.state === KeyframeNode.LoadState.LOADED) {
hasLoadedKeyframe = true;
}
}
const meetsScreenSpaceError =
spatialNode.screenSpaceError < targetScreenSpaceError;
if (meetsScreenSpaceError || !hasLoadedKeyframe) {
// Free up memory
spatialNode.children = undefined;
return;
}
if (!defined(spatialNode.children)) {
spatialNode.constructChildNodes(shape, dimensions);
}
for (let childIndex = 0; childIndex < 8; childIndex++) {
const child = spatialNode.children[childIndex];
addToQueueRecursive(child, visibilityPlaneMask);
}
}
priorityQueue.reset();
addToQueueRecursive(rootNode, CullingVolume.MASK_INDETERMINATE);
const highPriorityKeyframeNodes = that._highPriorityKeyframeNodes;
let highPriorityKeyframeNodeCount = 0;
let highPriorityKeyframeNode;
while (priorityQueue.length > 0) {
highPriorityKeyframeNode = priorityQueue.removeMaximum();
highPriorityKeyframeNode.highPriorityFrameNumber = frameNumber;
highPriorityKeyframeNodes[
highPriorityKeyframeNodeCount
] = highPriorityKeyframeNode;
highPriorityKeyframeNodeCount++;
}
const keyframeNodesInMegatexture = that._keyframeNodesInMegatexture;
// TODO: some of the megatexture state should be stored once, not duplicate for each megatexture
const megatexture = that.megatextures[0];
const keyframeNodesInMegatextureCount = megatexture.occupiedCount;
keyframeNodesInMegatexture.length = keyframeNodesInMegatextureCount;
keyframeNodesInMegatexture.sort(function (a, b) {
if (a.highPriorityFrameNumber === b.highPriorityFrameNumber) {
return b.priority - a.priority;
}
return b.highPriorityFrameNumber - a.highPriorityFrameNumber;
});
let destroyedCount = 0;
let addedCount = 0;
for (
let highPriorityKeyframeNodeIndex = 0;
highPriorityKeyframeNodeIndex < highPriorityKeyframeNodeCount;
highPriorityKeyframeNodeIndex++
) {
highPriorityKeyframeNode =
highPriorityKeyframeNodes[highPriorityKeyframeNodeIndex];
if (
highPriorityKeyframeNode.state === KeyframeNode.LoadState.LOADED ||
highPriorityKeyframeNode.spatialNode === undefined
) {
// Already loaded, so nothing to do.
// Or destroyed when adding a higher priority node
continue;
}
if (highPriorityKeyframeNode.state === KeyframeNode.LoadState.UNLOADED) {
requestData(that, highPriorityKeyframeNode);
}
if (highPriorityKeyframeNode.state === KeyframeNode.LoadState.RECEIVED) {
let addNodeIndex = 0;
if (megatexture.isFull()) {
// If the megatexture is full, try removing a discardable node with the lowest priority.
addNodeIndex = keyframeNodesInMegatextureCount - 1 - destroyedCount;
destroyedCount++;
const discardNode = keyframeNodesInMegatexture[addNodeIndex];
discardNode.spatialNode.destroyKeyframeNode(
discardNode,
that.megatextures
);
} else {
addNodeIndex = keyframeNodesInMegatextureCount + addedCount;
addedCount++;
}
highPriorityKeyframeNode.spatialNode.addKeyframeNodeToMegatextures(
highPriorityKeyframeNode,
that.megatextures
);
keyframeNodesInMegatexture[addNodeIndex] = highPriorityKeyframeNode;
}
}
}
/**
* @function
*
* @param {VoxelTraversal} that
*
* @private
*/
function printDebugInformation(
that,
loadAndUnloadTimeMs,
generateOctreeTimeMs,
totalTimeMs
) {
const keyframeCount = that._keyframeCount;
const rootNode = that.rootNode;
const loadStateCount = Object.keys(KeyframeNode.LoadState).length;
const loadStatesByKeyframe = new Array(loadStateCount);
const loadStateByCount = new Array(loadStateCount);
let nodeCountTotal = 0;
for (
let loadStateIndex = 0;
loadStateIndex < loadStateCount;
loadStateIndex++
) {
const keyframeArray = new Array(keyframeCount);
loadStatesByKeyframe[loadStateIndex] = keyframeArray;
for (let i = 0; i < keyframeCount; i++) {
keyframeArray[i] = 0;
}
loadStateByCount[loadStateIndex] = 0;
}
/**
* @ignore
* @param {SpatialNode} node
*/
function traverseRecursive(node) {
const keyframeNodes = node.keyframeNodes;
for (
let keyframeIndex = 0;
keyframeIndex < keyframeNodes.length;
keyframeIndex++
) {
const keyframeNode = keyframeNodes[keyframeIndex];
const keyframe = keyframeNode.keyframe;
const state = keyframeNode.state;
loadStatesByKeyframe[state][keyframe] += 1;
loadStateByCount[state] += 1;
nodeCountTotal++;
}
if (defined(node.children)) {
for (let childIndex = 0; childIndex < 8; childIndex++) {
const child = node.children[childIndex];
traverseRecursive(child);
}
}
}
traverseRecursive(rootNode);
const loadedKeyframeStatistics = `KEYFRAMES: ${
loadStatesByKeyframe[KeyframeNode.LoadState.LOADED]
}`;
const loadStateStatistics =
`UNLOADED: ${loadStateByCount[KeyframeNode.LoadState.UNLOADED]} | ` +
`RECEIVING: ${loadStateByCount[KeyframeNode.LoadState.RECEIVING]} | ` +
`RECEIVED: ${loadStateByCount[KeyframeNode.LoadState.RECEIVED]} | ` +
`LOADED: ${loadStateByCount[KeyframeNode.LoadState.LOADED]} | ` +
`FAILED: ${loadStateByCount[KeyframeNode.LoadState.FAILED]} | ` +
`UNAVAILABLE: ${loadStateByCount[KeyframeNode.LoadState.UNAVAILABLE]} | ` +
`TOTAL: ${nodeCountTotal}`;
const loadAndUnloadTimeMsRounded =
Math.round(loadAndUnloadTimeMs * 100) / 100;
const generateOctreeTimeMsRounded =
Math.round(generateOctreeTimeMs * 100) / 100;
const totalTimeMsRounded = Math.round(totalTimeMs * 100) / 100;
const timerStatistics =
`LOAD: ${loadAndUnloadTimeMsRounded} | ` +
`OCT: ${generateOctreeTimeMsRounded} | ` +
`ALL: ${totalTimeMsRounded}`;
console.log(
`${loadedKeyframeStatistics} || ${loadStateStatistics} || ${timerStatistics}`
);
}
// GPU Octree Layout
// (shown as binary tree instead of octree for demonstration purposes)
//
// Tree representation:
// 0
// / \
// / \
// / \
// 1 3
// / \ / \
// L0 2 L3 L4
// / \
// L1 L2
//
//
// Array representation:
// L = leaf index
// * = index to parent node
// index: 0_______ 1________ 2________ 3_________
// array: [*0, 1, 3, *0, L0, 2, *1 L1, L2, *0, L3, L4]
//
// The array is generated from a depth-first traversal. The end result could be an unbalanced tree,
// so the parent index is stored at each node to make it possible to traverse upwards.
const GpuOctreeFlag = {
// Data is an octree index.
INTERNAL: 0,
// Data is a leaf node.
LEAF: 1,
// When leaf data is packed in the octree and there's a node that is forced to
// render but has no data of its own (such as when its siblings are renderable but it
// is not), signal that it's using its parent's data.
PACKED_LEAF_FROM_PARENT: 2,
};
/**
* @function
*
* @param {VoxelTraversal} that
* @param {FrameState} frameState
* @param {number} sampleCount
* @param {number} levelBlendFactor
* @private
*/
function generateOctree(that, sampleCount, levelBlendFactor) {
const targetSse = that._primitive._screenSpaceError;
const keyframeLocation = that._keyframeLocation;
const frameNumber = that._frameNumber;
const useLeafNodes = sampleCount >= 2;
let internalNodeCount = 0;
let leafNodeCount = 0;
const internalNodeOctreeData = [];
const leafNodeOctreeData = [];
/**
* @ignore
* @param {SpatialNode} node
* @param {number} childOctreeIndex
* @param {number} childEntryIndex
* @param {number} parentOctreeIndex
* @param {number} parentEntryIndex
*/
function buildOctree(
node,
childOctreeIndex,
childEntryIndex,
parentOctreeIndex,
parentEntryIndex
) {
let hasRenderableChildren = false;
if (defined(node.children)) {
for (let c = 0; c < 8; c++) {
const childNode = node.children[c];
childNode.computeSurroundingRenderableKeyframeNodes(keyframeLocation);
if (childNode.isRenderable(frameNumber)) {
hasRenderableChildren = true;
}
}
}
if (hasRenderableChildren) {
// Point the parent and child octree indexes at each other
internalNodeOctreeData[parentEntryIndex] =
(GpuOctreeFlag.INTERNAL << 16) | childOctreeIndex;
internalNodeOctreeData[childEntryIndex] = parentOctreeIndex;
internalNodeCount++;
// Recurse over children
parentOctreeIndex = childOctreeIndex;
parentEntryIndex = parentOctreeIndex * 9 + 1;
for (let cc = 0; cc < 8; cc++) {
const child = node.children[cc];
childOctreeIndex = internalNodeCount;
childEntryIndex = childOctreeIndex * 9 + 0;
buildOctree(
child,
childOctreeIndex,
childEntryIndex,
parentOctreeIndex,
parentEntryIndex + cc
);
}
} else {
// Store the leaf node information instead
// Recursion stops here because there are no renderable children
if (useLeafNodes) {
const baseIdx = leafNodeCount * 5;
const keyframeNode = node.renderableKeyframeNodePrevious;
const levelDifference = node.level - keyframeNode.spatialNode.level;
const parentNode = keyframeNode.spatialNode.parent;
const parentKeyframeNode = defined(parentNode)
? parentNode.renderableKeyframeNodePrevious
: keyframeNode;
const lodLerp = getLodLerp(node, targetSse, levelBlendFactor);
const levelDifferenceChild = levelDifference;
const levelDifferenceParent = 1;
const megatextureIndexChild = keyframeNode.megatextureIndex;
const megatextureIndexParent = parentKeyframeNode.megatextureIndex;
leafNodeOctreeData[baseIdx + 0] = lodLerp;
leafNodeOctreeData[baseIdx + 1] = levelDifferenceChild;
leafNodeOctreeData[baseIdx + 2] = levelDifferenceParent;
leafNodeOctreeData[baseIdx + 3] = megatextureIndexChild;
leafNodeOctreeData[baseIdx + 4] = megatextureIndexParent;
internalNodeOctreeData[parentEntryIndex] =
(GpuOctreeFlag.LEAF << 16) | leafNodeCount;
} else {
const keyframeNode = node.renderableKeyframeNodePrevious;
const levelDifference = node.level - keyframeNode.spatialNode.level;
const flag =
levelDifference === 0
? GpuOctreeFlag.LEAF
: GpuOctreeFlag.PACKED_LEAF_FROM_PARENT;
internalNodeOctreeData[parentEntryIndex] =
(flag << 16) | keyframeNode.megatextureIndex;
}
leafNodeCount++;
}
}
const rootNode = that.rootNode;
rootNode.computeSurroundingRenderableKeyframeNodes(keyframeLocation);
if (rootNode.isRenderable(frameNumber)) {
buildOctree(rootNode, 0, 0, 0, 0);
}
copyToInternalNodeTexture(
internalNodeOctreeData,
9,
that.internalNodeTilesPerRow,
that.internalNodeTexture
);
if (useLeafNodes) {
copyToLeafNodeTexture(
leafNodeOctreeData,
2,
that.leafNodeTilesPerRow,
that.leafNodeTexture
);
}
}
/**
* Compute an interpolation factor between a node and its parent
* @param {SpatialNode} node
* @param {number} targetSse
* @param {number} levelBlendFactor
* @returns {number}
* @private
*/
function getLodLerp(node, targetSse, levelBlendFactor) {
if (node.parent === undefined) {
return 0.0;
}
const sse = node.screenSpaceError;
const parentSse = node.parent.screenSpaceError;
const lodLerp = (targetSse - sse) / (parentSse - sse);
const blended = (lodLerp + levelBlendFactor - 1.0) / levelBlendFactor;
return CesiumMath.clamp(blended, 0.0, 1.0);
}
/**
*
* @param {number[]} data
* @param {number} texelsPerTile
* @param {number} tilesPerRow
* @param {Texture} texture
* @private
*/
function copyToInternalNodeTexture(data, texelsPerTile, tilesPerRow, texture) {
const channelCount = PixelFormat.componentsLength(texture.pixelFormat);
const tileCount = Math.ceil(data.length / texelsPerTile);
const copyWidth = Math.max(
1,
texelsPerTile * Math.min(tileCount, tilesPerRow)
);
const copyHeight = Math.max(1, Math.ceil(tileCount / tilesPerRow));
const textureData = new Uint8Array(copyWidth * copyHeight * channelCount);
for (let i = 0; i < data.length; i++) {
const val = data[i];
const startIndex = i * channelCount;
for (let j = 0; j < channelCount; j++) {
textureData[startIndex + j] = (val >>> (j * 8)) & 0xff;
}
}
const source = {
arrayBufferView: textureData,
width: copyWidth,
height: copyHeight,
};
const copyOptions = {
source: source,
xOffset: 0,
yOffset: 0,
};
texture.copyFrom(copyOptions);
}
/**
*
* @param {number[]} data
* @param {number} texelsPerTile
* @param {number} tilesPerRow
* @param {Texture} texture
* @private
*/
function copyToLeafNodeTexture(data, texelsPerTile, tilesPerRow, texture) {
const channelCount = PixelFormat.componentsLength(texture.pixelFormat);
const datasPerTile = 5;
const tileCount = Math.ceil(data.length / datasPerTile);
const copyWidth = Math.max(
1,
texelsPerTile * Math.min(tileCount, tilesPerRow)
);
const copyHeight = Math.max(1, Math.ceil(tileCount / tilesPerRow));
const textureData = new Uint8Array(copyWidth * copyHeight * channelCount);
for (let tileIndex = 0; tileIndex < tileCount; tileIndex++) {
const timeLerp = data[tileIndex * datasPerTile + 0];
const previousKeyframeLevelsAbove = data[tileIndex * datasPerTile + 1];
const nextKeyframeLevelsAbove = data[tileIndex * datasPerTile + 2];
const previousKeyframeMegatextureIndex = data[tileIndex * datasPerTile + 3];
const nextKeyframeMegatextureIndex = data[tileIndex * datasPerTile + 4];
const timeLerpCompressed = CesiumMath.clamp(
Math.floor(65536 * timeLerp),
0,
65535
);
textureData[tileIndex * 8 + 0] = (timeLerpCompressed >>> 0) & 0xff;
textureData[tileIndex * 8 + 1] = (timeLerpCompressed >>> 8) & 0xff;
textureData[tileIndex * 8 + 2] = previousKeyframeLevelsAbove & 0xff;
textureData[tileIndex * 8 + 3] = nextKeyframeLevelsAbove & 0xff;
textureData[tileIndex * 8 + 4] =
(previousKeyframeMegatextureIndex >>> 0) & 0xff;
textureData[tileIndex * 8 + 5] =
(previousKeyframeMegatextureIndex >>> 8) & 0xff;
textureData[tileIndex * 8 + 6] =
(nextKeyframeMegatextureIndex >>> 0) & 0xff;
textureData[tileIndex * 8 + 7] =
(nextKeyframeMegatextureIndex >>> 8) & 0xff;
}
const source = {
arrayBufferView: textureData,
width: copyWidth,
height: copyHeight,
};
const copyOptions = {
source: source,
xOffset: 0,
yOffset: 0,
};
texture.copyFrom(copyOptions);
}
/**
* @param {number} tileCount
* @param {Cartesian3} dimensions
* @param {MetadataType[]} types
* @param {MetadataComponentType[]} componentTypes
*/
VoxelTraversal.getApproximateTextureMemoryByteLength = function (
tileCount,
dimensions,
types,
componentTypes
) {
let textureMemoryByteLength = 0;
const length = types.length;
for (let i = 0; i < length; i++) {
const type = types[i];
const componentType = componentTypes[i];
const componentCount = MetadataType.getComponentCount(type);
textureMemoryByteLength += Megatexture.getApproximateTextureMemoryByteLength(
tileCount,
dimensions,
componentCount,
componentType
);
}
return textureMemoryByteLength;
};
export default VoxelTraversal;