@threlte/extras
Version:
Utilities, abstractions and plugins for your Threlte apps
191 lines (190 loc) • 7.84 kB
JavaScript
import { Points, Mesh, Vector3, Matrix4, Ray, Sphere, Box3, BufferGeometry, BufferAttribute, MeshBasicMaterial, BatchedMesh } from 'three';
import { acceleratedRaycast, computeBatchedBoundsTree, computeBoundsTree, disposeBatchedBoundsTree, disposeBoundsTree, INTERSECTED, MeshBVHHelper, NOT_INTERSECTED } from 'three-mesh-bvh';
const material = new MeshBasicMaterial();
const trianglePoint = new Vector3();
const inverseMatrix = new Matrix4();
const localRay = new Ray();
const sphere = new Sphere();
const expandedBox = new Box3();
const worldVertex = new Vector3();
const closestOnRay = new Vector3();
const pointsToMesh = new WeakMap();
const objectToHelper = new WeakMap();
const createGeometryBVH = (geometry, opts) => {
geometry.computeBoundsTree = computeBoundsTree;
geometry.disposeBoundsTree = disposeBoundsTree;
geometry.computeBoundsTree(opts);
return () => {
geometry.disposeBoundsTree();
};
};
export const createMeshBVH = (mesh, opts) => {
const cleanup = createGeometryBVH(mesh.geometry, opts);
mesh.raycast = acceleratedRaycast;
if (opts.helper) {
const helper = new MeshBVHHelper(mesh);
mesh.add(helper);
objectToHelper.set(mesh, helper);
}
return () => {
cleanup();
mesh.geometry.disposeBoundsTree();
mesh.raycast = Mesh.prototype.raycast;
if (opts.helper) {
const helper = objectToHelper.get(mesh);
helper?.removeFromParent();
objectToHelper.delete(mesh);
}
};
};
export const createBatchedMeshBVH = (mesh, opts) => {
mesh.computeBoundsTree = computeBatchedBoundsTree;
mesh.disposeBoundsTree = disposeBatchedBoundsTree;
mesh.addGeometry = (...args) => {
const result = BatchedMesh.prototype.addGeometry.call(mesh, ...args);
mesh.computeBoundsTree(result, opts);
return result;
};
if (opts.helper) {
const helper = new MeshBVHHelper(mesh);
mesh.add(helper);
objectToHelper.set(mesh, helper);
}
return () => {
if (mesh.boundsTrees) {
mesh.disposeBoundsTree();
}
mesh.addGeometry = BatchedMesh.prototype.addGeometry;
mesh.raycast = BatchedMesh.prototype.raycast;
if (opts.helper) {
const helper = objectToHelper.get(mesh);
helper?.removeFromParent();
objectToHelper.delete(mesh);
}
};
};
export const createPointsBVH = (points, opts) => {
const geometry = new BufferGeometry();
const positions = points.geometry.getAttribute('position');
const indices = new Uint32Array(positions.count * 3);
for (let i = 0, k = 0, l = positions.count; i < l; i += 1, k += 3) {
indices[k + 0] = indices[k + 1] = indices[k + 2] = i;
}
geometry.setAttribute('position', positions);
geometry.setIndex(new BufferAttribute(indices, 1));
const cleanup = createGeometryBVH(geometry, opts);
const mesh = new Mesh(geometry, material);
mesh.visible = false;
mesh.raycast = acceleratedRaycast;
pointsToMesh.set(points, mesh);
points.raycast = raycastPoints;
if (opts.helper) {
const helper = new MeshBVHHelper(mesh);
objectToHelper.set(points, helper);
points.add(helper, mesh);
}
return () => {
cleanup();
points.raycast = Points.prototype.raycast;
pointsToMesh.delete(points);
if (opts.helper) {
const helper = objectToHelper.get(points);
helper?.removeFromParent();
mesh.removeFromParent();
objectToHelper.delete(points);
}
};
};
function raycastPoints(raycaster, intersects) {
const mesh = pointsToMesh.get(this);
if (!mesh) {
return;
}
const { threshold } = raycaster.params.Points;
// Checking boundingSphere distance to ray
if (this.geometry.boundingSphere === null) {
this.geometry.computeBoundingSphere();
}
sphere.copy(this.geometry.boundingSphere).applyMatrix4(this.matrixWorld);
sphere.radius += threshold;
if (!raycaster.ray.intersectsSphere(sphere)) {
return;
}
const { geometry } = mesh;
const indexArray = geometry.getIndex().array;
const positionArray = geometry.getAttribute('position').array;
// Convert to local space using max axis scale (not average)
const worldScale = this.matrixWorld.getMaxScaleOnAxis();
const localThreshold = threshold / worldScale;
const localThresholdSq = localThreshold * localThreshold;
// only prune if caller asked for a single hit
const firstHitOnly = raycaster.firstHitOnly === true;
let closestDistance = Number.POSITIVE_INFINITY;
inverseMatrix.copy(this.matrixWorld).invert();
localRay.copy(raycaster.ray).applyMatrix4(inverseMatrix);
geometry.boundsTree?.shapecast({
// Disable sorting when we don't prune, it just adds overhead
boundsTraverseOrder: firstHitOnly ? (box) => box.distanceToPoint(localRay.origin) : undefined,
intersectsBounds: (box, _isLeaf, score) => {
if (score !== undefined && score > closestDistance) {
return NOT_INTERSECTED;
}
// expand in local units by localThreshold
const expanded = expandedBox.copy(box).expandByScalar(localThreshold);
return localRay.intersectsBox(expanded) ? INTERSECTED : NOT_INTERSECTED;
},
intersectsRange: (offset, count) => {
const end = offset + count;
for (let tri = offset; tri < end; tri++) {
const resolvedTri = geometry.boundsTree?.resolveTriangleIndex?.(tri) ?? tri;
const index = indexArray[3 * resolvedTri]; // [i, i, i]
const px = positionArray[3 * index];
const py = positionArray[3 * index + 1];
const pz = positionArray[3 * index + 2];
trianglePoint.set(px, py, pz);
// same test as Points.raycast (local units)
const d2 = localRay.distanceSqToPoint(trianglePoint);
if (d2 > localThresholdSq) {
continue;
}
// distance used only for pruning when firstHitOnly
if (firstHitOnly) {
const localDist = localRay.origin.distanceTo(trianglePoint);
if (localDist >= closestDistance) {
continue;
}
closestDistance = localDist;
}
// world-space vertex
worldVertex.copy(trianglePoint).applyMatrix4(this.matrixWorld);
// point on the ray closest to that vertex
const pointOnRay = raycaster.ray.closestPointToPoint(worldVertex, closestOnRay);
// use world-space values
const distance = raycaster.ray.origin.distanceTo(pointOnRay);
if (distance < raycaster.near || distance > raycaster.far) {
if (!firstHitOnly) {
continue;
}
return false;
}
// world-space perpendicular distance to the ray
const distanceToRay = Math.sqrt(raycaster.ray.distanceSqToPoint(worldVertex));
intersects.push({
distance,
distanceToRay,
point: pointOnRay.clone(),
index,
face: null,
faceIndex: null,
barycoord: null,
object: this
});
if (firstHitOnly) {
// short-circuit this branch
return true;
}
}
return false;
}
});
}