@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
608 lines (548 loc) • 17.9 kB
JavaScript
import AxisAlignedBoundingBox from "./AxisAlignedBoundingBox.js";
import Cartesian3 from "./Cartesian3.js";
import defined from "./defined.js";
import IntersectionTests from "./IntersectionTests.js";
import Matrix4 from "./Matrix4.js";
import Ray from "./Ray.js";
import TaskProcessor from "./TaskProcessor.js";
import Cartographic from "./Cartographic.js";
import SceneMode from "../Scene/SceneMode.js";
import Interval from "./Interval.js";
import Check from "./Check.js";
import DeveloperError from "./DeveloperError.js";
// Terrain picker can be 4 levels deep (0-3)
const MAXIMUM_TERRAIN_PICKER_LEVEL = 3;
/**
* Creates an object that handles arbitrary ray intersections with a terrain mesh using a spatial acceleration structure.
*
* @alias TerrainPicker
* @constructor
*
* @param {Float32Array} vertices The terrain mesh's vertex buffer.
* @param {Uint8Array|Uint16Array|Uint32Array} indices The terrain mesh's index buffer.
* @param {TerrainEncoding} encoding The terrain mesh's vertex encoding.
*
* @private
*/
function TerrainPicker(vertices, indices, encoding) {
//>>includeStart('debug', pragmas.debug);
Check.defined("vertices", vertices);
Check.defined("indices", indices);
Check.defined("encoding", encoding);
//>>includeEnd('debug');
/**
* The terrain mesh's vertex buffer.
* @type {Float32Array}
*/
this._vertices = vertices;
/**
* The terrain mesh's index buffer.
* @type {Uint32Array}
*/
this._indices = indices;
/**
* The terrain mesh's vertex encoding.
* @type {TerrainEncoding}
*/
this._encoding = encoding;
/**
* The inverse of the terrain mesh tile's transform from world space to local space.
* @type {Matrix4}
*/
this._inverseTransform = new Matrix4(); // Compute as-needed on rebuild
/**
* Whether or not to reset this terrain mesh's picker on the next ray intersection.
* @type {Boolean}
*/
this._needsRebuild = true;
/**
* The root node of the terrain picker's quadtree.
* @type {TerrainPickerNode}
*/
this._rootNode = new TerrainPickerNode();
}
const incrementallyBuildTerrainPickerTaskProcessor = new TaskProcessor(
"incrementallyBuildTerrainPicker",
);
Object.defineProperties(TerrainPicker.prototype, {
/**
* Indicates whether the terrain picker needs to be rebuilt due to changes in the underlying terrain mesh's vertices or indices.
* @type {boolean}
*/
needsRebuild: {
get: function () {
return this._needsRebuild;
},
set: function (value) {
this._needsRebuild = value;
},
},
});
/**
* A node in the terrain picker quadtree.
* @constructor
* @private
*/
function TerrainPickerNode() {
/**
* The tree-space x-coordinate of this node.
* @type {Number}
*/
this.x = 0;
/**
* The tree-space y-coordinate of this node.
* @type {Number}
*/
this.y = 0;
/**
* The level of this node in the quadtree.
* @type {Number}
*/
this.level = 0;
/**
* The axis-aligned bounding box of this node (in the tree's local space).
* @type {AxisAlignedBoundingBox}
*/
this.aabb = createAABBForNode(this.x, this.y, this.level);
/**
* The indices of the triangles that intersect this node.
* @type {Uint32Array}
*/
this.intersectingTriangles = new Uint32Array(0);
/**
* The child terrain picker nodes of this node.
* @type {TerrainPickerNode[]}
*/
this.children = [];
/**
* Whether or not this node is currently building its children on a worker.
* @type {Boolean}
*/
this.buildingChildren = false;
}
/**
* Adds a child node to this node.
*
* @param {number} childIdx The index of the child to add (0-3).
* @memberof TerrainPickerNode
*/
TerrainPickerNode.prototype.addChild = function (childIdx) {
//>>includeStart('debug', pragmas.debug);
if (childIdx < 0 || childIdx > 3) {
throw new DeveloperError(
"TerrainPickerNode child index must be between 0 and 3, inclusive.",
);
}
//>>includeEnd('debug');
const childNode = new TerrainPickerNode();
// Use bitwise operations to get child x,y from child index and parent x,y
childNode.x = this.x * 2 + (childIdx & 1);
childNode.y = this.y * 2 + ((childIdx >> 1) & 1);
childNode.level = this.level + 1;
childNode.aabb = createAABBForNode(childNode.x, childNode.y, childNode.level);
this.children[childIdx] = childNode;
};
const scratchTransformedRay = new Ray();
const scratchTrianglePoints = [
new Cartesian3(),
new Cartesian3(),
new Cartesian3(),
];
/**
* Determines the point on the mesh where the given ray intersects.
* @param {Ray} ray The ray to test.
* @param {Matrix4} tileTransform The terrain mesh tile's transform from local space to world space.
* @param {Boolean} cullBackFaces Whether to consider back-facing triangles as intersections.
* @param {SceneMode} mode The scene mode (2D/3D/Columbus View).
* @param {MapProjection} projection The map projection.
* @returns {Cartesian3 | undefined} result The intersection point, or undefined if there is no intersection.
* @memberof TerrainPicker
* @private
*/
TerrainPicker.prototype.rayIntersect = function (
ray,
tileTransform,
cullBackFaces,
mode,
projection,
) {
// Lazily (re)create the terrain picker
if (this._needsRebuild) {
reset(this, tileTransform);
}
const invTransform = this._inverseTransform;
const transformedRay = scratchTransformedRay;
transformedRay.origin = Matrix4.multiplyByPoint(
invTransform,
ray.origin,
transformedRay.origin,
);
transformedRay.direction = Matrix4.multiplyByPointAsVector(
invTransform,
ray.direction,
transformedRay.direction,
);
const intersections = [];
getNodesIntersectingRay(this._rootNode, transformedRay, intersections);
return findClosestPointInClosestNode(
this,
intersections,
ray,
cullBackFaces,
mode,
projection,
);
};
/**
* Resets the terrain picker's quadtree structure to just the root node. Done whenever the underlying terrain mesh changes.
* @param terrainPicker The terrain picker to reset.
* @private
*/
function reset(terrainPicker, tileTransform) {
// PERFORMANCE_IDEA: warm-start the terrain picker by building a level on a worker.
// This currently isn't feasible because you can only copy the vertex buffer to a worker (slow) or transfer ownership (can't do picking on main thread in meantime).
// SharedArrayBuffers could be used, but most environments do not support them.
Matrix4.inverse(tileTransform, terrainPicker._inverseTransform);
terrainPicker._needsRebuild = false;
const triangleCount = terrainPicker._indices.length / 3;
const intersectingTriangles = new Uint32Array(triangleCount);
for (let i = 0; i < triangleCount; ++i) {
intersectingTriangles[i] = i;
}
terrainPicker._rootNode.intersectingTriangles = intersectingTriangles;
terrainPicker._rootNode.children.length = 0;
}
const scratchAABBMin = new Cartesian3();
const scratchAABBMax = new Cartesian3();
/**
* Creates an axis-aligned bounding box for a quadtree node at the given tree-space coordinates and level.
* This AABB is in the tree's local space (where the root node of the tree is a unit cube in its own local space).
*
* @param {number} x The x coordinate of the node.
* @param {number} y The y coordinate of the node.
* @param {number} level The level of the node.
* @returns {AxisAlignedBoundingBox} The axis-aligned bounding box for the node.
*/
function createAABBForNode(x, y, level) {
const sizeAtLevel = 1.0 / Math.pow(2, level);
const aabbMin = Cartesian3.fromElements(
x * sizeAtLevel - 0.5,
y * sizeAtLevel - 0.5,
-0.5,
scratchAABBMin,
);
const aabbMax = Cartesian3.fromElements(
(x + 1) * sizeAtLevel - 0.5,
(y + 1) * sizeAtLevel - 0.5,
0.5,
scratchAABBMax,
);
return AxisAlignedBoundingBox.fromCorners(aabbMin, aabbMax);
}
/**
* Packs triangle vertex positions and index into provided buffers, for the worker to process.
* (The worker does tests to organize triangles into child nodes of the quadtree.)
* @param {Float32Array} trianglePositionsBuffer The buffer to pack triangle vertex positions into.
* @param {Uint32Array} triangleIndicesBuffer The buffer to pack triangle indices into.
* @param {Cartesian3[]} trianglePositions The triangle's vertex positions.
* @param {number} triangleIndex The triangle's index in the overall tile's index buffer.
* @param {number} bufferIndex The index to use to pack into the buffers.
* @private
*/
function packTriangleBuffers(
trianglePositionsBuffer,
triangleIndicesBuffer,
trianglePositions,
triangleIndex,
bufferIndex,
) {
Cartesian3.pack(
trianglePositions[0],
trianglePositionsBuffer,
9 * bufferIndex,
);
Cartesian3.pack(
trianglePositions[1],
trianglePositionsBuffer,
9 * bufferIndex + 3,
);
Cartesian3.pack(
trianglePositions[2],
trianglePositionsBuffer,
9 * bufferIndex + 6,
);
triangleIndicesBuffer[bufferIndex] = triangleIndex;
}
/**
* @typedef {Object} IntersectingNode
* @property {TerrainPickerNode} node - The intersecting quadtree node.
* @property {Interval} interval - The interval along the ray where the intersection occurs.
* @private
*/
const scratchInterval = new Interval();
/**
* Recursively gathers all nodes in the quadtree that intersect the ray.
*
* @param {TerrainPickerNode} currentNode The current node being tested.
* @param {Ray} ray The ray to test.
* @param {IntersectingNode[]} intersectingNodes The array to store intersecting nodes in.
* @private
*/
function getNodesIntersectingRay(currentNode, ray, intersectingNodes) {
const interval = IntersectionTests.rayAxisAlignedBoundingBox(
ray,
currentNode.aabb,
scratchInterval,
);
if (!defined(interval)) {
return;
}
const isLeaf = !currentNode.children.length || currentNode.buildingChildren;
if (isLeaf) {
intersectingNodes.push({
node: currentNode,
interval: new Interval(interval.start, interval.stop),
});
return;
}
for (let i = 0; i < currentNode.children.length; i++) {
getNodesIntersectingRay(currentNode.children[i], ray, intersectingNodes);
}
}
/**
* Finds the closest intersecting node along the ray, in world space, and the closest point in that node,
* by testing all triangles in the closest node against the ray.
*
* @param {TerrainPicker} terrainPicker The terrain picker.
* @param {IntersectingNode[]} intersections The nodes that intersect the ray, along with the intersection intervals along said ray.
* @param {Ray} ray The ray to test.
* @param {boolean} cullBackFaces Whether to cull back faces.
* @param {SceneMode} mode The scene mode (2D/3D/Columbus View).
* @param {MapProjection} projection The map projection.
* @returns The closest point in world space, or undefined if no intersection.
* @private
*/
function findClosestPointInClosestNode(
terrainPicker,
intersections,
ray,
cullBackFaces,
mode,
projection,
) {
const sortedIntersections = intersections.sort(function (a, b) {
return a.interval.start - b.interval.start;
});
let minT = Number.MAX_VALUE;
for (let i = 0; i < sortedIntersections.length; i++) {
const intersection = sortedIntersections[i];
const intersectionResult = getClosestTriangleInNode(
terrainPicker,
ray,
intersection.node,
cullBackFaces,
mode,
projection,
);
minT = Math.min(intersectionResult, minT);
if (minT !== Number.MAX_VALUE) {
break;
}
}
if (minT !== Number.MAX_VALUE) {
return Ray.getPoint(ray, minT);
}
return undefined;
}
/**
* Test all triangles in the given node against the ray, returning the closest intersection t value along the ray.
* Additionally, collect the triangles' positions and indices along the way to launch worker process that uses them to build out child nodes.
*
* @param {TerrainPicker} terrainPicker The terrain picker.
* @param {Ray} ray The ray to test.
* @param {TerrainPickerNode} node The node to test.
* @param {boolean} cullBackFaces Whether to cull back faces.
* @param {SceneMode} mode The scene mode (2D/3D/Columbus View).
* @param {MapProjection} projection The map projection.
* @returns {number} The closest intersection t value along the ray, or Number.MAX_VALUE if no intersection.
* @private
*/
function getClosestTriangleInNode(
terrainPicker,
ray,
node,
cullBackFaces,
mode,
projection,
) {
let result = Number.MAX_VALUE;
const encoding = terrainPicker._encoding;
const indices = terrainPicker._indices;
const vertices = terrainPicker._vertices;
const triangleCount = node.intersectingTriangles.length;
const isMaxLevel = node.level >= MAXIMUM_TERRAIN_PICKER_LEVEL;
const shouldBuildChildren = !isMaxLevel && !node.buildingChildren;
let trianglePositions;
let triangleIndices;
if (shouldBuildChildren) {
// If the tree can be built deeper, prepare buffers to store triangle data for child nodes
trianglePositions = new Float32Array(triangleCount * 9); // 3 vertices per triangle * 3 floats per vertex
triangleIndices = new Uint32Array(triangleCount);
}
for (let i = 0; i < triangleCount; i++) {
const triIndex = node.intersectingTriangles[i];
const v0 = getVertexPosition(
encoding,
mode,
projection,
vertices,
indices[3 * triIndex],
scratchTrianglePoints[0],
);
const v1 = getVertexPosition(
encoding,
mode,
projection,
vertices,
indices[3 * triIndex + 1],
scratchTrianglePoints[1],
);
const v2 = getVertexPosition(
encoding,
mode,
projection,
vertices,
indices[3 * triIndex + 2],
scratchTrianglePoints[2],
);
const triT = IntersectionTests.rayTriangleParametric(
ray,
v0,
v1,
v2,
cullBackFaces,
);
if (defined(triT) && triT < result && triT >= 0) {
result = triT;
}
if (shouldBuildChildren) {
packTriangleBuffers(
trianglePositions,
triangleIndices,
scratchTrianglePoints,
triIndex,
i,
);
}
}
if (shouldBuildChildren) {
for (let childIdx = 0; childIdx < 4; childIdx++) {
node.addChild(childIdx);
}
addTrianglesToChildrenNodes(
terrainPicker._inverseTransform,
node,
triangleIndices,
trianglePositions,
);
}
return result;
}
const scratchCartographic = new Cartographic();
/**
* Gets a vertex position from the buffer, taking into account the exaggeration and scene mode of the terrain.
*
* @param {TerrainEncoding} encoding The terrain encoding.
* @param {SceneMode} mode The scene mode (2D/3D/Columbus View).
* @param {MapProjection} projection The map projection.
* @param {Float32Array} vertices The vertex buffer of the terrain mesh.
* @param {Number} index The index of the vertex to get.
* @param {Cartesian3} result The decoded, exaggerated, and possibly projected vertex position.
* @returns {Cartesian3} The result vertex position.
* @private
*/
function getVertexPosition(
encoding,
mode,
projection,
vertices,
index,
result,
) {
let position = encoding.getExaggeratedPosition(vertices, index, result);
if (mode === SceneMode.SCENE3D) {
return position;
}
const ellipsoid = projection.ellipsoid;
const positionCartographic = ellipsoid.cartesianToCartographic(
position,
scratchCartographic,
);
position = projection.project(positionCartographic, result);
// Swizzle because coordinate basis are different in 2D/Columbus View
position = Cartesian3.fromElements(
position.z,
position.x,
position.y,
result,
);
return position;
}
/**
* Adds triangles to the child nodes of the given node by launching a worker process to do AABB-triangle testing.
*
* @param {Matrix4} inverseTransform
* @param {TerrainNode} node
* @param {Uint32Array} triangleIndices
* @param {Float32Array} trianglePositions
* @returns {Promise<void>} A promise that resolves when the triangles have been added to the child nodes.
* @private
*/
async function addTrianglesToChildrenNodes(
inverseTransform,
node,
triangleIndices,
trianglePositions,
) {
node.buildingChildren = true;
// Prepare data to be sent to a worker
const inverseTransformPacked = new Float64Array(16);
Matrix4.pack(inverseTransform, inverseTransformPacked, 0);
const aabbArray = new Float64Array(6 * 4); // 6 elements per AABB, 4 children
for (let i = 0; i < 4; i++) {
Cartesian3.pack(node.children[i].aabb.minimum, aabbArray, i * 6);
Cartesian3.pack(node.children[i].aabb.maximum, aabbArray, i * 6 + 3);
}
const parameters = {
aabbs: aabbArray,
inverseTransform: inverseTransformPacked,
triangleIndices: triangleIndices,
trianglePositions: trianglePositions,
};
const transferableObjects = [
aabbArray.buffer,
inverseTransformPacked.buffer,
triangleIndices.buffer,
trianglePositions.buffer,
];
const incrementallyBuildTerrainPickerPromise =
incrementallyBuildTerrainPickerTaskProcessor.scheduleTask(
parameters,
transferableObjects,
);
if (!defined(incrementallyBuildTerrainPickerPromise)) {
// Failed to schedule task, retry on next pick
node.buildingChildren = false;
return;
}
// After worker completes, it transfers back a buffer of intersecting triangles for each child node
// Assign these to the child nodes
const result = await incrementallyBuildTerrainPickerPromise;
result.intersectingTrianglesArrays.forEach((buffer, index) => {
node.children[index].intersectingTriangles = new Uint32Array(buffer);
});
// The node's triangles have been distributed to its children
node.intersectingTriangles = new Uint32Array(0);
node.buildingChildren = false;
}
export default TerrainPicker;