import { Vec3 } from '../math/Vec3' import { Quaternion } from '../math/Quaternion' import { Transform } from '../math/Transform' import { RaycastResult } from '../collision/RaycastResult' import { Shape } from '../shapes/Shape' import { AABB } from '../collision/AABB' import type { Body } from '../objects/Body' import type { Sphere } from '../shapes/Sphere' import type { Box } from '../shapes/Box' import type { Plane } from '../shapes/Plane' import type { Heightfield } from '../shapes/Heightfield' import type { ConvexPolyhedron } from '../shapes/ConvexPolyhedron' import type { Trimesh } from '../shapes/Trimesh' import type { World } from '../world/World' /** * RAY_MODES */ export const RAY_MODES = { /** CLOSEST */ CLOSEST: 1, /** ANY */ ANY: 2, /** ALL */ ALL: 4, } as const /** * RayMode */ export type RayMode = typeof RAY_MODES[keyof typeof RAY_MODES] /** * RayOptions */ export type RayOptions = { /** * from */ from?: Vec3 /** * to */ to?: Vec3 /** * mode */ mode?: RayMode /** * result */ result?: RaycastResult /** * If set to `true`, the ray skips any hits with normal.dot(rayDirection) < 0. * @default false */ skipBackfaces?: boolean /** * collisionFilterMask * @default -1 */ collisionFilterMask?: number /** * collisionFilterGroup * @default -1 */ collisionFilterGroup?: number /** * Set to `false` if you don't want the Ray to take `collisionResponse` flags into account on bodies and shapes. * @default true */ checkCollisionResponse?: boolean /** * callback */ callback?: RaycastCallback } export type RaycastCallback = (result: RaycastResult) => void /** * A line in 3D space that intersects bodies and return points. */ export class Ray { /** * from */ from: Vec3 /** * to */ to: Vec3 /** * direction */ direction: Vec3 /** * The precision of the ray. Used when checking parallelity etc. * @default 0.0001 */ precision: number /** * Set to `false` if you don't want the Ray to take `collisionResponse` flags into account on bodies and shapes. * @default true */ checkCollisionResponse: boolean /** * If set to `true`, the ray skips any hits with normal.dot(rayDirection) < 0. * @default false */ skipBackfaces: boolean /** * collisionFilterMask * @default -1 */ collisionFilterMask: number /** * collisionFilterGroup * @default -1 */ collisionFilterGroup: number /** * The intersection mode. Should be Ray.ANY, Ray.ALL or Ray.CLOSEST. * @default RAY.ANY */ mode: number /** * Current result object. */ result: RaycastResult /** * Will be set to `true` during intersectWorld() if the ray hit anything. */ hasHit: boolean /** * User-provided result callback. Will be used if mode is Ray.ALL. */ callback: RaycastCallback /** * CLOSEST */ static CLOSEST = RAY_MODES.CLOSEST /** * ANY */ static ANY = RAY_MODES.ANY /** * ALL */ static ALL = RAY_MODES.ALL get [Shape.types.SPHERE]() { return this._intersectSphere } get [Shape.types.PLANE]() { return this._intersectPlane } get [Shape.types.BOX]() { return this._intersectBox } get [Shape.types.CYLINDER]() { return this._intersectConvex } get [Shape.types.CONVEXPOLYHEDRON]() { return this._intersectConvex } get [Shape.types.HEIGHTFIELD]() { return this._intersectHeightfield } get [Shape.types.TRIMESH]() { return this._intersectTrimesh } constructor(from = new Vec3(), to = new Vec3()) { this.from = from.clone() this.to = to.clone() this.direction = new Vec3() this.precision = 0.0001 this.checkCollisionResponse = true this.skipBackfaces = false this.collisionFilterMask = -1 this.collisionFilterGroup = -1 this.mode = Ray.ANY this.result = new RaycastResult() this.hasHit = false this.callback = (result) => {} } /** * Do itersection against all bodies in the given World. * @return True if the ray hit anything, otherwise false. */ intersectWorld(world: World, options: RayOptions): boolean { this.mode = options.mode || Ray.ANY this.result = options.result || new RaycastResult() this.skipBackfaces = !!options.skipBackfaces this.collisionFilterMask = typeof options.collisionFilterMask !== 'undefined' ? options.collisionFilterMask : -1 this.collisionFilterGroup = typeof options.collisionFilterGroup !== 'undefined' ? options.collisionFilterGroup : -1 this.checkCollisionResponse = typeof options.checkCollisionResponse !== 'undefined' ? options.checkCollisionResponse : true if (options.from) { this.from.copy(options.from) } if (options.to) { this.to.copy(options.to) } this.callback = options.callback || (() => {}) this.hasHit = false this.result.reset() this.updateDirection() this.getAABB(tmpAABB) tmpArray.length = 0 world.broadphase.aabbQuery(world, tmpAABB, tmpArray) this.intersectBodies(tmpArray) return this.hasHit } /** * Shoot a ray at a body, get back information about the hit. * @deprecated @param result set the result property of the Ray instead. */ intersectBody(body: Body, result?: RaycastResult): void { if (result) { this.result = result this.updateDirection() } const checkCollisionResponse = this.checkCollisionResponse if (checkCollisionResponse && !body.collisionResponse) { return } if ( (this.collisionFilterGroup & body.collisionFilterMask) === 0 || (body.collisionFilterGroup & this.collisionFilterMask) === 0 ) { return } const xi = intersectBody_xi const qi = intersectBody_qi for (let i = 0, N = body.shapes.length; i < N; i++) { const shape = body.shapes[i] if (checkCollisionResponse && !shape.collisionResponse) { continue // Skip } body.quaternion.mult(body.shapeOrientations[i], qi) body.quaternion.vmult(body.shapeOffsets[i], xi) xi.vadd(body.position, xi) this.intersectShape(shape, qi, xi, body) if (this.result.shouldStop) { break } } } /** * Shoot a ray at an array bodies, get back information about the hit. * @param bodies An array of Body objects. * @deprecated @param result set the result property of the Ray instead. * */ intersectBodies(bodies: Body[], result?: RaycastResult): void { if (result) { this.result = result this.updateDirection() } for (let i = 0, l = bodies.length; !this.result.shouldStop && i < l; i++) { this.intersectBody(bodies[i]) } } /** * Updates the direction vector. */ private updateDirection(): void { this.to.vsub(this.from, this.direction) this.direction.normalize() } private intersectShape(shape: Shape, quat: Quaternion, position: Vec3, body: Body): void { const from = this.from // Checking boundingSphere const distance = distanceFromIntersection(from, this.direction, position) if (distance > shape.boundingSphereRadius) { return } const intersectMethod = this[shape.type as RayMode] as any if (intersectMethod) { intersectMethod.call(this, shape, quat, position, body, shape) } } _intersectBox(box: Box, quat: Quaternion, position: Vec3, body: Body, reportedShape: Shape): void { return this._intersectConvex(box.convexPolyhedronRepresentation, quat, position, body, reportedShape) } _intersectPlane(shape: Plane, quat: Quaternion, position: Vec3, body: Body, reportedShape: Shape): void { const from = this.from const to = this.to const direction = this.direction // Get plane normal const worldNormal = new Vec3(0, 0, 1) quat.vmult(worldNormal, worldNormal) const len = new Vec3() from.vsub(position, len) const planeToFrom = len.dot(worldNormal) to.vsub(position, len) const planeToTo = len.dot(worldNormal) if (planeToFrom * planeToTo > 0) { // "from" and "to" are on the same side of the plane... bail out return } if (from.distanceTo(to) < planeToFrom) { return } const n_dot_dir = worldNormal.dot(direction) if (Math.abs(n_dot_dir) < this.precision) { // No intersection return } const planePointToFrom = new Vec3() const dir_scaled_with_t = new Vec3() const hitPointWorld = new Vec3() from.vsub(position, planePointToFrom) const t = -worldNormal.dot(planePointToFrom) / n_dot_dir direction.scale(t, dir_scaled_with_t) from.vadd(dir_scaled_with_t, hitPointWorld) this.reportIntersection(worldNormal, hitPointWorld, reportedShape, body, -1) } /** * Get the world AABB of the ray. */ getAABB(aabb: AABB): void { const { lowerBound, upperBound } = aabb const to = this.to const from = this.from lowerBound.x = Math.min(to.x, from.x) lowerBound.y = Math.min(to.y, from.y) lowerBound.z = Math.min(to.z, from.z) upperBound.x = Math.max(to.x, from.x) upperBound.y = Math.max(to.y, from.y) upperBound.z = Math.max(to.z, from.z) } _intersectHeightfield(shape: Heightfield, quat: Quaternion, position: Vec3, body: Body, reportedShape: Shape): void { const data = shape.data const w = shape.elementSize // Convert the ray to local heightfield coordinates const localRay = intersectHeightfield_localRay //new Ray(this.from, this.to); localRay.from.copy(this.from) localRay.to.copy(this.to) Transform.pointToLocalFrame(position, quat, localRay.from, localRay.from) Transform.pointToLocalFrame(position, quat, localRay.to, localRay.to) localRay.updateDirection() // Get the index of the data points to test against const index = intersectHeightfield_index let iMinX let iMinY let iMaxX let iMaxY // Set to max iMinX = iMinY = 0 iMaxX = iMaxY = shape.data.length - 1 const aabb = new AABB() localRay.getAABB(aabb) shape.getIndexOfPosition(aabb.lowerBound.x, aabb.lowerBound.y, index, true) iMinX = Math.max(iMinX, index[0]) iMinY = Math.max(iMinY, index[1]) shape.getIndexOfPosition(aabb.upperBound.x, aabb.upperBound.y, index, true) iMaxX = Math.min(iMaxX, index[0] + 1) iMaxY = Math.min(iMaxY, index[1] + 1) for (let i = iMinX; i < iMaxX; i++) { for (let j = iMinY; j < iMaxY; j++) { if (this.result.shouldStop) { return } shape.getAabbAtIndex(i, j, aabb) if (!aabb.overlapsRay(localRay)) { continue } // Lower triangle shape.getConvexTrianglePillar(i, j, false) Transform.pointToWorldFrame(position, quat, shape.pillarOffset, worldPillarOffset) this._intersectConvex(shape.pillarConvex, quat, worldPillarOffset, body, reportedShape, intersectConvexOptions) if (this.result.shouldStop) { return } // Upper triangle shape.getConvexTrianglePillar(i, j, true) Transform.pointToWorldFrame(position, quat, shape.pillarOffset, worldPillarOffset) this._intersectConvex(shape.pillarConvex, quat, worldPillarOffset, body, reportedShape, intersectConvexOptions) } } } _intersectSphere(sphere: Sphere, quat: Quaternion, position: Vec3, body: Body, reportedShape: Shape): void { const from = this.from const to = this.to const r = sphere.radius const a = (to.x - from.x) ** 2 + (to.y - from.y) ** 2 + (to.z - from.z) ** 2 const b = 2 * ((to.x - from.x) * (from.x - position.x) + (to.y - from.y) * (from.y - position.y) + (to.z - from.z) * (from.z - position.z)) const c = (from.x - position.x) ** 2 + (from.y - position.y) ** 2 + (from.z - position.z) ** 2 - r ** 2 const delta = b ** 2 - 4 * a * c const intersectionPoint = Ray_intersectSphere_intersectionPoint const normal = Ray_intersectSphere_normal if (delta < 0) { // No intersection return } else if (delta === 0) { // single intersection point from.lerp(to, delta, intersectionPoint) intersectionPoint.vsub(position, normal) normal.normalize() this.reportIntersection(normal, intersectionPoint, reportedShape, body, -1) } else { const d1 = (-b - Math.sqrt(delta)) / (2 * a) const d2 = (-b + Math.sqrt(delta)) / (2 * a) if (d1 >= 0 && d1 <= 1) { from.lerp(to, d1, intersectionPoint) intersectionPoint.vsub(position, normal) normal.normalize() this.reportIntersection(normal, intersectionPoint, reportedShape, body, -1) } if (this.result.shouldStop) { return } if (d2 >= 0 && d2 <= 1) { from.lerp(to, d2, intersectionPoint) intersectionPoint.vsub(position, normal) normal.normalize() this.reportIntersection(normal, intersectionPoint, reportedShape, body, -1) } } } _intersectConvex( shape: ConvexPolyhedron, quat: Quaternion, position: Vec3, body: Body, reportedShape: Shape, options?: { faceList: number[] } ): void { const minDistNormal = intersectConvex_minDistNormal const normal = intersectConvex_normal const vector = intersectConvex_vector const minDistIntersect = intersectConvex_minDistIntersect const faceList = (options && options.faceList) || null // Checking faces const faces = shape.faces const vertices = shape.vertices const normals = shape.faceNormals const direction = this.direction const from = this.from const to = this.to const fromToDistance = from.distanceTo(to) const minDist = -1 const Nfaces = faceList ? faceList.length : faces.length const result = this.result for (let j = 0; !result.shouldStop && j < Nfaces; j++) { const fi = faceList ? faceList[j] : j const face = faces[fi] const faceNormal = normals[fi] const q = quat const x = position // determine if ray intersects the plane of the face // note: this works regardless of the direction of the face normal // Get plane point in world coordinates... vector.copy(vertices[face[0]]) q.vmult(vector, vector) vector.vadd(x, vector) // ...but make it relative to the ray from. We'll fix this later. vector.vsub(from, vector) // Get plane normal q.vmult(faceNormal, normal) // If this dot product is negative, we have something interesting const dot = direction.dot(normal) // Bail out if ray and plane are parallel if (Math.abs(dot) < this.precision) { continue } // calc distance to plane const scalar = normal.dot(vector) / dot // if negative distance, then plane is behind ray if (scalar < 0) { continue } // if (dot < 0) { // Intersection point is from + direction * scalar direction.scale(scalar, intersectPoint) intersectPoint.vadd(from, intersectPoint) // a is the point we compare points b and c with. a.copy(vertices[face[0]]) q.vmult(a, a) x.vadd(a, a) for (let i = 1; !result.shouldStop && i < face.length - 1; i++) { // Transform 3 vertices to world coords b.copy(vertices[face[i]]) c.copy(vertices[face[i + 1]]) q.vmult(b, b) q.vmult(c, c) x.vadd(b, b) x.vadd(c, c) const distance = intersectPoint.distanceTo(from) if ( !(Ray.pointInTriangle(intersectPoint, a, b, c) || Ray.pointInTriangle(intersectPoint, b, a, c)) || distance > fromToDistance ) { continue } this.reportIntersection(normal, intersectPoint, reportedShape, body, fi) } // } } } /** * @todo Optimize by transforming the world to local space first. * @todo Use Octree lookup */ _intersectTrimesh( mesh: Trimesh, quat: Quaternion, position: Vec3, body: Body, reportedShape: Shape, options?: { faceList?: any[] } ): void { const normal = intersectTrimesh_normal const triangles = intersectTrimesh_triangles const treeTransform = intersectTrimesh_treeTransform const vector = intersectConvex_vector const localDirection = intersectTrimesh_localDirection const localFrom = intersectTrimesh_localFrom const localTo = intersectTrimesh_localTo const worldIntersectPoint = intersectTrimesh_worldIntersectPoint const worldNormal = intersectTrimesh_worldNormal // Checking faces const indices = mesh.indices const vertices = mesh.vertices // const normals = mesh.faceNormals const from = this.from const to = this.to const direction = this.direction treeTransform.position.copy(position) treeTransform.quaternion.copy(quat) // Transform ray to local space! Transform.vectorToLocalFrame(position, quat, direction, localDirection) Transform.pointToLocalFrame(position, quat, from, localFrom) Transform.pointToLocalFrame(position, quat, to, localTo) localTo.x *= mesh.scale.x localTo.y *= mesh.scale.y localTo.z *= mesh.scale.z localFrom.x *= mesh.scale.x localFrom.y *= mesh.scale.y localFrom.z *= mesh.scale.z localTo.vsub(localFrom, localDirection) localDirection.normalize() const fromToDistanceSquared = localFrom.distanceSquared(localTo) mesh.tree.rayQuery(this, treeTransform, triangles) for (let i = 0, N = triangles.length; !this.result.shouldStop && i !== N; i++) { const trianglesIndex = triangles[i] mesh.getNormal(trianglesIndex, normal) // determine if ray intersects the plane of the face // note: this works regardless of the direction of the face normal // Get plane point in world coordinates... mesh.getVertex(indices[trianglesIndex * 3], a) // ...but make it relative to the ray from. We'll fix this later. a.vsub(localFrom, vector) // If this dot product is negative, we have something interesting const dot = localDirection.dot(normal) // Bail out if ray and plane are parallel // if (Math.abs( dot ) < this.precision){ // continue; // } // calc distance to plane const scalar = normal.dot(vector) / dot // if negative distance, then plane is behind ray if (scalar < 0) { continue } // Intersection point is from + direction * scalar localDirection.scale(scalar, intersectPoint) intersectPoint.vadd(localFrom, intersectPoint) // Get triangle vertices mesh.getVertex(indices[trianglesIndex * 3 + 1], b) mesh.getVertex(indices[trianglesIndex * 3 + 2], c) const squaredDistance = intersectPoint.distanceSquared(localFrom) if ( !(Ray.pointInTriangle(intersectPoint, b, a, c) || Ray.pointInTriangle(intersectPoint, a, b, c)) || squaredDistance > fromToDistanceSquared ) { continue } // transform intersectpoint and normal to world Transform.vectorToWorldFrame(quat, normal, worldNormal) Transform.pointToWorldFrame(position, quat, intersectPoint, worldIntersectPoint) this.reportIntersection(worldNormal, worldIntersectPoint, reportedShape, body, trianglesIndex) } triangles.length = 0 } /** * @return True if the intersections should continue */ private reportIntersection(normal: Vec3, hitPointWorld: Vec3, shape: Shape, body: Body, hitFaceIndex: number): void { const from = this.from const to = this.to const distance = from.distanceTo(hitPointWorld) const result = this.result // Skip back faces? if (this.skipBackfaces && normal.dot(this.direction) > 0) { return } result.hitFaceIndex = typeof hitFaceIndex !== 'undefined' ? hitFaceIndex : -1 switch (this.mode) { case Ray.ALL: this.hasHit = true result.set(from, to, normal, hitPointWorld, shape, body, distance) result.hasHit = true this.callback(result) break case Ray.CLOSEST: // Store if closer than current closest if (distance < result.distance || !result.hasHit) { this.hasHit = true result.hasHit = true result.set(from, to, normal, hitPointWorld, shape, body, distance) } break case Ray.ANY: // Report and stop. this.hasHit = true result.hasHit = true result.set(from, to, normal, hitPointWorld, shape, body, distance) result.shouldStop = true break } } /** * As per "Barycentric Technique" as named * {@link https://www.blackpawn.com/texts/pointinpoly/default.html here} but without the division */ static pointInTriangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3): boolean { c.vsub(a, v0) b.vsub(a, v1) p.vsub(a, v2) 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) let u let v return ( (u = dot11 * dot02 - dot01 * dot12) >= 0 && (v = dot00 * dot12 - dot01 * dot02) >= 0 && u + v < dot00 * dot11 - dot01 * dot01 ) } } const tmpAABB = new AABB() const tmpArray: Body[] = [] const v1 = new Vec3() const v2 = new Vec3() const intersectBody_xi = new Vec3() const intersectBody_qi = new Quaternion() const intersectPoint = new Vec3() const a = new Vec3() const b = new Vec3() const c = new Vec3() const d = new Vec3() const tmpRaycastResult = new RaycastResult() const intersectConvexOptions = { faceList: [0], } const worldPillarOffset = new Vec3() const intersectHeightfield_localRay = new Ray() const intersectHeightfield_index: number[] = [] const intersectHeightfield_minMax = [] const Ray_intersectSphere_intersectionPoint = new Vec3() const Ray_intersectSphere_normal = new Vec3() const intersectConvex_normal = new Vec3() const intersectConvex_minDistNormal = new Vec3() const intersectConvex_minDistIntersect = new Vec3() const intersectConvex_vector = new Vec3() const intersectTrimesh_normal = new Vec3() const intersectTrimesh_localDirection = new Vec3() const intersectTrimesh_localFrom = new Vec3() const intersectTrimesh_localTo = new Vec3() const intersectTrimesh_worldNormal = new Vec3() const intersectTrimesh_worldIntersectPoint = new Vec3() const intersectTrimesh_localAABB = new AABB() const intersectTrimesh_triangles: number[] = [] const intersectTrimesh_treeTransform = new Transform() const v0 = new Vec3() const intersect = new Vec3() function distanceFromIntersection(from: Vec3, direction: Vec3, position: Vec3): number { // v0 is vector from from to position position.vsub(from, v0) const dot = v0.dot(direction) // intersect = direction*dot + from direction.scale(dot, intersect) intersect.vadd(from, intersect) const distance = position.distanceTo(intersect) return distance }