@woosh/meep-engine 2.140.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/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
- 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 -461
- package/src/engine/physics/REVIEW_002.md +151 -0
- package/src/engine/physics/REVIEW_003.md +166 -0
- package/src/engine/physics/constraint/DofMode.d.ts +28 -0
- package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
- package/src/engine/physics/constraint/DofMode.js +35 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +38 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
- package/src/engine/physics/constraint/solve_constraints.js +673 -0
- package/src/engine/physics/ecs/Joint.d.ts +294 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
- package/src/engine/physics/ecs/Joint.js +402 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
- package/src/engine/physics/fluid/FluidField.d.ts +14 -10
- package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidField.js +14 -10
- package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidSimulator.js +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
- package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
- package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
- package/src/engine/physics/island/IslandBuilder.js +33 -16
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +75 -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/solver/solve_contacts.d.ts +28 -0
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +169 -1
- 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
|
@@ -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;
|
|
@@ -51,6 +51,34 @@ export function warm_start_contacts(manifolds: ManifoldStore, system: PhysicsSys
|
|
|
51
51
|
* @param {PhysicsSystem} system
|
|
52
52
|
*/
|
|
53
53
|
export function refresh_contacts(manifolds: ManifoldStore, system: PhysicsSystem): void;
|
|
54
|
+
/**
|
|
55
|
+
* Stage 2b (per substep) — the concave counterpart of {@link refresh_contacts}.
|
|
56
|
+
*
|
|
57
|
+
* For each concave-involved slot, re-runs the narrowphase geometry at the
|
|
58
|
+
* current substep pose ({@link redetect_pair_geometry}, which rewrites the
|
|
59
|
+
* manifold's witness / normal / depth in place), then re-derives the solver
|
|
60
|
+
* scratch (lever arms, tangent basis, effective masses, position bias) for
|
|
61
|
+
* that slot's contacts from the fresh geometry. This is the whole point of
|
|
62
|
+
* the concave path: a body rocking on a mesh changes which triangle (and
|
|
63
|
+
* normal) it rests on within a single outer step, and freezing that — as the
|
|
64
|
+
* convex analytic refresh does — pumps energy in. Re-detecting gives a
|
|
65
|
+
* correct per-substep normal so the body settles.
|
|
66
|
+
*
|
|
67
|
+
* Cost is ~one narrowphase dispatch per concave slot per substep — acceptable
|
|
68
|
+
* because concave-involved pairs are rare (the common concave case, a convex
|
|
69
|
+
* body on static terrain, is convex on the moving side and never lands here).
|
|
70
|
+
* The contact count is held fixed (redetect updates geometry only), so the
|
|
71
|
+
* scratch stays aligned with prepare.
|
|
72
|
+
*
|
|
73
|
+
* Must run before {@link refresh_contacts} / {@link warm_start_contacts} each
|
|
74
|
+
* substep. `rest_bias` (captured at prepare) and the `scratch_max_jn`
|
|
75
|
+
* restitution gate are preserved.
|
|
76
|
+
*
|
|
77
|
+
* @param {ManifoldStore} manifolds
|
|
78
|
+
* @param {PhysicsSystem} system reads `__body_collider_lists`, `__bodies`,
|
|
79
|
+
* `__transforms`.
|
|
80
|
+
*/
|
|
81
|
+
export function redetect_concave_contacts(manifolds: ManifoldStore, system: PhysicsSystem): void;
|
|
54
82
|
/**
|
|
55
83
|
* Stage 3 (per substep) — velocity iterations enforcing pure
|
|
56
84
|
* non-penetration (`vn → 0`) plus Coulomb-disk friction. No bias: depth
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"AAgcA;;;;;;;;;;;;;GAaG;AACH,0FAHW,MAAM,GACJ,MAAM,CA4JlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,2FAuCC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wFAgEC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,iGAmFC;AAED;;;;;;;;GAQG;AACH,uFAFW,MAAM,QA2GhB;AAED;;;;;;;;;;;GAWG;AACH,yFA4DC;AAED;;;;;;;;;;;;;;GAcG;AACH,2FAFW,MAAM,QA4EhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,oFAJW,MAAM,UACN,MAAM,cACN,MAAM,QAahB;AAxlCD;;;;;GAKG;AACH,0CAFU,MAAM,CAEuB;AAEvC;;;GAGG;AACH,0CAFU,MAAM,CAEsB"}
|
|
@@ -5,6 +5,7 @@ import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
|
|
|
5
5
|
import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
|
|
6
6
|
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
7
7
|
import { v3_quat3_apply_inverse } from "../../../core/geom/vec3/v3_quat3_apply_inverse.js";
|
|
8
|
+
import { redetect_pair_geometry } from "../narrowphase/narrowphase_step.js";
|
|
8
9
|
import { friction_cone_clamp } from "./friction_cone.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -18,7 +19,8 @@ import { friction_cone_clamp } from "./friction_cone.js";
|
|
|
18
19
|
* prepare_contacts(manifolds, system, h) // once per outer step
|
|
19
20
|
* for each substep:
|
|
20
21
|
* (system integrates gravity by h)
|
|
21
|
-
*
|
|
22
|
+
* redetect_concave_contacts(manifolds, system) // concave: fresh narrowphase
|
|
23
|
+
* refresh_contacts(manifolds, system) // convex: analytic re-derive
|
|
22
24
|
* warm_start_contacts(manifolds, system) // replay impulse — per substep!
|
|
23
25
|
* solve_velocity(manifolds, system, iters) // non-penetration + friction
|
|
24
26
|
* solve_position(manifolds, system, pos_iters)
|
|
@@ -177,6 +179,27 @@ let scratch_pos_jn = new Float64Array(64);
|
|
|
177
179
|
*/
|
|
178
180
|
let scratch_max_jn = new Float64Array(64);
|
|
179
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Per-contact flag: 1 if the contact's pair involves a concave body, else 0.
|
|
184
|
+
* Concave contacts take the per-substep re-detection path
|
|
185
|
+
* ({@link redetect_concave_contacts}) — their feature genuinely changes as
|
|
186
|
+
* the body rocks, so the analytic refresh that freezes it would pump energy.
|
|
187
|
+
* Convex contacts (flag 0) take the cheap analytic {@link refresh_contacts}.
|
|
188
|
+
* @type {Uint8Array}
|
|
189
|
+
*/
|
|
190
|
+
let scratch_concave = new Uint8Array(64);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Distinct concave-involved manifold slots touched this step, with the body
|
|
194
|
+
* indices needed to fetch their collider lists for re-detection. Parallel
|
|
195
|
+
* arrays, `g_concave_slot_count` valid entries, filled by
|
|
196
|
+
* {@link prepare_contacts}.
|
|
197
|
+
*/
|
|
198
|
+
let concave_slot = new Uint32Array(32);
|
|
199
|
+
let concave_slot_idxA = new Uint32Array(32);
|
|
200
|
+
let concave_slot_idxB = new Uint32Array(32);
|
|
201
|
+
let g_concave_slot_count = 0;
|
|
202
|
+
|
|
180
203
|
/**
|
|
181
204
|
* Shared cross-stage state, set by {@link prepare_contacts} and read by the
|
|
182
205
|
* per-substep stages within the same outer step. Single-threaded, so plain
|
|
@@ -205,6 +228,17 @@ function ensure_capacity(n) {
|
|
|
205
228
|
if (scratch_max_jn.length < n) {
|
|
206
229
|
scratch_max_jn = new Float64Array(n * 2);
|
|
207
230
|
}
|
|
231
|
+
if (scratch_concave.length < n) {
|
|
232
|
+
scratch_concave = new Uint8Array(n * 2);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ensure_concave_slot_capacity(n) {
|
|
237
|
+
if (concave_slot.length < n) {
|
|
238
|
+
concave_slot = new Uint32Array(n * 2);
|
|
239
|
+
concave_slot_idxA = new Uint32Array(n * 2);
|
|
240
|
+
concave_slot_idxB = new Uint32Array(n * 2);
|
|
241
|
+
}
|
|
208
242
|
}
|
|
209
243
|
|
|
210
244
|
/**
|
|
@@ -452,6 +486,8 @@ export function prepare_contacts(manifolds, system, dt_sub) {
|
|
|
452
486
|
const mus = scratch_mu;
|
|
453
487
|
const pos_jn = scratch_pos_jn;
|
|
454
488
|
|
|
489
|
+
g_concave_slot_count = 0;
|
|
490
|
+
|
|
455
491
|
let c = 0;
|
|
456
492
|
for (let i = 0; i < total_slots; i++) {
|
|
457
493
|
const slot = slot_list[i];
|
|
@@ -466,6 +502,19 @@ export function prepare_contacts(manifolds, system, dt_sub) {
|
|
|
466
502
|
if (colA === null || colB === null) continue;
|
|
467
503
|
if (pair_is_sensor(rbA, colA, rbB, colB)) continue;
|
|
468
504
|
|
|
505
|
+
// A pair is "concave" when either side's shape is non-convex. Those
|
|
506
|
+
// contacts take the per-substep re-detection path (the contact
|
|
507
|
+
// feature moves as the body rocks); convex pairs keep the cheap
|
|
508
|
+
// analytic refresh. Recorded once per slot for redetect_concave_contacts.
|
|
509
|
+
const slot_concave = (colA.shape.is_convex === false) || (colB.shape.is_convex === false);
|
|
510
|
+
if (slot_concave) {
|
|
511
|
+
ensure_concave_slot_capacity(g_concave_slot_count + 1);
|
|
512
|
+
concave_slot[g_concave_slot_count] = slot;
|
|
513
|
+
concave_slot_idxA[g_concave_slot_count] = idxA;
|
|
514
|
+
concave_slot_idxB[g_concave_slot_count] = idxB;
|
|
515
|
+
g_concave_slot_count++;
|
|
516
|
+
}
|
|
517
|
+
|
|
469
518
|
const invMA = inv_mass_of(rbA);
|
|
470
519
|
const invMB = inv_mass_of(rbB);
|
|
471
520
|
|
|
@@ -545,6 +594,7 @@ export function prepare_contacts(manifolds, system, dt_sub) {
|
|
|
545
594
|
mus[c] = friction_combined;
|
|
546
595
|
pos_jn[c] = 0;
|
|
547
596
|
scratch_max_jn[c] = 0;
|
|
597
|
+
scratch_concave[c] = slot_concave ? 1 : 0;
|
|
548
598
|
|
|
549
599
|
idx[c * INDEX_STRIDE] = slot;
|
|
550
600
|
idx[c * INDEX_STRIDE + 1] = k;
|
|
@@ -654,6 +704,11 @@ export function refresh_contacts(manifolds, system) {
|
|
|
654
704
|
const spook_a = g_spook_a;
|
|
655
705
|
|
|
656
706
|
for (let ci = 0; ci < count; ci++) {
|
|
707
|
+
// Concave contacts are refreshed by redetect_concave_contacts (fresh
|
|
708
|
+
// narrowphase geometry each substep), not by the analytic rotation of
|
|
709
|
+
// frozen anchors — their feature moves as the body rocks.
|
|
710
|
+
if (scratch_concave[ci] === 1) continue;
|
|
711
|
+
|
|
657
712
|
const slot = idx[ci * INDEX_STRIDE];
|
|
658
713
|
const cidx = idx[ci * INDEX_STRIDE + 1];
|
|
659
714
|
const idxA = idx[ci * INDEX_STRIDE + 2];
|
|
@@ -704,6 +759,118 @@ export function refresh_contacts(manifolds, system) {
|
|
|
704
759
|
}
|
|
705
760
|
}
|
|
706
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Stage 2b (per substep) — the concave counterpart of {@link refresh_contacts}.
|
|
764
|
+
*
|
|
765
|
+
* For each concave-involved slot, re-runs the narrowphase geometry at the
|
|
766
|
+
* current substep pose ({@link redetect_pair_geometry}, which rewrites the
|
|
767
|
+
* manifold's witness / normal / depth in place), then re-derives the solver
|
|
768
|
+
* scratch (lever arms, tangent basis, effective masses, position bias) for
|
|
769
|
+
* that slot's contacts from the fresh geometry. This is the whole point of
|
|
770
|
+
* the concave path: a body rocking on a mesh changes which triangle (and
|
|
771
|
+
* normal) it rests on within a single outer step, and freezing that — as the
|
|
772
|
+
* convex analytic refresh does — pumps energy in. Re-detecting gives a
|
|
773
|
+
* correct per-substep normal so the body settles.
|
|
774
|
+
*
|
|
775
|
+
* Cost is ~one narrowphase dispatch per concave slot per substep — acceptable
|
|
776
|
+
* because concave-involved pairs are rare (the common concave case, a convex
|
|
777
|
+
* body on static terrain, is convex on the moving side and never lands here).
|
|
778
|
+
* The contact count is held fixed (redetect updates geometry only), so the
|
|
779
|
+
* scratch stays aligned with prepare.
|
|
780
|
+
*
|
|
781
|
+
* Must run before {@link refresh_contacts} / {@link warm_start_contacts} each
|
|
782
|
+
* substep. `rest_bias` (captured at prepare) and the `scratch_max_jn`
|
|
783
|
+
* restitution gate are preserved.
|
|
784
|
+
*
|
|
785
|
+
* @param {ManifoldStore} manifolds
|
|
786
|
+
* @param {PhysicsSystem} system reads `__body_collider_lists`, `__bodies`,
|
|
787
|
+
* `__transforms`.
|
|
788
|
+
*/
|
|
789
|
+
export function redetect_concave_contacts(manifolds, system) {
|
|
790
|
+
const ns = g_concave_slot_count;
|
|
791
|
+
if (ns === 0) return;
|
|
792
|
+
|
|
793
|
+
const lists = system.__body_collider_lists;
|
|
794
|
+
|
|
795
|
+
// 1. Re-detect fresh geometry into the manifold for each concave slot.
|
|
796
|
+
for (let s = 0; s < ns; s++) {
|
|
797
|
+
redetect_pair_geometry(manifolds, concave_slot[s], lists[concave_slot_idxA[s]], lists[concave_slot_idxB[s]]);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 2. Re-derive the solver scratch for every concave contact from the
|
|
801
|
+
// fresh manifold geometry (lever arms, tangents, effective masses,
|
|
802
|
+
// position bias). Mirrors prepare's per-contact setup but reads the
|
|
803
|
+
// just-updated witness / normal / depth instead of the prepare-time
|
|
804
|
+
// values; no local-frame anchors are needed since we re-detect rather
|
|
805
|
+
// than rotate frozen anchors.
|
|
806
|
+
const count = g_contact_count;
|
|
807
|
+
const data = manifolds.data_buffer;
|
|
808
|
+
const pre = scratch_pre;
|
|
809
|
+
const idx = scratch_idx;
|
|
810
|
+
const spook_a = g_spook_a;
|
|
811
|
+
const spook_eps = g_spook_eps;
|
|
812
|
+
|
|
813
|
+
for (let ci = 0; ci < count; ci++) {
|
|
814
|
+
if (scratch_concave[ci] === 0) continue;
|
|
815
|
+
|
|
816
|
+
const slot = idx[ci * INDEX_STRIDE];
|
|
817
|
+
const cidx = idx[ci * INDEX_STRIDE + 1];
|
|
818
|
+
const idxA = idx[ci * INDEX_STRIDE + 2];
|
|
819
|
+
const idxB = idx[ci * INDEX_STRIDE + 3];
|
|
820
|
+
const rbA = system.__bodies[idxA];
|
|
821
|
+
const rbB = system.__bodies[idxB];
|
|
822
|
+
const trA = system.__transforms[idxA];
|
|
823
|
+
const trB = system.__transforms[idxB];
|
|
824
|
+
const invMA = inv_mass_of(rbA);
|
|
825
|
+
const invMB = inv_mass_of(rbB);
|
|
826
|
+
|
|
827
|
+
const slot_off = manifolds.slot_data_offset(slot);
|
|
828
|
+
const off = slot_off + cidx * CONTACT_STRIDE;
|
|
829
|
+
|
|
830
|
+
const wax = data[off], way = data[off + 1], waz = data[off + 2];
|
|
831
|
+
const wbx = data[off + 3], wby = data[off + 4], wbz = data[off + 5];
|
|
832
|
+
const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];
|
|
833
|
+
const depth = data[off + 9];
|
|
834
|
+
|
|
835
|
+
const px = (wax + wbx) * 0.5;
|
|
836
|
+
const py = (way + wby) * 0.5;
|
|
837
|
+
const pz = (waz + wbz) * 0.5;
|
|
838
|
+
|
|
839
|
+
const rax = px - trA.position.x, ray = py - trA.position.y, raz = pz - trA.position.z;
|
|
840
|
+
const rbx = px - trB.position.x, rby = py - trB.position.y, rbz = pz - trB.position.z;
|
|
841
|
+
|
|
842
|
+
const pre_off = ci * PRE_STRIDE;
|
|
843
|
+
pre[pre_off + 6] = rax; pre[pre_off + 7] = ray; pre[pre_off + 8] = raz;
|
|
844
|
+
pre[pre_off + 9] = rbx; pre[pre_off + 10] = rby; pre[pre_off + 11] = rbz;
|
|
845
|
+
|
|
846
|
+
build_tangents(pre, pre_off + 12, nx, ny, nz);
|
|
847
|
+
const t1x = pre[pre_off + 12], t1y = pre[pre_off + 13], t1z = pre[pre_off + 14];
|
|
848
|
+
const t2x = pre[pre_off + 15], t2y = pre[pre_off + 16], t2z = pre[pre_off + 17];
|
|
849
|
+
|
|
850
|
+
const k_n = invMA + invMB
|
|
851
|
+
+ angular_jacobian_contribution(rbA, trA, rax, ray, raz, nx, ny, nz, scratch_inertia_a)
|
|
852
|
+
+ angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, nx, ny, nz, scratch_inertia_b);
|
|
853
|
+
const k_t1 = invMA + invMB
|
|
854
|
+
+ angular_jacobian_contribution(rbA, trA, rax, ray, raz, t1x, t1y, t1z, scratch_inertia_a)
|
|
855
|
+
+ angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, t1x, t1y, t1z, scratch_inertia_b);
|
|
856
|
+
const k_t2 = invMA + invMB
|
|
857
|
+
+ angular_jacobian_contribution(rbA, trA, rax, ray, raz, t2x, t2y, t2z, scratch_inertia_a)
|
|
858
|
+
+ angular_jacobian_contribution(rbB, trB, rbx, rby, rbz, t2x, t2y, t2z, scratch_inertia_b);
|
|
859
|
+
|
|
860
|
+
const k_n_eff = k_n + spook_eps;
|
|
861
|
+
pre[pre_off + 18] = k_n_eff > 0 ? 1 / k_n_eff : 0;
|
|
862
|
+
pre[pre_off + 19] = k_t1 > 0 ? 1 / k_t1 : 0;
|
|
863
|
+
pre[pre_off + 20] = k_t2 > 0 ? 1 / k_t2 : 0;
|
|
864
|
+
|
|
865
|
+
let bias_position = 0;
|
|
866
|
+
if (depth > PENETRATION_SLOP) {
|
|
867
|
+
bias_position = -spook_a * (depth - PENETRATION_SLOP);
|
|
868
|
+
if (bias_position < -MAX_POSITION_BIAS) bias_position = -MAX_POSITION_BIAS;
|
|
869
|
+
}
|
|
870
|
+
pre[pre_off + 22] = bias_position;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
707
874
|
/**
|
|
708
875
|
* Stage 3 (per substep) — velocity iterations enforcing pure
|
|
709
876
|
* non-penetration (`vn → 0`) plus Coulomb-disk friction. No bias: depth
|
|
@@ -1006,6 +1173,7 @@ export function solve_contacts(manifolds, system, dt,
|
|
|
1006
1173
|
pos_iters = DEFAULT_POSITION_ITERATIONS) {
|
|
1007
1174
|
if (dt <= 0) return;
|
|
1008
1175
|
if (prepare_contacts(manifolds, system, dt) === 0) return;
|
|
1176
|
+
redetect_concave_contacts(manifolds, system);
|
|
1009
1177
|
refresh_contacts(manifolds, system);
|
|
1010
1178
|
warm_start_contacts(manifolds, system);
|
|
1011
1179
|
solve_velocity(manifolds, system, iters);
|