ogl
Version:
WebGL Library
346 lines (297 loc) • 13.9 kB
JavaScript
// TODO: barycentric code shouldn't be here, but where?
// TODO: SphereCast?
import { Vec2 } from '../math/Vec2.js';
import { Vec3 } from '../math/Vec3.js';
import { Mat4 } from '../math/Mat4.js';
const tempVec2a = new Vec2();
const tempVec2b = new Vec2();
const tempVec2c = new Vec2();
const tempVec3a = new Vec3();
const tempVec3b = new Vec3();
const tempVec3c = new Vec3();
const tempVec3d = new Vec3();
const tempVec3e = new Vec3();
const tempVec3f = new Vec3();
const tempVec3g = new Vec3();
const tempVec3h = new Vec3();
const tempVec3i = new Vec3();
const tempVec3j = new Vec3();
const tempVec3k = new Vec3();
const tempMat4 = new Mat4();
export class Raycast {
constructor() {
this.origin = new Vec3();
this.direction = new Vec3();
}
// Set ray from mouse unprojection
castMouse(camera, mouse = [0, 0]) {
if (camera.type === 'orthographic') {
// Set origin
// Since camera is orthographic, origin is not the camera position
const { left, right, bottom, top, zoom } = camera;
const x = left / zoom + ((right - left) / zoom) * (mouse[0] * 0.5 + 0.5);
const y = bottom / zoom + ((top - bottom) / zoom) * (mouse[1] * 0.5 + 0.5);
this.origin.set(x, y, 0);
this.origin.applyMatrix4(camera.worldMatrix);
// Set direction
// https://community.khronos.org/t/get-direction-from-transformation-matrix-or-quat/65502/2
this.direction.x = -camera.worldMatrix[8];
this.direction.y = -camera.worldMatrix[9];
this.direction.z = -camera.worldMatrix[10];
} else {
// Set origin
camera.worldMatrix.getTranslation(this.origin);
// Set direction
this.direction.set(mouse[0], mouse[1], 0.5);
camera.unproject(this.direction);
this.direction.sub(this.origin).normalize();
}
}
intersectBounds(meshes, { maxDistance, output = [] } = {}) {
if (!Array.isArray(meshes)) meshes = [meshes];
const invWorldMat4 = tempMat4;
const origin = tempVec3a;
const direction = tempVec3b;
const hits = output;
hits.length = 0;
meshes.forEach((mesh) => {
// Create bounds
if (!mesh.geometry.bounds || mesh.geometry.bounds.radius === Infinity) mesh.geometry.computeBoundingSphere();
const bounds = mesh.geometry.bounds;
invWorldMat4.inverse(mesh.worldMatrix);
// Get max distance locally
let localMaxDistance;
if (maxDistance) {
direction.copy(this.direction).scaleRotateMatrix4(invWorldMat4);
localMaxDistance = maxDistance * direction.len();
}
// Take world space ray and make it object space to align with bounding box
origin.copy(this.origin).applyMatrix4(invWorldMat4);
direction.copy(this.direction).transformDirection(invWorldMat4);
// Break out early if bounds too far away from origin
if (maxDistance) {
if (origin.distance(bounds.center) - bounds.radius > localMaxDistance) return;
}
let localDistance = 0;
// Check origin isn't inside bounds before testing intersection
if (mesh.geometry.raycast === 'sphere') {
if (origin.distance(bounds.center) > bounds.radius) {
localDistance = this.intersectSphere(bounds, origin, direction);
if (!localDistance) return;
}
} else {
if (
origin.x < bounds.min.x ||
origin.x > bounds.max.x ||
origin.y < bounds.min.y ||
origin.y > bounds.max.y ||
origin.z < bounds.min.z ||
origin.z > bounds.max.z
) {
localDistance = this.intersectBox(bounds, origin, direction);
if (!localDistance) return;
}
}
if (maxDistance && localDistance > localMaxDistance) return;
// Create object on mesh to avoid generating lots of objects
if (!mesh.hit) mesh.hit = { localPoint: new Vec3(), point: new Vec3() };
mesh.hit.localPoint.copy(direction).multiply(localDistance).add(origin);
mesh.hit.point.copy(mesh.hit.localPoint).applyMatrix4(mesh.worldMatrix);
mesh.hit.distance = mesh.hit.point.distance(this.origin);
hits.push(mesh);
});
hits.sort((a, b) => a.hit.distance - b.hit.distance);
return hits;
}
intersectMeshes(meshes, { cullFace = true, maxDistance, includeUV = true, includeNormal = true, output = [] } = {}) {
// Test bounds first before testing geometry
const hits = this.intersectBounds(meshes, { maxDistance, output });
if (!hits.length) return hits;
const invWorldMat4 = tempMat4;
const origin = tempVec3a;
const direction = tempVec3b;
const a = tempVec3c;
const b = tempVec3d;
const c = tempVec3e;
const closestFaceNormal = tempVec3f;
const faceNormal = tempVec3g;
const barycoord = tempVec3h;
const uvA = tempVec2a;
const uvB = tempVec2b;
const uvC = tempVec2c;
for (let i = hits.length - 1; i >= 0; i--) {
const mesh = hits[i];
invWorldMat4.inverse(mesh.worldMatrix);
// Get max distance locally
let localMaxDistance;
if (maxDistance) {
direction.copy(this.direction).scaleRotateMatrix4(invWorldMat4);
localMaxDistance = maxDistance * direction.len();
}
// Take world space ray and make it object space to align with bounding box
origin.copy(this.origin).applyMatrix4(invWorldMat4);
direction.copy(this.direction).transformDirection(invWorldMat4);
let localDistance = 0;
let closestA, closestB, closestC;
const geometry = mesh.geometry;
const attributes = geometry.attributes;
const index = attributes.index;
const start = Math.max(0, geometry.drawRange.start);
const end = Math.min(index ? index.count : attributes.position.count, geometry.drawRange.start + geometry.drawRange.count);
for (let j = start; j < end; j += 3) {
// Position attribute indices for each triangle
const ai = index ? index.data[j] : j;
const bi = index ? index.data[j + 1] : j + 1;
const ci = index ? index.data[j + 2] : j + 2;
a.fromArray(attributes.position.data, ai * 3);
b.fromArray(attributes.position.data, bi * 3);
c.fromArray(attributes.position.data, ci * 3);
const distance = this.intersectTriangle(a, b, c, cullFace, origin, direction, faceNormal);
if (!distance) continue;
// Too far away
if (maxDistance && distance > localMaxDistance) continue;
if (!localDistance || distance < localDistance) {
localDistance = distance;
closestA = ai;
closestB = bi;
closestC = ci;
closestFaceNormal.copy(faceNormal);
}
}
if (!localDistance) hits.splice(i, 1);
// Update hit values from bounds-test
mesh.hit.localPoint.copy(direction).multiply(localDistance).add(origin);
mesh.hit.point.copy(mesh.hit.localPoint).applyMatrix4(mesh.worldMatrix);
mesh.hit.distance = mesh.hit.point.distance(this.origin);
// Add unique hit objects on mesh to avoid generating lots of objects
if (!mesh.hit.faceNormal) {
mesh.hit.localFaceNormal = new Vec3();
mesh.hit.faceNormal = new Vec3();
mesh.hit.uv = new Vec2();
mesh.hit.localNormal = new Vec3();
mesh.hit.normal = new Vec3();
}
// Add face normal data which is already computed
mesh.hit.localFaceNormal.copy(closestFaceNormal);
mesh.hit.faceNormal.copy(mesh.hit.localFaceNormal).transformDirection(mesh.worldMatrix);
// Optional data, opt out to optimise a bit if necessary
if (includeUV || includeNormal) {
// Calculate barycoords to find uv values at hit point
a.fromArray(attributes.position.data, closestA * 3);
b.fromArray(attributes.position.data, closestB * 3);
c.fromArray(attributes.position.data, closestC * 3);
this.getBarycoord(mesh.hit.localPoint, a, b, c, barycoord);
}
if (includeUV && attributes.uv) {
uvA.fromArray(attributes.uv.data, closestA * 2);
uvB.fromArray(attributes.uv.data, closestB * 2);
uvC.fromArray(attributes.uv.data, closestC * 2);
mesh.hit.uv.set(
uvA.x * barycoord.x + uvB.x * barycoord.y + uvC.x * barycoord.z,
uvA.y * barycoord.x + uvB.y * barycoord.y + uvC.y * barycoord.z
);
}
if (includeNormal && attributes.normal) {
a.fromArray(attributes.normal.data, closestA * 3);
b.fromArray(attributes.normal.data, closestB * 3);
c.fromArray(attributes.normal.data, closestC * 3);
mesh.hit.localNormal.set(
a.x * barycoord.x + b.x * barycoord.y + c.x * barycoord.z,
a.y * barycoord.x + b.y * barycoord.y + c.y * barycoord.z,
a.z * barycoord.x + b.z * barycoord.y + c.z * barycoord.z
);
mesh.hit.normal.copy(mesh.hit.localNormal).transformDirection(mesh.worldMatrix);
}
}
hits.sort((a, b) => a.hit.distance - b.hit.distance);
return hits;
}
intersectSphere(sphere, origin = this.origin, direction = this.direction) {
const ray = tempVec3c;
ray.sub(sphere.center, origin);
const tca = ray.dot(direction);
const d2 = ray.dot(ray) - tca * tca;
const radius2 = sphere.radius * sphere.radius;
if (d2 > radius2) return 0;
const thc = Math.sqrt(radius2 - d2);
const t0 = tca - thc;
const t1 = tca + thc;
if (t0 < 0 && t1 < 0) return 0;
if (t0 < 0) return t1;
return t0;
}
// Ray AABB - Ray Axis aligned bounding box testing
intersectBox(box, origin = this.origin, direction = this.direction) {
let tmin, tmax, tYmin, tYmax, tZmin, tZmax;
const invdirx = 1 / direction.x;
const invdiry = 1 / direction.y;
const invdirz = 1 / direction.z;
const min = box.min;
const max = box.max;
tmin = ((invdirx >= 0 ? min.x : max.x) - origin.x) * invdirx;
tmax = ((invdirx >= 0 ? max.x : min.x) - origin.x) * invdirx;
tYmin = ((invdiry >= 0 ? min.y : max.y) - origin.y) * invdiry;
tYmax = ((invdiry >= 0 ? max.y : min.y) - origin.y) * invdiry;
if (tmin > tYmax || tYmin > tmax) return 0;
if (tYmin > tmin) tmin = tYmin;
if (tYmax < tmax) tmax = tYmax;
tZmin = ((invdirz >= 0 ? min.z : max.z) - origin.z) * invdirz;
tZmax = ((invdirz >= 0 ? max.z : min.z) - origin.z) * invdirz;
if (tmin > tZmax || tZmin > tmax) return 0;
if (tZmin > tmin) tmin = tZmin;
if (tZmax < tmax) tmax = tZmax;
if (tmax < 0) return 0;
return tmin >= 0 ? tmin : tmax;
}
intersectTriangle(a, b, c, backfaceCulling = true, origin = this.origin, direction = this.direction, normal = tempVec3g) {
// from https://github.com/mrdoob/three.js/blob/master/src/math/Ray.js
// which is from http://www.geometrictools.com/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h
const edge1 = tempVec3h;
const edge2 = tempVec3i;
const diff = tempVec3j;
edge1.sub(b, a);
edge2.sub(c, a);
normal.cross(edge1, edge2);
let DdN = direction.dot(normal);
if (!DdN) return 0;
let sign;
if (DdN > 0) {
if (backfaceCulling) return 0;
sign = 1;
} else {
sign = -1;
DdN = -DdN;
}
diff.sub(origin, a);
let DdQxE2 = sign * direction.dot(edge2.cross(diff, edge2));
if (DdQxE2 < 0) return 0;
let DdE1xQ = sign * direction.dot(edge1.cross(diff));
if (DdE1xQ < 0) return 0;
if (DdQxE2 + DdE1xQ > DdN) return 0;
let QdN = -sign * diff.dot(normal);
if (QdN < 0) return 0;
return QdN / DdN;
}
getBarycoord(point, a, b, c, target = tempVec3h) {
// From https://github.com/mrdoob/three.js/blob/master/src/math/Triangle.js
// static/instance method to calculate barycentric coordinates
// based on: http://www.blackpawn.com/texts/pointinpoly/default.html
const v0 = tempVec3i;
const v1 = tempVec3j;
const v2 = tempVec3k;
v0.sub(c, a);
v1.sub(b, a);
v2.sub(point, a);
const dot00 = v0.dot(v0);
const dot01 = v0.dot(v1);
const dot02 = v0.dot(v2);
const dot11 = v1.dot(v1);
const dot12 = v1.dot(v2);
const denom = dot00 * dot11 - dot01 * dot01;
if (denom === 0) return target.set(-2, -1, -1);
const invDenom = 1 / denom;
const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return target.set(1 - u - v, v, u);
}
}