@woosh/meep-engine 2.141.0 → 2.142.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +67 -42
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +340 -186
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
- package/src/engine/physics/PLAN.md +705 -578
- package/src/engine/physics/REVIEW_003.md +166 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +402 -165
- package/src/engine/physics/ecs/Joint.d.ts +115 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +168 -0
- package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
- package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
- package/src/engine/physics/queries/raycast.d.ts +11 -9
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +108 -159
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
- package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
2
|
+
import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
|
|
3
|
+
import { mesh_enumerate_triangles } from "./decomposition/mesh_enumerate_triangles.js";
|
|
4
|
+
import { heightmap_enumerate_triangles } from "./decomposition/heightmap_enumerate_triangles.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* # Ray vs a concave shape (mesh / heightmap)
|
|
8
|
+
*
|
|
9
|
+
* Refines a ray hit against a non-convex collider by triangle decomposition,
|
|
10
|
+
* mirroring the contact narrowphase's concave path. Working in the shape's
|
|
11
|
+
* local frame:
|
|
12
|
+
* 1. clip the ray to the shape's local bounding box (a quick reject, and a
|
|
13
|
+
* finite query segment even when `tMax` is unbounded);
|
|
14
|
+
* 2. enumerate the triangles overlapping that segment's AABB
|
|
15
|
+
* (`mesh_enumerate_triangles` / `heightmap_enumerate_triangles`);
|
|
16
|
+
* 3. two-sided Möller–Trumbore each, keeping the nearest crossing;
|
|
17
|
+
* 4. return its distance and the world surface normal (the triangle's
|
|
18
|
+
* geometric normal, oriented to face the ray).
|
|
19
|
+
*
|
|
20
|
+
* The triangle scratch grows on saturation and is bounded by {@link MAX_TRIS}
|
|
21
|
+
* (a ray crossing more triangles than that in one mesh is not a realistic
|
|
22
|
+
* raycast — it would mean skewering a 65k-triangle mesh lengthwise).
|
|
23
|
+
*
|
|
24
|
+
* @author Alex Goldring
|
|
25
|
+
* @copyright Company Named Limited (c) 2026
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const lo = new Float64Array(3); // ray origin, shape-local
|
|
29
|
+
const ld = new Float64Array(3); // ray direction, shape-local
|
|
30
|
+
const bounds = new Float64Array(6); // shape local AABB
|
|
31
|
+
|
|
32
|
+
const INITIAL_TRIS = 1024;
|
|
33
|
+
const MAX_TRIS = 1 << 16;
|
|
34
|
+
let tri_buf = new Float64Array(INITIAL_TRIS * TRIANGLE_FLOAT_STRIDE);
|
|
35
|
+
|
|
36
|
+
const EPS = 1e-12;
|
|
37
|
+
const AABB_FATTEN = 1e-5; // guards triangles whose AABB just abuts the segment
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {AbstractShape3D} shape a concave shape (`is_convex === false`)
|
|
41
|
+
* @param {{x:number,y:number,z:number}} position
|
|
42
|
+
* @param {ArrayLike<number>} rotation body rotation quaternion (x,y,z,w)
|
|
43
|
+
* @param {number} ox @param {number} oy @param {number} oz ray origin (world)
|
|
44
|
+
* @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
|
|
45
|
+
* @param {number} tMax
|
|
46
|
+
* @param {Float64Array|Vector3} outNormal world surface normal, written on hit
|
|
47
|
+
* @returns {number} hit distance, or `Infinity` on miss
|
|
48
|
+
*/
|
|
49
|
+
export function refine_ray_concave(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
|
|
50
|
+
const qx = rotation[0], qy = rotation[1], qz = rotation[2], qw = rotation[3];
|
|
51
|
+
// World → local (inverse body rotation; unit dir stays unit → t preserved).
|
|
52
|
+
v3_quat3_apply(lo, 0, ox - position.x, oy - position.y, oz - position.z, -qx, -qy, -qz, qw);
|
|
53
|
+
v3_quat3_apply(ld, 0, dx, dy, dz, -qx, -qy, -qz, qw);
|
|
54
|
+
const lox = lo[0], loy = lo[1], loz = lo[2];
|
|
55
|
+
const ldx = ld[0], ldy = ld[1], ldz = ld[2];
|
|
56
|
+
|
|
57
|
+
// Clip the local ray to the shape's local AABB → segment [tEnter, tExit].
|
|
58
|
+
shape.compute_bounding_box(bounds);
|
|
59
|
+
let tEnter = 0, tExit = tMax;
|
|
60
|
+
if (ldx > EPS || ldx < -EPS) {
|
|
61
|
+
const inv = 1 / ldx;
|
|
62
|
+
let t1 = (bounds[0] - lox) * inv, t2 = (bounds[3] - lox) * inv;
|
|
63
|
+
if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
|
|
64
|
+
if (t1 > tEnter) tEnter = t1;
|
|
65
|
+
if (t2 < tExit) tExit = t2;
|
|
66
|
+
} else if (lox < bounds[0] || lox > bounds[3]) return Infinity;
|
|
67
|
+
if (ldy > EPS || ldy < -EPS) {
|
|
68
|
+
const inv = 1 / ldy;
|
|
69
|
+
let t1 = (bounds[1] - loy) * inv, t2 = (bounds[4] - loy) * inv;
|
|
70
|
+
if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
|
|
71
|
+
if (t1 > tEnter) tEnter = t1;
|
|
72
|
+
if (t2 < tExit) tExit = t2;
|
|
73
|
+
} else if (loy < bounds[1] || loy > bounds[4]) return Infinity;
|
|
74
|
+
if (ldz > EPS || ldz < -EPS) {
|
|
75
|
+
const inv = 1 / ldz;
|
|
76
|
+
let t1 = (bounds[2] - loz) * inv, t2 = (bounds[5] - loz) * inv;
|
|
77
|
+
if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
|
|
78
|
+
if (t1 > tEnter) tEnter = t1;
|
|
79
|
+
if (t2 < tExit) tExit = t2;
|
|
80
|
+
} else if (loz < bounds[2] || loz > bounds[5]) return Infinity;
|
|
81
|
+
if (tExit < tEnter) return Infinity; // ray misses the shape's AABB entirely
|
|
82
|
+
|
|
83
|
+
// Query AABB = bbox of the clipped local segment.
|
|
84
|
+
const sax = lox + ldx * tEnter, say = loy + ldy * tEnter, saz = loz + ldz * tEnter;
|
|
85
|
+
const sbx = lox + ldx * tExit, sby = loy + ldy * tExit, sbz = loz + ldz * tExit;
|
|
86
|
+
const qminx = (sax < sbx ? sax : sbx) - AABB_FATTEN;
|
|
87
|
+
const qminy = (say < sby ? say : sby) - AABB_FATTEN;
|
|
88
|
+
const qminz = (saz < sbz ? saz : sbz) - AABB_FATTEN;
|
|
89
|
+
const qmaxx = (sax > sbx ? sax : sbx) + AABB_FATTEN;
|
|
90
|
+
const qmaxy = (say > sby ? say : sby) + AABB_FATTEN;
|
|
91
|
+
const qmaxz = (saz > sbz ? saz : sbz) + AABB_FATTEN;
|
|
92
|
+
|
|
93
|
+
const enumerate = shape.isHeightMapShape3D === true ? heightmap_enumerate_triangles : mesh_enumerate_triangles;
|
|
94
|
+
let cap = (tri_buf.length / TRIANGLE_FLOAT_STRIDE) | 0;
|
|
95
|
+
let count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
|
|
96
|
+
while (count === cap && cap < MAX_TRIS) {
|
|
97
|
+
cap = cap * 2 < MAX_TRIS ? cap * 2 : MAX_TRIS;
|
|
98
|
+
tri_buf = new Float64Array(cap * TRIANGLE_FLOAT_STRIDE);
|
|
99
|
+
count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Two-sided Möller–Trumbore over the candidates; keep the nearest.
|
|
103
|
+
let best = tMax;
|
|
104
|
+
let found = false;
|
|
105
|
+
let nx = 0, ny = 0, nz = 0;
|
|
106
|
+
for (let i = 0; i < count; i++) {
|
|
107
|
+
const o = i * TRIANGLE_FLOAT_STRIDE;
|
|
108
|
+
const ax = tri_buf[o], ay = tri_buf[o + 1], az = tri_buf[o + 2];
|
|
109
|
+
const e1x = tri_buf[o + 3] - ax, e1y = tri_buf[o + 4] - ay, e1z = tri_buf[o + 5] - az;
|
|
110
|
+
const e2x = tri_buf[o + 6] - ax, e2y = tri_buf[o + 7] - ay, e2z = tri_buf[o + 8] - az;
|
|
111
|
+
|
|
112
|
+
const px = ldy * e2z - ldz * e2y, py = ldz * e2x - ldx * e2z, pz = ldx * e2y - ldy * e2x;
|
|
113
|
+
const det = e1x * px + e1y * py + e1z * pz;
|
|
114
|
+
if (det < EPS && det > -EPS) continue; // ray parallel to the triangle
|
|
115
|
+
const invDet = 1 / det;
|
|
116
|
+
|
|
117
|
+
const tvx = lox - ax, tvy = loy - ay, tvz = loz - az;
|
|
118
|
+
const u = (tvx * px + tvy * py + tvz * pz) * invDet;
|
|
119
|
+
if (u < 0 || u > 1) continue;
|
|
120
|
+
|
|
121
|
+
const wx = tvy * e1z - tvz * e1y, wy = tvz * e1x - tvx * e1z, wz = tvx * e1y - tvy * e1x;
|
|
122
|
+
const v = (ldx * wx + ldy * wy + ldz * wz) * invDet;
|
|
123
|
+
if (v < 0 || u + v > 1) continue;
|
|
124
|
+
|
|
125
|
+
const t = (e2x * wx + e2y * wy + e2z * wz) * invDet;
|
|
126
|
+
if (t < 0 || t >= best) continue;
|
|
127
|
+
|
|
128
|
+
best = t;
|
|
129
|
+
found = true;
|
|
130
|
+
// Geometric normal e1 × e2 (local, unnormalised).
|
|
131
|
+
nx = e1y * e2z - e1z * e2y;
|
|
132
|
+
ny = e1z * e2x - e1x * e2z;
|
|
133
|
+
nz = e1x * e2y - e1y * e2x;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!found) return Infinity;
|
|
137
|
+
|
|
138
|
+
// Normalise, orient to face the ray, then rotate the local normal to world.
|
|
139
|
+
let nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
140
|
+
if (nl === 0) nl = 1;
|
|
141
|
+
nx /= nl; ny /= nl; nz /= nl;
|
|
142
|
+
if (nx * ldx + ny * ldy + nz * ldz > 0) { nx = -nx; ny = -ny; nz = -nz; }
|
|
143
|
+
v3_quat3_apply(outNormal, 0, nx, ny, nz, qx, qy, qz, qw);
|
|
144
|
+
return best;
|
|
145
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refine a broadphase ray hit against a body's true shape geometry.
|
|
3
|
+
*
|
|
4
|
+
* Transforms the world ray into the shape's local frame once (rotate by the
|
|
5
|
+
* inverse body rotation — a unit direction stays unit, so the hit distance is
|
|
6
|
+
* preserved), runs the matching canonical primitive ({@link ray_shapes}), and
|
|
7
|
+
* rotates the returned local normal back to world. Spheres are
|
|
8
|
+
* rotation-invariant, so they only need the translation.
|
|
9
|
+
*
|
|
10
|
+
* Handles the convex primitives (sphere / box / capsule). Concave shapes
|
|
11
|
+
* (mesh / heightmap) and arbitrary composite convex shapes return
|
|
12
|
+
* {@link RAY_REFINE_UNSUPPORTED} — the concave path is wired in separately, and
|
|
13
|
+
* composites fall back to the broadphase AABB hit (no regression).
|
|
14
|
+
*
|
|
15
|
+
* @param {AbstractShape3D} shape
|
|
16
|
+
* @param {{x:number,y:number,z:number}} position body world position
|
|
17
|
+
* @param {ArrayLike<number>} rotation body world rotation quaternion (x,y,z,w)
|
|
18
|
+
* @param {number} ox @param {number} oy @param {number} oz ray origin (world)
|
|
19
|
+
* @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
|
|
20
|
+
* @param {number} tMax
|
|
21
|
+
* @param {Float64Array|Vector3} outNormal world surface normal, written on a
|
|
22
|
+
* finite hit
|
|
23
|
+
* @returns {number} hit distance, `Infinity` on a refined miss, or
|
|
24
|
+
* {@link RAY_REFINE_UNSUPPORTED} when the shape has no exact ray test here
|
|
25
|
+
*/
|
|
26
|
+
export function refine_ray_hit(shape: AbstractShape3D, position: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
z: number;
|
|
30
|
+
}, rotation: ArrayLike<number>, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tMax: number, outNormal: Float64Array | Vector3): number;
|
|
31
|
+
/**
|
|
32
|
+
* Sentinel returned by {@link refine_ray_hit} for a shape it has no exact ray
|
|
33
|
+
* test for (composite / arbitrary convex). The caller should keep the
|
|
34
|
+
* broadphase AABB result for that leaf rather than treat it as a miss.
|
|
35
|
+
* Distinct from `Infinity` (a refined miss) and any finite hit distance.
|
|
36
|
+
* @type {number}
|
|
37
|
+
*/
|
|
38
|
+
export const RAY_REFINE_UNSUPPORTED: number;
|
|
39
|
+
//# sourceMappingURL=refine_ray_hit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refine_ray_hit.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/refine_ray_hit.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,iEAVW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAC5B,UAAU,MAAM,CAAC,MACjB,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,MAAa,MAAM,MAAa,MAAM,QAC5C,MAAM,aACN,YAAY,UAAQ,GAElB,MAAM,CAsClB;AAzED;;;;;;GAMG;AACH,qCAFU,MAAM,CAEyB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
2
|
+
import { ray_sphere_local, ray_box_local, ray_capsule_local } from "./ray_shapes.js";
|
|
3
|
+
import { refine_ray_concave } from "./refine_ray_concave.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sentinel returned by {@link refine_ray_hit} for a shape it has no exact ray
|
|
7
|
+
* test for (composite / arbitrary convex). The caller should keep the
|
|
8
|
+
* broadphase AABB result for that leaf rather than treat it as a miss.
|
|
9
|
+
* Distinct from `Infinity` (a refined miss) and any finite hit distance.
|
|
10
|
+
* @type {number}
|
|
11
|
+
*/
|
|
12
|
+
export const RAY_REFINE_UNSUPPORTED = -1;
|
|
13
|
+
|
|
14
|
+
const lo = new Float64Array(3); // ray origin in shape-local frame
|
|
15
|
+
const ld = new Float64Array(3); // ray direction in shape-local frame
|
|
16
|
+
const ln = new Float64Array(3); // surface normal in shape-local frame
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Refine a broadphase ray hit against a body's true shape geometry.
|
|
20
|
+
*
|
|
21
|
+
* Transforms the world ray into the shape's local frame once (rotate by the
|
|
22
|
+
* inverse body rotation — a unit direction stays unit, so the hit distance is
|
|
23
|
+
* preserved), runs the matching canonical primitive ({@link ray_shapes}), and
|
|
24
|
+
* rotates the returned local normal back to world. Spheres are
|
|
25
|
+
* rotation-invariant, so they only need the translation.
|
|
26
|
+
*
|
|
27
|
+
* Handles the convex primitives (sphere / box / capsule). Concave shapes
|
|
28
|
+
* (mesh / heightmap) and arbitrary composite convex shapes return
|
|
29
|
+
* {@link RAY_REFINE_UNSUPPORTED} — the concave path is wired in separately, and
|
|
30
|
+
* composites fall back to the broadphase AABB hit (no regression).
|
|
31
|
+
*
|
|
32
|
+
* @param {AbstractShape3D} shape
|
|
33
|
+
* @param {{x:number,y:number,z:number}} position body world position
|
|
34
|
+
* @param {ArrayLike<number>} rotation body world rotation quaternion (x,y,z,w)
|
|
35
|
+
* @param {number} ox @param {number} oy @param {number} oz ray origin (world)
|
|
36
|
+
* @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
|
|
37
|
+
* @param {number} tMax
|
|
38
|
+
* @param {Float64Array|Vector3} outNormal world surface normal, written on a
|
|
39
|
+
* finite hit
|
|
40
|
+
* @returns {number} hit distance, `Infinity` on a refined miss, or
|
|
41
|
+
* {@link RAY_REFINE_UNSUPPORTED} when the shape has no exact ray test here
|
|
42
|
+
*/
|
|
43
|
+
export function refine_ray_hit(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
|
|
44
|
+
if (shape.isUnitSphereShape3D === true) {
|
|
45
|
+
// Rotation-invariant: translate into the sphere's frame; the local
|
|
46
|
+
// normal is already the world normal.
|
|
47
|
+
return ray_sphere_local(outNormal, ox - position.x, oy - position.y, oz - position.z, dx, dy, dz, tMax, 1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (shape.isBoxShape3D === true || shape.isCapsuleShape3D === true) {
|
|
51
|
+
const qx = rotation[0], qy = rotation[1], qz = rotation[2], qw = rotation[3];
|
|
52
|
+
// World → local: rotate by the inverse (conjugate) body rotation.
|
|
53
|
+
v3_quat3_apply(lo, 0, ox - position.x, oy - position.y, oz - position.z, -qx, -qy, -qz, qw);
|
|
54
|
+
v3_quat3_apply(ld, 0, dx, dy, dz, -qx, -qy, -qz, qw);
|
|
55
|
+
|
|
56
|
+
let t;
|
|
57
|
+
if (shape.isBoxShape3D === true) {
|
|
58
|
+
const h = shape.half_extents;
|
|
59
|
+
t = ray_box_local(ln, lo[0], lo[1], lo[2], ld[0], ld[1], ld[2], tMax, h.x, h.y, h.z);
|
|
60
|
+
} else {
|
|
61
|
+
t = ray_capsule_local(ln, lo[0], lo[1], lo[2], ld[0], ld[1], ld[2], tMax, shape.radius, shape.height * 0.5);
|
|
62
|
+
}
|
|
63
|
+
if (t === Infinity) return Infinity;
|
|
64
|
+
|
|
65
|
+
// Local normal → world: rotate by the body rotation.
|
|
66
|
+
v3_quat3_apply(outNormal, 0, ln[0], ln[1], ln[2], qx, qy, qz, qw);
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Concave (mesh / heightmap): triangle decomposition.
|
|
71
|
+
if (shape.is_convex === false) {
|
|
72
|
+
return refine_ray_concave(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Arbitrary composite convex (TransformedShape3D / UnionShape3D / …):
|
|
76
|
+
// no exact ray test yet — caller falls back to the broadphase AABB hit.
|
|
77
|
+
return RAY_REFINE_UNSUPPORTED;
|
|
78
|
+
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Raycast against both broadphase trees (static + dynamic) of a
|
|
3
|
-
* {@link PhysicsSystem},
|
|
4
|
-
* `true` on hit, `false` on
|
|
3
|
+
* {@link PhysicsSystem}, refined against each candidate's true shape geometry.
|
|
4
|
+
* Fills `result` with the nearest hit and returns `true` on hit, `false` on
|
|
5
|
+
* miss. `result.t` is the exact surface distance and `result.normal` the true
|
|
6
|
+
* surface normal for sphere / box / capsule / mesh / heightmap colliders;
|
|
7
|
+
* composite convex shapes (no exact ray test yet) fall back to the broadphase
|
|
8
|
+
* AABB hit + AABB-face normal.
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* call site, no API change).
|
|
10
|
+
* Multi-collider bodies resolve their primary (first-attached) collider — the
|
|
11
|
+
* BVH leaf encodes only `body_id`; per-collider rays need the leaf user-data to
|
|
12
|
+
* carry the collider index (future work).
|
|
10
13
|
*
|
|
11
14
|
* @param {PhysicsSystem} system
|
|
12
15
|
* @param {Ray3} ray origin + unit direction + `tMax`
|
|
13
16
|
* @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
|
|
14
|
-
* @param {(entity:number, collider:Collider)=>boolean} [filter]
|
|
15
|
-
*
|
|
16
|
-
* per BVH leaf that crosses the ray.
|
|
17
|
+
* @param {(entity:number, collider:Collider)=>boolean} [filter] called once per
|
|
18
|
+
* crossing leaf; defaults to {@link returnTrue}.
|
|
17
19
|
* @returns {boolean} true on hit, false on miss
|
|
18
20
|
*/
|
|
19
21
|
export function raycast(system: PhysicsSystem, ray: Ray3, result: PhysicsSurfacePoint, filter?: (entity: number, collider: Collider) => boolean): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"raycast.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/raycast.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"raycast.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/raycast.js"],"names":[],"mappings":"AA2IA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,yGAJmB,MAAM,yBAAsB,OAAO,GAEzC,OAAO,CAwCnB"}
|
|
@@ -8,42 +8,67 @@ import {
|
|
|
8
8
|
} from "../../../core/bvh2/bvh3/BVH.js";
|
|
9
9
|
import { returnTrue } from "../../../core/function/returnTrue.js";
|
|
10
10
|
import { aabb3_near_distance_to_intersection_ray_segment } from "../../../core/geom/3d/aabb/aabb3_near_distance_to_intersection_ray_segment.js";
|
|
11
|
+
import { refine_ray_hit, RAY_REFINE_UNSUPPORTED } from "../narrowphase/refine_ray_hit.js";
|
|
11
12
|
|
|
12
13
|
const stack = SCRATCH_UINT32_TRAVERSAL_STACK;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Reusable nearest-hit accumulator + scratch. Module-scoped so {@link raycast}
|
|
17
|
+
* doesn't allocate per call; the physics step is single-threaded so contention
|
|
18
|
+
* isn't a concern.
|
|
19
|
+
* @type {{best_t:number, best_body:number}}
|
|
20
|
+
*/
|
|
21
|
+
const acc = { best_t: Infinity, best_body: 0 };
|
|
22
|
+
const best_normal = new Float64Array(3); // winning hit's world normal
|
|
23
|
+
const cand_normal = new Float64Array(3); // per-leaf candidate normal
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Outward AABB-face normal at a hit point — the fallback normal for a leaf
|
|
27
|
+
* whose shape has no exact ray test (composite convex). The hit point is
|
|
28
|
+
* projected into the AABB's normalised local space (centre = 0, faces = ±1);
|
|
29
|
+
* the dominant component's axis is the entry face and its sign the outward
|
|
30
|
+
* normal.
|
|
31
|
+
*/
|
|
32
|
+
function aabb_face_normal(out, min_x, min_y, min_z, max_x, max_y, max_z, hx, hy, hz) {
|
|
33
|
+
const cx = (min_x + max_x) * 0.5;
|
|
34
|
+
const cy = (min_y + max_y) * 0.5;
|
|
35
|
+
const cz = (min_z + max_z) * 0.5;
|
|
36
|
+
const px = (hx - cx) * 2 / (max_x - min_x);
|
|
37
|
+
const py = (hy - cy) * 2 / (max_y - min_y);
|
|
38
|
+
const pz = (hz - cz) * 2 / (max_z - min_z);
|
|
39
|
+
const apx = px < 0 ? -px : px;
|
|
40
|
+
const apy = py < 0 ? -py : py;
|
|
41
|
+
const apz = pz < 0 ? -pz : pz;
|
|
42
|
+
if (apx >= apy && apx >= apz) { out[0] = px >= 0 ? 1 : -1; out[1] = 0; out[2] = 0; }
|
|
43
|
+
else if (apy >= apz) { out[0] = 0; out[1] = py >= 0 ? 1 : -1; out[2] = 0; }
|
|
44
|
+
else { out[0] = 0; out[1] = 0; out[2] = pz >= 0 ? 1 : -1; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Walk a single BVH along a ray, refining each crossing leaf against its true
|
|
49
|
+
* shape geometry and keeping the nearest confirmed hit.
|
|
16
50
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
51
|
+
* Pruning stays on the *inflated leaf AABB* entry distance `t_near`: a shape
|
|
52
|
+
* hit is always at or beyond its tight AABB entry, which is at or beyond the
|
|
53
|
+
* inflated-AABB entry, so a subtree whose AABB entry is past the current best
|
|
54
|
+
* cannot contain a closer hit. A leaf whose ray crosses the fat AABB but misses
|
|
55
|
+
* the true shape contributes nothing — the correctness gain over broadphase.
|
|
19
56
|
*
|
|
20
|
-
* @param {BVH} bvh
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {number} ox
|
|
23
|
-
* @param {number}
|
|
24
|
-
* @param {number}
|
|
25
|
-
* @param {number} dx
|
|
26
|
-
* @param {number} dy
|
|
27
|
-
* @param {number} dz
|
|
28
|
-
* @param {number} inv_dx
|
|
29
|
-
* @param {number} inv_dy
|
|
30
|
-
* @param {number} inv_dz
|
|
57
|
+
* @param {BVH} bvh @param {number} root
|
|
58
|
+
* @param {PhysicsSystem} system
|
|
59
|
+
* @param {number} ox @param {number} oy @param {number} oz ray origin
|
|
60
|
+
* @param {number} dx @param {number} dy @param {number} dz ray dir (unit)
|
|
61
|
+
* @param {number} inv_dx @param {number} inv_dy @param {number} inv_dz
|
|
31
62
|
* @param {number} max_distance
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {(body_id:number)=>boolean} test per-leaf filter
|
|
63
|
+
* @param {(entity:number, collider:Collider)=>boolean} filter
|
|
34
64
|
*/
|
|
35
65
|
function bvh_raycast_nearest(
|
|
36
|
-
bvh, root,
|
|
37
|
-
ox, oy, oz,
|
|
38
|
-
dx, dy, dz,
|
|
66
|
+
bvh, root, system,
|
|
67
|
+
ox, oy, oz, dx, dy, dz,
|
|
39
68
|
inv_dx, inv_dy, inv_dz,
|
|
40
|
-
max_distance,
|
|
41
|
-
acc,
|
|
42
|
-
test
|
|
69
|
+
max_distance, filter
|
|
43
70
|
) {
|
|
44
|
-
if (root === NULL_NODE)
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
71
|
+
if (root === NULL_NODE) return;
|
|
47
72
|
|
|
48
73
|
const float32 = bvh.__data_float32;
|
|
49
74
|
const uint32 = bvh.__data_uint32;
|
|
@@ -66,181 +91,105 @@ function bvh_raycast_nearest(
|
|
|
66
91
|
);
|
|
67
92
|
|
|
68
93
|
// No intersection or this subtree can't beat the best known hit — prune.
|
|
69
|
-
if (t_near >= acc.best_t)
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
94
|
+
if (t_near >= acc.best_t) continue;
|
|
72
95
|
|
|
73
96
|
const child_1 = uint32[address + COLUMN_CHILD_1];
|
|
74
97
|
if (child_1 !== NULL_NODE) {
|
|
75
98
|
stack[pointer++] = uint32[address + COLUMN_CHILD_2];
|
|
76
99
|
stack[pointer++] = child_1;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Leaf — refine against the true shape.
|
|
104
|
+
const body_id = uint32[address + COLUMN_USER_DATA];
|
|
105
|
+
const entity = system.entityOf(body_id);
|
|
106
|
+
if (entity < 0) continue; // unlinked concurrently
|
|
107
|
+
const idx = system.__index_of(body_id);
|
|
108
|
+
const collider = system.__primary_collider(idx);
|
|
109
|
+
if (collider === null) continue;
|
|
110
|
+
if (!filter(entity, collider)) continue;
|
|
111
|
+
|
|
112
|
+
const tr = system.__transforms[idx];
|
|
113
|
+
const refined = refine_ray_hit(
|
|
114
|
+
collider.shape, tr.position, tr.rotation,
|
|
115
|
+
ox, oy, oz, dx, dy, dz, acc.best_t, cand_normal
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (refined === RAY_REFINE_UNSUPPORTED) {
|
|
119
|
+
// No exact ray test for this shape — keep the broadphase AABB hit.
|
|
120
|
+
if (t_near < acc.best_t) {
|
|
80
121
|
acc.best_t = t_near;
|
|
81
122
|
acc.best_body = body_id;
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
aabb_face_normal(best_normal,
|
|
124
|
+
float32[address], float32[address + 1], float32[address + 2],
|
|
125
|
+
float32[address + 3], float32[address + 4], float32[address + 5],
|
|
126
|
+
ox + dx * t_near, oy + dy * t_near, oz + dz * t_near);
|
|
84
127
|
}
|
|
128
|
+
} else if (refined < acc.best_t) { // a refined miss is Infinity → never wins
|
|
129
|
+
acc.best_t = refined;
|
|
130
|
+
acc.best_body = body_id;
|
|
131
|
+
best_normal[0] = cand_normal[0];
|
|
132
|
+
best_normal[1] = cand_normal[1];
|
|
133
|
+
best_normal[2] = cand_normal[2];
|
|
85
134
|
}
|
|
86
135
|
}
|
|
87
136
|
|
|
88
137
|
stack.pointer = stack_top;
|
|
89
138
|
}
|
|
90
139
|
|
|
91
|
-
/**
|
|
92
|
-
* Reusable nearest-hit accumulator. Module-scoped so {@link raycast} doesn't
|
|
93
|
-
* allocate per call. The physics step is single-threaded so contention isn't
|
|
94
|
-
* a concern.
|
|
95
|
-
* @type {{best_t:number, best_body:number, best_node:number, best_bvh:BVH|null}}
|
|
96
|
-
*/
|
|
97
|
-
const acc = { best_t: Infinity, best_body: 0, best_node: 0, best_bvh: null };
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Reusable closure bound to the current system + user filter so we don't
|
|
101
|
-
* allocate a new function object per call. Re-wired at the top of each
|
|
102
|
-
* {@link raycast} invocation. The BVH traversal sees a stable identity
|
|
103
|
-
* which helps V8 keep the call site monomorphic.
|
|
104
|
-
*
|
|
105
|
-
* Carries state via the module-scoped `bound_*` slots below.
|
|
106
|
-
* @type {(body_id:number)=>boolean}
|
|
107
|
-
*/
|
|
108
|
-
const bound_test = (body_id) => {
|
|
109
|
-
const system = bound_system;
|
|
110
|
-
const entity = system.entityOf(body_id);
|
|
111
|
-
if (entity < 0) return false;
|
|
112
|
-
// v1 limitation: when a multi-collider body's BVH leaf is hit, we pass
|
|
113
|
-
// the body's primary (first-attached) collider rather than the specific
|
|
114
|
-
// collider the ray actually crossed. Sufficient for entity-level
|
|
115
|
-
// filtering; per-collider filtering needs the BVH user_data scheme to
|
|
116
|
-
// encode collider index too — future work.
|
|
117
|
-
const collider = system.__primary_collider(system.__index_of(body_id));
|
|
118
|
-
return bound_filter(entity, collider);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
let bound_system = null;
|
|
122
|
-
let bound_filter = returnTrue;
|
|
123
|
-
|
|
124
140
|
/**
|
|
125
141
|
* Raycast against both broadphase trees (static + dynamic) of a
|
|
126
|
-
* {@link PhysicsSystem},
|
|
127
|
-
* `true` on hit, `false` on
|
|
142
|
+
* {@link PhysicsSystem}, refined against each candidate's true shape geometry.
|
|
143
|
+
* Fills `result` with the nearest hit and returns `true` on hit, `false` on
|
|
144
|
+
* miss. `result.t` is the exact surface distance and `result.normal` the true
|
|
145
|
+
* surface normal for sphere / box / capsule / mesh / heightmap colliders;
|
|
146
|
+
* composite convex shapes (no exact ray test yet) fall back to the broadphase
|
|
147
|
+
* AABB hit + AABB-face normal.
|
|
128
148
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* call site, no API change).
|
|
149
|
+
* Multi-collider bodies resolve their primary (first-attached) collider — the
|
|
150
|
+
* BVH leaf encodes only `body_id`; per-collider rays need the leaf user-data to
|
|
151
|
+
* carry the collider index (future work).
|
|
133
152
|
*
|
|
134
153
|
* @param {PhysicsSystem} system
|
|
135
154
|
* @param {Ray3} ray origin + unit direction + `tMax`
|
|
136
155
|
* @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
|
|
137
|
-
* @param {(entity:number, collider:Collider)=>boolean} [filter]
|
|
138
|
-
*
|
|
139
|
-
* per BVH leaf that crosses the ray.
|
|
156
|
+
* @param {(entity:number, collider:Collider)=>boolean} [filter] called once per
|
|
157
|
+
* crossing leaf; defaults to {@link returnTrue}.
|
|
140
158
|
* @returns {boolean} true on hit, false on miss
|
|
141
159
|
*/
|
|
142
160
|
export function raycast(system, ray, result, filter = returnTrue) {
|
|
143
|
-
const ox = ray.origin_x;
|
|
144
|
-
const
|
|
145
|
-
const oz = ray.origin_z;
|
|
146
|
-
const dx = ray.direction_x;
|
|
147
|
-
const dy = ray.direction_y;
|
|
148
|
-
const dz = ray.direction_z;
|
|
161
|
+
const ox = ray.origin_x, oy = ray.origin_y, oz = ray.origin_z;
|
|
162
|
+
const dx = ray.direction_x, dy = ray.direction_y, dz = ray.direction_z;
|
|
149
163
|
const max_distance = ray.tMax;
|
|
150
164
|
|
|
151
165
|
acc.best_t = max_distance;
|
|
152
166
|
acc.best_body = 0;
|
|
153
|
-
|
|
154
|
-
acc.best_bvh = null;
|
|
167
|
+
best_normal[0] = 0; best_normal[1] = 0; best_normal[2] = 0;
|
|
155
168
|
|
|
156
|
-
|
|
157
|
-
bound_filter = filter;
|
|
158
|
-
|
|
159
|
-
const inv_dx = 1 / dx;
|
|
160
|
-
const inv_dy = 1 / dy;
|
|
161
|
-
const inv_dz = 1 / dz;
|
|
169
|
+
const inv_dx = 1 / dx, inv_dy = 1 / dy, inv_dz = 1 / dz;
|
|
162
170
|
|
|
163
171
|
bvh_raycast_nearest(
|
|
164
|
-
system.staticBvh, system.staticBvh.root,
|
|
165
|
-
ox, oy, oz,
|
|
166
|
-
dx, dy, dz,
|
|
167
|
-
inv_dx, inv_dy, inv_dz,
|
|
168
|
-
max_distance,
|
|
169
|
-
acc, bound_test
|
|
172
|
+
system.staticBvh, system.staticBvh.root, system,
|
|
173
|
+
ox, oy, oz, dx, dy, dz, inv_dx, inv_dy, inv_dz, max_distance, filter
|
|
170
174
|
);
|
|
171
|
-
|
|
172
175
|
bvh_raycast_nearest(
|
|
173
|
-
system.dynamicBvh, system.dynamicBvh.root,
|
|
174
|
-
ox, oy, oz,
|
|
175
|
-
dx, dy, dz,
|
|
176
|
-
inv_dx, inv_dy, inv_dz,
|
|
177
|
-
max_distance,
|
|
178
|
-
acc, bound_test
|
|
176
|
+
system.dynamicBvh, system.dynamicBvh.root, system,
|
|
177
|
+
ox, oy, oz, dx, dy, dz, inv_dx, inv_dy, inv_dz, max_distance, filter
|
|
179
178
|
);
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
// Any hit updates best_t to strictly below max_distance (both the refined
|
|
181
|
+
// and AABB-fallback paths require t < current best); a miss leaves it at
|
|
182
|
+
// max_distance. This is body_id-agnostic — the first body packs to id 0.
|
|
183
|
+
if (acc.best_t >= max_distance) return false;
|
|
184
184
|
|
|
185
185
|
const entity = system.entityOf(acc.best_body);
|
|
186
|
-
if (entity < 0)
|
|
187
|
-
// Body was unlinked concurrently; treat as a miss.
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
186
|
+
if (entity < 0) return false; // body unlinked concurrently → treat as miss
|
|
190
187
|
|
|
191
|
-
// Hit position in world space.
|
|
192
188
|
const t = acc.best_t;
|
|
193
|
-
const hx = ox + dx * t;
|
|
194
|
-
const hy = oy + dy * t;
|
|
195
|
-
const hz = oz + dz * t;
|
|
196
|
-
|
|
197
|
-
// AABB face normal at the hit point. Read the best leaf's AABB back from
|
|
198
|
-
// its BVH; project the hit point into the AABB's normalised local space
|
|
199
|
-
// (centre = 0, faces = ±1); the dominant component is the entry-face
|
|
200
|
-
// axis, its sign gives the outward normal.
|
|
201
|
-
const bvh = acc.best_bvh;
|
|
202
|
-
const node_address = acc.best_node * ELEMENT_WORD_COUNT;
|
|
203
|
-
const f32 = bvh.__data_float32;
|
|
204
|
-
const aabb_min_x = f32[node_address];
|
|
205
|
-
const aabb_min_y = f32[node_address + 1];
|
|
206
|
-
const aabb_min_z = f32[node_address + 2];
|
|
207
|
-
const aabb_max_x = f32[node_address + 3];
|
|
208
|
-
const aabb_max_y = f32[node_address + 4];
|
|
209
|
-
const aabb_max_z = f32[node_address + 5];
|
|
210
|
-
|
|
211
|
-
const cx = (aabb_min_x + aabb_max_x) * 0.5;
|
|
212
|
-
const cy = (aabb_min_y + aabb_max_y) * 0.5;
|
|
213
|
-
const cz = (aabb_min_z + aabb_max_z) * 0.5;
|
|
214
|
-
// Multiplicative inverse of the half-extent saves one divide per axis.
|
|
215
|
-
// A degenerate AABB (zero extent on an axis) is impossible for live BVH
|
|
216
|
-
// leaves — physics shapes always have non-zero bounding extent.
|
|
217
|
-
const inv_half_x = 2 / (aabb_max_x - aabb_min_x);
|
|
218
|
-
const inv_half_y = 2 / (aabb_max_y - aabb_min_y);
|
|
219
|
-
const inv_half_z = 2 / (aabb_max_z - aabb_min_z);
|
|
220
|
-
|
|
221
|
-
const px = (hx - cx) * inv_half_x;
|
|
222
|
-
const py = (hy - cy) * inv_half_y;
|
|
223
|
-
const pz = (hz - cz) * inv_half_z;
|
|
224
|
-
|
|
225
|
-
const apx = px < 0 ? -px : px;
|
|
226
|
-
const apy = py < 0 ? -py : py;
|
|
227
|
-
const apz = pz < 0 ? -pz : pz;
|
|
228
|
-
|
|
229
|
-
let nx, ny, nz;
|
|
230
|
-
if (apx >= apy && apx >= apz) {
|
|
231
|
-
nx = px >= 0 ? 1 : -1; ny = 0; nz = 0;
|
|
232
|
-
} else if (apy >= apz) {
|
|
233
|
-
nx = 0; ny = py >= 0 ? 1 : -1; nz = 0;
|
|
234
|
-
} else {
|
|
235
|
-
nx = 0; ny = 0; nz = pz >= 0 ? 1 : -1;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Fill result. Direct typed-array writes — Vector3 extends Float32Array,
|
|
239
|
-
// and there are no observers subscribed to a query-result vector.
|
|
240
189
|
const rp = result.position;
|
|
241
190
|
const rn = result.normal;
|
|
242
|
-
rp[0] =
|
|
243
|
-
rn[0] =
|
|
191
|
+
rp[0] = ox + dx * t; rp[1] = oy + dy * t; rp[2] = oz + dz * t;
|
|
192
|
+
rn[0] = best_normal[0]; rn[1] = best_normal[1]; rn[2] = best_normal[2];
|
|
244
193
|
result.t = t;
|
|
245
194
|
result.entity = entity;
|
|
246
195
|
result.body_id = acc.best_body;
|