import { Matrix4 } from '../math/matrix4.js';
import { Scene } from '../scene/scene.js';
import { Camera } from '../scene/camera.js';
import { Mesh } from '../scene/mesh.js';
/**
* Base value for boolean defaults.
*
* @type {boolean}
*/
const DEFAULT_RECURSIVE = true;
/**
* Default filter value.
*
* @type {null}
*/
const DEFAULT_FILTER = null;
/**
* Default sorting flag for raycast results.
*
* @type {boolean}
*/
const DEFAULT_SORT = true;
/**
* Minimum NDC value for the near plane.
*
* @type {number}
*/
const NDC_NEAR = -1.0;
/**
* Maximum NDC value for the far plane.
*
* @type {number}
*/
const NDC_FAR = 1.0;
/**
* Clip-space `W component` for unprojection.
*
* @type {number}
*/
const CLIP_W = 1.0;
/**
* Zero component value.
*
* @type {number}
*/
const ZERO_COMPONENT = 0.0;
/**
* One component value.
*
* @type {number}
*/
const ONE_COMPONENT = 1.0;
/**
* Matrix element indices for the 4x4 matrices.
*
* @type {number}
*/
const MATRIX_INDEX_00 = 0;
const MATRIX_INDEX_01 = 1;
const MATRIX_INDEX_02 = 2;
const MATRIX_INDEX_03 = 3;
const MATRIX_INDEX_10 = 4;
const MATRIX_INDEX_11 = 5;
const MATRIX_INDEX_12 = 6;
const MATRIX_INDEX_13 = 7;
const MATRIX_INDEX_20 = 8;
const MATRIX_INDEX_21 = 9;
const MATRIX_INDEX_22 = 10;
const MATRIX_INDEX_23 = 11;
const MATRIX_INDEX_30 = 12;
const MATRIX_INDEX_31 = 13;
const MATRIX_INDEX_32 = 14;
const MATRIX_INDEX_33 = 15;
/**
* Loop start index.
*
* @type {number}
*/
const LOOP_START_INDEX = 0;
/**
* Loop increment value.
*
* @type {number}
*/
const LOOP_INCREMENT = 1;
/**
* Index for X component in arrays.
*
* @type {number}
*/
const VECTOR_INDEX_X = 0;
/**
* Index for Y component in arrays.
*
* @type {number}
*/
const VECTOR_INDEX_Y = 1;
/**
* Index for Z component in arrays.
*
* @type {number}
*/
const VECTOR_INDEX_Z = 2;
/**
* Range array index: min.
*
* @type {number}
*/
const RANGE_MIN_INDEX = 0;
/**
* Range array index: max.
*
* @type {number}
*/
const RANGE_MAX_INDEX = 1;
/**
* Minimum ray parameter value.
*
* @type {number}
*/
const RAY_T_MIN = 0.0;
/**
* Maximum ray parameter initial value.
*
* @type {number}
*/
const RAY_T_MAX_INIT = Number.POSITIVE_INFINITY;
/**
* Epsilon used for ray direction checks.
*
* @type {number}
*/
const RAY_DIRECTION_EPSILON = 1e-8;
/**
* Inverse numerator used in direction inverse computations.
*
* @type {number}
*/
const INVERSE_DIRECTION_NUMERATOR = 1.0;
/**
* Component key for X.
*
* @type {string}
*/
const COMPONENT_X = 'x';
/**
* Component key for Y.
*
* @type {string}
*/
const COMPONENT_Y = 'y';
/**
* Component key for Z.
*
* @type {string}
*/
const COMPONENT_Z = 'z';
/**
* Intersection object key.
*
* @type {string}
*/
const INTERSECTION_OBJECT_KEY = 'object';
/**
* Intersection distance key.
*
* @type {string}
*/
const INTERSECTION_DISTANCE_KEY = 'distance';
/**
* Intersection point key.
*
* @type {string}
*/
const INTERSECTION_POINT_KEY = 'point';
/**
* Raycast options key: recursive.
*
* @type {string}
*/
const OPTION_RECURSIVE_KEY = 'recursive';
/**
* Raycast options key: filter.
*
* @type {string}
*/
const OPTION_FILTER_KEY = 'filter';
/**
* Raycast options key: sort.
*
* @type {string}
*/
const OPTION_SORT_KEY = 'sort';
/**
* Mouse NDC key: x.
*
* @type {string}
*/
const MOUSE_NDC_X_KEY = 'x';
/**
* Mouse NDC key: y.
*
* @type {string}
*/
const MOUSE_NDC_Y_KEY = 'y';
/**
* Error message: invalid scene.
*
* @type {string}
*/
const ERROR_SCENE_TYPE = '`Raycaster.raycast` expects scene as a `Scene` instance.';
/**
* Error message: invalid camera.
*
* @type {string}
*/
const ERROR_CAMERA_TYPE = '`Raycaster.raycast` expects camera as a `Camera` instance.';
/**
* Error message: invalid mouse ndc object.
*
* @type {string}
*/
const ERROR_MOUSE_NDC_TYPE = '`Raycaster.raycast` expects `mouseNdc` as an object with numeric x/y.';
/**
* Error message: invalid options object.
*
* @type {string}
*/
const ERROR_OPTIONS_TYPE = '`Raycaster.raycast` expects options as a plain object.';
/**
* Error message: invalid recursive option.
*
* @type {string}
*/
const ERROR_OPTION_RECURSIVE_TYPE = '`Raycaster.raycast` option recursive must be a boolean.';
/**
* Error message: invalid filter option.
*
* @type {string}
*/
const ERROR_OPTION_FILTER_TYPE = '`Raycaster.raycast` option filter must be a function or null.';
/**
* Error message: invalid sort option.
*
* @type {string}
*/
const ERROR_OPTION_SORT_TYPE = '`Raycaster.raycast` option sort must be a boolean.';
/**
* Error message: invalid unproject W component.
*
* @type {string}
*/
const ERROR_UNPROJECT_W_ZERO = '`Raycaster.raycast` failed to unproject, `W component` is zero.';
/**
* @typedef {Object} RaycastPoint
* @property {number} x - X coordinate.
* @property {number} y - Y coordinate.
* @property {number} z - Z coordinate.
*/
/**
* @typedef {Object} Intersection
* @property {Mesh} object - Intersected mesh.
* @property {number} distance - Distance from ray origin to the intersection point.
* @property {RaycastPoint} point - Intersection point in world space.
*/
/**
* @typedef {Object} RaycastOptions
* @property {boolean} [recursive=true] - When true, traverses all descendants of the scene.
* @property {function(Mesh): boolean | null} [filter=null] - Optional mesh filter.
* @property {boolean} [sort=true] - When true, sorts intersections by distance.
*/
/**
* @typedef {Object} Ray
* @property {RaycastPoint} origin - Ray origin in world space.
* @property {RaycastPoint} direction - Normalized ray direction in world space.
*/
/**
* Raycaster performs raycasting against scene meshes using their local AABB.
*/
export class Raycaster {
/**
* Performs a raycast against the given scene and returns intersections.
*
* @param {Scene} scene - Scene to query.
* @param {Camera} camera - Camera used to build the ray.
* @param {{x: number, y: number}} mouseNdc - Mouse coordinates in NDC space [-1..1].
* @param {RaycastOptions} [options] - Optional raycast settings.
* @returns {Intersection[]} - Intersection list.
*/
raycast(scene, camera, mouseNdc, options = {}) {
if (!(scene instanceof Scene)) {
throw new TypeError(ERROR_SCENE_TYPE);
}
if (!(camera instanceof Camera)) {
throw new TypeError(ERROR_CAMERA_TYPE);
}
if (mouseNdc === null || typeof mouseNdc !== 'object' || Array.isArray(mouseNdc)) {
throw new TypeError(ERROR_MOUSE_NDC_TYPE);
}
if (typeof mouseNdc[MOUSE_NDC_X_KEY] !== 'number' || typeof mouseNdc[MOUSE_NDC_Y_KEY] !== 'number') {
throw new TypeError(ERROR_MOUSE_NDC_TYPE);
}
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError(ERROR_OPTIONS_TYPE);
}
const recursive = OPTION_RECURSIVE_KEY in options ? options[OPTION_RECURSIVE_KEY] : DEFAULT_RECURSIVE;
const filter = OPTION_FILTER_KEY in options ? options[OPTION_FILTER_KEY] : DEFAULT_FILTER;
const shouldSort = OPTION_SORT_KEY in options ? options[OPTION_SORT_KEY] : DEFAULT_SORT;
if (typeof recursive !== 'boolean') {
throw new TypeError(ERROR_OPTION_RECURSIVE_TYPE);
}
if (filter !== null && typeof filter !== 'function') {
throw new TypeError(ERROR_OPTION_FILTER_TYPE);
}
if (typeof shouldSort !== 'boolean') {
throw new TypeError(ERROR_OPTION_SORT_TYPE);
}
scene.updateWorldMatrix({ parentWorldMatrix: null });
const viewMatrix = camera.getViewMatrix();
const projectionMatrix = camera.getProjectionMatrix();
const viewProjectionMatrix = Matrix4.multiply(projectionMatrix, viewMatrix);
const inverseViewProjectionMatrix = Matrix4.invert(viewProjectionMatrix);
const ray = Raycaster.#createRay(inverseViewProjectionMatrix, mouseNdc[MOUSE_NDC_X_KEY], mouseNdc[MOUSE_NDC_Y_KEY]);
const intersections = [];
if (recursive) {
scene.traverse((object) => Raycaster.#collectIntersection(object, ray, filter, intersections));
} else {
const children = scene.children;
for (let index = LOOP_START_INDEX; index < children.length; index += LOOP_INCREMENT) {
Raycaster.#collectIntersection(children[index], ray, filter, intersections);
}
}
if (shouldSort) {
intersections.sort((left, right) => left[INTERSECTION_DISTANCE_KEY] - right[INTERSECTION_DISTANCE_KEY]);
}
return intersections;
}
/**
* @param {Object} object - Scene object.
* @param {Object} ray - Ray data with origin/direction.
* @param {Function | null} filter - Optional mesh filter.
* @param {Intersection[]} intersections - Output array.
* @private
*/
static #collectIntersection(object, ray, filter, intersections) {
if (!(object instanceof Mesh)) {
return;
}
if (object.isDisposed) {
return;
}
if (filter && filter(object) === false) {
return;
}
const geometry = object.geometry;
const boundingBoxMin = geometry.getBoundingBoxMin();
const boundingBoxMax = geometry.getBoundingBoxMax();
const inverseWorldMatrix = Matrix4.invert(object.worldMatrix);
const localOrigin = Raycaster.#transformPoint(inverseWorldMatrix, ray.origin, ONE_COMPONENT);
const localDirection = Raycaster.#transformPoint(inverseWorldMatrix, ray.direction, ZERO_COMPONENT);
const hitDistance = Raycaster.#intersectRayAabb(localOrigin, localDirection, boundingBoxMin, boundingBoxMax);
if (hitDistance === null) {
return;
}
const localPoint = {
[COMPONENT_X] : localOrigin[COMPONENT_X] + (localDirection[COMPONENT_X] * hitDistance),
[COMPONENT_Y] : localOrigin[COMPONENT_Y] + (localDirection[COMPONENT_Y] * hitDistance),
[COMPONENT_Z] : localOrigin[COMPONENT_Z] + (localDirection[COMPONENT_Z] * hitDistance)
};
const worldPoint = Raycaster.#transformPoint(object.worldMatrix, localPoint, ONE_COMPONENT);
const distance = Raycaster.#distanceBetween(ray.origin, worldPoint);
intersections.push({
[INTERSECTION_OBJECT_KEY] : object,
[INTERSECTION_DISTANCE_KEY] : distance,
[INTERSECTION_POINT_KEY] : worldPoint
});
}
/**
* @param {Float32Array} inverseViewProjectionMatrix - Inverse view-projection matrix.
* @param {number} ndcX - Normalized device coordinate X.
* @param {number} ndcY - Normalized device coordinate Y.
* @returns {Ray} - Ray with origin and direction in world space.
* @private
*/
static #createRay(inverseViewProjectionMatrix, ndcX, ndcY) {
const nearPoint = Raycaster.#unproject(inverseViewProjectionMatrix, ndcX, ndcY, NDC_NEAR);
const farPoint = Raycaster.#unproject(inverseViewProjectionMatrix, ndcX, ndcY, NDC_FAR);
const direction = Raycaster.#normalizeVector({
[COMPONENT_X] : farPoint[COMPONENT_X] - nearPoint[COMPONENT_X],
[COMPONENT_Y] : farPoint[COMPONENT_Y] - nearPoint[COMPONENT_Y],
[COMPONENT_Z] : farPoint[COMPONENT_Z] - nearPoint[COMPONENT_Z]
});
return {
origin: nearPoint,
direction
};
}
/**
* @param {Float32Array} matrix - Inverse view-projection matrix.
* @param {number} ndcX - NDC x.
* @param {number} ndcY - NDC y.
* @param {number} ndcZ - NDC z.
* @returns {RaycastPoint} - Unprojected point in world space.
* @private
*/
static #unproject(matrix, ndcX, ndcY, ndcZ) {
// Build the clip-space position from NDC coordinates:
const clipX = ndcX;
const clipY = ndcY;
const clipZ = ndcZ;
const clipW = CLIP_W;
// Compute world-space (homogeneous) using the inverse view-projection matrix:
const worldX = (matrix[MATRIX_INDEX_00] * clipX)
+ (matrix[MATRIX_INDEX_10] * clipY)
+ (matrix[MATRIX_INDEX_20] * clipZ)
+ (matrix[MATRIX_INDEX_30] * clipW);
const worldY = (matrix[MATRIX_INDEX_01] * clipX)
+ (matrix[MATRIX_INDEX_11] * clipY)
+ (matrix[MATRIX_INDEX_21] * clipZ)
+ (matrix[MATRIX_INDEX_31] * clipW);
const worldZ = (matrix[MATRIX_INDEX_02] * clipX)
+ (matrix[MATRIX_INDEX_12] * clipY)
+ (matrix[MATRIX_INDEX_22] * clipZ)
+ (matrix[MATRIX_INDEX_32] * clipW);
const worldW = (matrix[MATRIX_INDEX_03] * clipX)
+ (matrix[MATRIX_INDEX_13] * clipY)
+ (matrix[MATRIX_INDEX_23] * clipZ)
+ (matrix[MATRIX_INDEX_33] * clipW);
if (worldW === ZERO_COMPONENT) {
throw new Error(ERROR_UNPROJECT_W_ZERO);
}
// Compute the `reciprocal W` value for perspective divide:
const inverseW = ONE_COMPONENT / worldW;
return {
[COMPONENT_X] : worldX * inverseW,
[COMPONENT_Y] : worldY * inverseW,
[COMPONENT_Z] : worldZ * inverseW
};
}
/**
* @param {Float32Array} matrix - Matrix to apply.
* @param {RaycastPoint} point - Point to transform.
* @param {number} wComponent - Homogeneous W component (1 for points, 0 for vectors).
* @returns {RaycastPoint} - Transformed (x, y, z) after applying the 4x4 matrix with the given `W component`.
* @private
*/
static #transformPoint(matrix, point, wComponent) {
const x = point[COMPONENT_X];
const y = point[COMPONENT_Y];
const z = point[COMPONENT_Z];
return {
[COMPONENT_X]: (matrix[MATRIX_INDEX_00] * x)
+ (matrix[MATRIX_INDEX_10] * y)
+ (matrix[MATRIX_INDEX_20] * z)
+ (matrix[MATRIX_INDEX_30] * wComponent),
[COMPONENT_Y]: (matrix[MATRIX_INDEX_01] * x)
+ (matrix[MATRIX_INDEX_11] * y)
+ (matrix[MATRIX_INDEX_21] * z)
+ (matrix[MATRIX_INDEX_31] * wComponent),
[COMPONENT_Z]: (matrix[MATRIX_INDEX_02] * x)
+ (matrix[MATRIX_INDEX_12] * y)
+ (matrix[MATRIX_INDEX_22] * z)
+ (matrix[MATRIX_INDEX_32] * wComponent)
};
}
/**
* @param {RaycastPoint} vector - Vector to normalize.
* @returns {RaycastPoint} - Normalized vector, returns zero vector when input length is zero.
* @private
*/
static #normalizeVector(vector) {
const x = vector[COMPONENT_X];
const y = vector[COMPONENT_Y];
const z = vector[COMPONENT_Z];
const length = Math.sqrt((x * x) + (y * y) + (z * z));
if (length === ZERO_COMPONENT) {
return {
[COMPONENT_X] : ZERO_COMPONENT,
[COMPONENT_Y] : ZERO_COMPONENT,
[COMPONENT_Z] : ZERO_COMPONENT
};
}
const inverseLength = ONE_COMPONENT / length;
return {
[COMPONENT_X] : x * inverseLength,
[COMPONENT_Y] : y * inverseLength,
[COMPONENT_Z] : z * inverseLength
};
}
/**
* @param {RaycastPoint} origin - Ray origin in local space.
* @param {RaycastPoint} direction - Ray direction in local space.
* @param {Float32Array} min - AABB minimum.
* @param {Float32Array} max - AABB maximum.
* @returns {number | null} - Intersection distance parameter along the ray direction, null when the ray misses the AABB.
* @private
*/
static #intersectRayAabb(origin, direction, min, max) {
// Initialize the running ray-AABB intersection interval:
let tMin = RAY_T_MIN;
let tMax = RAY_T_MAX_INIT;
// Extract ray origin components:
const originX = origin[COMPONENT_X];
const originY = origin[COMPONENT_Y];
const originZ = origin[COMPONENT_Z];
// Extract ray direction components:
const directionX = direction[COMPONENT_X];
const directionY = direction[COMPONENT_Y];
const directionZ = direction[COMPONENT_Z];
// Extract AABB bounds components:
const minX = min[VECTOR_INDEX_X];
const minY = min[VECTOR_INDEX_Y];
const minZ = min[VECTOR_INDEX_Z];
const maxX = max[VECTOR_INDEX_X];
const maxY = max[VECTOR_INDEX_Y];
const maxZ = max[VECTOR_INDEX_Z];
// Compute the ray intersection range for the X axis slab:
const rangeX = Raycaster.#getAxisRange(originX, directionX, minX, maxX);
// No overlap with the X slab, so no intersection with the AABB:
if (!rangeX) {
return null;
}
// Intersect the running interval with the X slab interval:
tMin = Math.max(tMin, rangeX[RANGE_MIN_INDEX]);
tMax = Math.min(tMax, rangeX[RANGE_MAX_INDEX]);
// If entry exceeds exit, the slabs do not overlap, so no intersection:
if (tMin > tMax) {
return null;
}
// The same, but for Y:
const rangeY = Raycaster.#getAxisRange(originY, directionY, minY, maxY);
if (!rangeY) {
return null;
}
tMin = Math.max(tMin, rangeY[RANGE_MIN_INDEX]);
tMax = Math.min(tMax, rangeY[RANGE_MAX_INDEX]);
if (tMin > tMax) {
return null;
}
// The same, but for Z:
const rangeZ = Raycaster.#getAxisRange(originZ, directionZ, minZ, maxZ);
if (!rangeZ) {
return null;
}
tMin = Math.max(tMin, rangeZ[RANGE_MIN_INDEX]);
tMax = Math.min(tMax, rangeZ[RANGE_MAX_INDEX]);
if (tMin > tMax) {
return null;
}
if (tMax < RAY_T_MIN) {
return null;
}
return tMin >= RAY_T_MIN ? tMin : tMax;
}
/**
* @param {number} origin - Ray origin component.
* @param {number} direction - Ray direction component.
* @param {number} min - AABB min component.
* @param {number} max - AABB max component.
* @returns {number[] | null} - Two-element array for this axis slab, or null if the ray misses the slab.
* @private
*/
static #getAxisRange(origin, direction, min, max) {
if (Math.abs(direction) < RAY_DIRECTION_EPSILON) {
return (origin < min || origin > max) ? null : [RAY_T_MIN, RAY_T_MAX_INIT];
}
const inverseDirection = INVERSE_DIRECTION_NUMERATOR / direction;
let axisEntry = (min - origin) * inverseDirection;
let axisExit = (max - origin) * inverseDirection;
if (axisEntry > axisExit) {
const tempValue = axisEntry;
axisEntry = axisExit;
axisExit = tempValue;
}
return [axisEntry, axisExit];
}
/**
* @param {RaycastPoint} from - Start point.
* @param {RaycastPoint} to - End point.
* @returns {number} - Euclidean distance between the two points.
* @private
*/
static #distanceBetween(from, to) {
const deltaX = to[COMPONENT_X] - from[COMPONENT_X];
const deltaY = to[COMPONENT_Y] - from[COMPONENT_Y];
const deltaZ = to[COMPONENT_Z] - from[COMPONENT_Z];
return Math.sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
}
}