@woosh/meep-engine 2.144.0 → 2.146.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/bvh2/bvh3/BVH.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/BVH.js +158 -4
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts +56 -0
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/CylinderShape3D.js +223 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
- package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -302
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +13 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +16 -2
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallJump.js +11 -3
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +30 -35
- package/src/engine/control/first-person/collision/KinematicMover.d.ts +35 -5
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
- package/src/engine/control/first-person/collision/KinematicMover.js +634 -424
- package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
- package/src/engine/physics/PLAN.md +943 -767
- package/src/engine/physics/body/BodyStorage.d.ts +9 -0
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +23 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
- package/src/engine/physics/ccd/linear_sweep.js +238 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +185 -183
- package/src/engine/simulation/Ticker.d.ts +14 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +136 -1
|
@@ -1,451 +1,486 @@
|
|
|
1
|
-
import { assert } from "../../../assert.js";
|
|
2
|
-
import { clamp } from "../../../math/clamp.js";
|
|
3
|
-
import { v3_length } from "../../vec3/v3_length.js";
|
|
4
|
-
import { Vector3 } from "../../Vector3.js";
|
|
5
|
-
import { AbstractShape3D } from "./AbstractShape3D.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Heightmap shape, intended primarily for terrain.
|
|
9
|
-
*
|
|
10
|
-
* The shape is a closed solid bounded below by the plane perpendicular to
|
|
11
|
-
* {@link orientation} (the "floor") and above by a height-field surface
|
|
12
|
-
* defined by a {@link Sampler2D}. Heights are sampled with Catmull-Rom
|
|
13
|
-
* filtering ({@link Sampler2D#sampleChannelCatmullRomUV})
|
|
14
|
-
* the terrain
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const b = this._basis;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
result[
|
|
355
|
-
result[
|
|
356
|
-
result[
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* the
|
|
363
|
-
*
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* the footprint
|
|
396
|
-
*
|
|
397
|
-
*/
|
|
398
|
-
get
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1
|
+
import { assert } from "../../../assert.js";
|
|
2
|
+
import { clamp } from "../../../math/clamp.js";
|
|
3
|
+
import { v3_length } from "../../vec3/v3_length.js";
|
|
4
|
+
import { Vector3 } from "../../Vector3.js";
|
|
5
|
+
import { AbstractShape3D } from "./AbstractShape3D.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Heightmap shape, intended primarily for terrain.
|
|
9
|
+
*
|
|
10
|
+
* The shape is a closed solid bounded below by the plane perpendicular to
|
|
11
|
+
* {@link orientation} (the "floor") and above by a height-field surface
|
|
12
|
+
* defined by a {@link Sampler2D}. Heights are sampled with Catmull-Rom
|
|
13
|
+
* filtering ({@link Sampler2D#sampleChannelCatmullRomUV}) — the *same* cubic
|
|
14
|
+
* the terrain renderer uses (`sampleChannelBicubicUV` expands to the
|
|
15
|
+
* identical Catmull-Rom weights, with the matching `u·width − 0.5` UV
|
|
16
|
+
* convention), so the collision and render surfaces coincide at every point
|
|
17
|
+
* they both sample.
|
|
18
|
+
*
|
|
19
|
+
* They differ only in *tessellation density*: the renderer lays out
|
|
20
|
+
* `size × resolution` segments per side, while collision defaults to one
|
|
21
|
+
* quad per sampler cell. {@link tessellation} closes that gap — set it > 1
|
|
22
|
+
* to subdivide each sampler cell into N×N sub-cells so the contact surface
|
|
23
|
+
* approaches render fidelity even when the sampler is deliberately coarse.
|
|
24
|
+
*
|
|
25
|
+
* Local frame layout:
|
|
26
|
+
* - The orientation vector defines the local "up" axis (unit).
|
|
27
|
+
* - An orthonormal basis (u, v, n=orientation) is built from it.
|
|
28
|
+
* - Footprint extends along the basis-u axis over [-size.x/2, +size.x/2]
|
|
29
|
+
* and along the basis-v axis over [-size.z/2, +size.z/2].
|
|
30
|
+
* - The surface height (along orientation) at heightmap-UV (u01, v01) is
|
|
31
|
+
* `sampler.sampleChannelCatmullRomUV(u01, v01, 0)`.
|
|
32
|
+
* - The solid volume occupies `h ∈ [0, sampledHeight(u01, v01)]` along the
|
|
33
|
+
* orientation axis, with `0` being the floor at body-local origin.
|
|
34
|
+
* - `size.y` is the maximum height value of the heightfield (used for the
|
|
35
|
+
* bounding box). Sampler values are NOT clamped to it.
|
|
36
|
+
*
|
|
37
|
+
* NON-CONVEX. {@link support} throws — GJK/EPA cannot be run against a
|
|
38
|
+
* heightmap directly. The physics narrowphase must dispatch a dedicated
|
|
39
|
+
* grid-traversal path when one of the colliders is a heightmap.
|
|
40
|
+
*
|
|
41
|
+
* @author Alex Goldring
|
|
42
|
+
* @copyright Company Named Limited (c) 2026
|
|
43
|
+
*/
|
|
44
|
+
export class HeightMapShape3D extends AbstractShape3D {
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unit vector defining the local "up" axis (the direction the
|
|
50
|
+
* heightmap's surface faces). Default is +Y.
|
|
51
|
+
* @readonly
|
|
52
|
+
* @type {Vector3}
|
|
53
|
+
*/
|
|
54
|
+
this.orientation = new Vector3(0, 1, 0);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Bounding-box extents in the heightmap-local (u, height, v) frame.
|
|
58
|
+
* size.x — footprint extent along basis-u (perpendicular to orientation)
|
|
59
|
+
* size.y — maximum heightmap height (extent along orientation)
|
|
60
|
+
* size.z — footprint extent along basis-v (perpendicular to orientation)
|
|
61
|
+
* @readonly
|
|
62
|
+
* @type {Vector3}
|
|
63
|
+
*/
|
|
64
|
+
this.size = new Vector3(1, 1, 1);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sampler holding height values. Float32 backing recommended so the
|
|
68
|
+
* Terrain system's height texture plugs in directly. Single-channel
|
|
69
|
+
* sampler is the common case; only channel 0 is read.
|
|
70
|
+
* @type {Sampler2D | null}
|
|
71
|
+
*/
|
|
72
|
+
this.sampler = null;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Collision tessellation factor: the narrowphase splits each sampler
|
|
76
|
+
* cell into `tessellation × tessellation` sub-cells before emitting
|
|
77
|
+
* collision triangles, sampling the same Catmull-Rom filter at the
|
|
78
|
+
* finer sub-cell corners. `1` (the default) is one quad per sampler
|
|
79
|
+
* cell — the legacy behaviour. Larger values let a coarse sampler's
|
|
80
|
+
* contact surface approach the rendered mesh's fidelity.
|
|
81
|
+
*
|
|
82
|
+
* Non-negative integer (validated by {@link HeightMapShape3D.from});
|
|
83
|
+
* cost is O(N²) per cell, so the caller owns the fidelity/cost
|
|
84
|
+
* trade-off. `0` yields no collision triangles — the degenerate empty
|
|
85
|
+
* case, mirroring a zero footprint size. Affects only triangle
|
|
86
|
+
* enumeration — the continuous queries ({@link sample_height_at_uv},
|
|
87
|
+
* {@link signed_distance_at_point}, {@link nearest_point_on_surface})
|
|
88
|
+
* read the smooth filter directly and are unaffected.
|
|
89
|
+
* @type {number}
|
|
90
|
+
*/
|
|
91
|
+
this.tessellation = 1;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Cached orthonormal basis [u_x,u_y,u_z, v_x,v_y,v_z, n_x,n_y,n_z]
|
|
95
|
+
* built from {@link orientation}. Updated lazily by {@link _ensure_basis}.
|
|
96
|
+
* @private
|
|
97
|
+
* @type {Float64Array}
|
|
98
|
+
*/
|
|
99
|
+
this._basis = new Float64Array(9);
|
|
100
|
+
|
|
101
|
+
// last-seen orientation components, used to detect dirty basis
|
|
102
|
+
this._basis_orientation_x = NaN;
|
|
103
|
+
this._basis_orientation_y = NaN;
|
|
104
|
+
this._basis_orientation_z = NaN;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Convenience constructor.
|
|
109
|
+
* @param {Sampler2D} sampler
|
|
110
|
+
* @param {number} size_x footprint extent along basis-u
|
|
111
|
+
* @param {number} size_y maximum heightmap height (along orientation)
|
|
112
|
+
* @param {number} size_z footprint extent along basis-v
|
|
113
|
+
* @param {Vector3} [orientation] defaults to +Y
|
|
114
|
+
* @param {number} [tessellation] collision sub-cells per sampler cell per
|
|
115
|
+
* axis; non-negative integer, defaults to 1 (one quad per sampler cell).
|
|
116
|
+
* See {@link HeightMapShape3D#tessellation}.
|
|
117
|
+
* @returns {HeightMapShape3D}
|
|
118
|
+
*/
|
|
119
|
+
static from(sampler, size_x, size_y, size_z, orientation, tessellation = 1) {
|
|
120
|
+
assert.isNumber(size_x, "size_x");
|
|
121
|
+
assert.isNumber(size_y, "size_y");
|
|
122
|
+
assert.isNumber(size_z, "size_z");
|
|
123
|
+
assert.greaterThanOrEqual(size_x, 0, "size_x");
|
|
124
|
+
assert.greaterThanOrEqual(size_y, 0, "size_y");
|
|
125
|
+
assert.greaterThanOrEqual(size_z, 0, "size_z");
|
|
126
|
+
assert.isNonNegativeInteger(tessellation, "tessellation");
|
|
127
|
+
|
|
128
|
+
const r = new HeightMapShape3D();
|
|
129
|
+
r.sampler = sampler;
|
|
130
|
+
r.size.set(size_x, size_y, size_z);
|
|
131
|
+
r.tessellation = tessellation;
|
|
132
|
+
|
|
133
|
+
if (orientation !== undefined) {
|
|
134
|
+
r.orientation.set(orientation.x, orientation.y, orientation.z);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return r;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Recompute the orthonormal basis (u, v, n) if {@link orientation} changed.
|
|
142
|
+
*
|
|
143
|
+
* Construction chosen so that the default orientation +Y produces the
|
|
144
|
+
* intuitive mapping `u = +X, v = +Z, n = +Y` — i.e. size.x runs along
|
|
145
|
+
* body X, size.z along body Z. We project body +X onto the plane
|
|
146
|
+
* perpendicular to n (the orientation), with a fall-back to projecting
|
|
147
|
+
* body +Z when n is too close to colinear with +X.
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
_ensure_basis() {
|
|
151
|
+
const nx = this.orientation[0];
|
|
152
|
+
const ny = this.orientation[1];
|
|
153
|
+
const nz = this.orientation[2];
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
nx === this._basis_orientation_x
|
|
157
|
+
&& ny === this._basis_orientation_y
|
|
158
|
+
&& nz === this._basis_orientation_z
|
|
159
|
+
) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// tangent u = normalize(project body +X onto plane perpendicular to n)
|
|
164
|
+
// fall back to body +Z if n is too colinear with +X
|
|
165
|
+
let u_x, u_y, u_z;
|
|
166
|
+
|
|
167
|
+
if (Math.abs(nx) < 0.9) {
|
|
168
|
+
// u = (+X) - (n . +X) * n = (1 - nx*nx, -nx*ny, -nx*nz)
|
|
169
|
+
u_x = 1 - nx * nx;
|
|
170
|
+
u_y = -nx * ny;
|
|
171
|
+
u_z = -nx * nz;
|
|
172
|
+
} else {
|
|
173
|
+
// u = (+Z) - (n . +Z) * n = (-nz*nx, -nz*ny, 1 - nz*nz)
|
|
174
|
+
u_x = -nz * nx;
|
|
175
|
+
u_y = -nz * ny;
|
|
176
|
+
u_z = 1 - nz * nz;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const u_inv = 1 / v3_length(u_x, u_y, u_z);
|
|
180
|
+
u_x *= u_inv;
|
|
181
|
+
u_y *= u_inv;
|
|
182
|
+
u_z *= u_inv;
|
|
183
|
+
|
|
184
|
+
// v = u × n (for n=+Y, u=+X this gives v=+Z, the intuitive choice)
|
|
185
|
+
const v_x = u_y * nz - u_z * ny;
|
|
186
|
+
const v_y = u_z * nx - u_x * nz;
|
|
187
|
+
const v_z = u_x * ny - u_y * nx;
|
|
188
|
+
|
|
189
|
+
const b = this._basis;
|
|
190
|
+
b[0] = u_x; b[1] = u_y; b[2] = u_z;
|
|
191
|
+
b[3] = v_x; b[4] = v_y; b[5] = v_z;
|
|
192
|
+
b[6] = nx; b[7] = ny; b[8] = nz;
|
|
193
|
+
|
|
194
|
+
this._basis_orientation_x = nx;
|
|
195
|
+
this._basis_orientation_y = ny;
|
|
196
|
+
this._basis_orientation_z = nz;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Sample the surface height at heightmap UV coordinates.
|
|
201
|
+
* Uses Catmull-Rom filtering to match the terrain system's geometry construction.
|
|
202
|
+
* @param {number} u01 horizontal UV, in [0, 1]
|
|
203
|
+
* @param {number} v01 vertical UV, in [0, 1]
|
|
204
|
+
* @returns {number} height along orientation axis
|
|
205
|
+
*/
|
|
206
|
+
sample_height_at_uv(u01, v01) {
|
|
207
|
+
return this.sampler.sampleChannelCatmullRomUV(u01, v01, 0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Project a body-local position to the surface and sample the height there.
|
|
212
|
+
* The returned height is in the orientation-axis direction.
|
|
213
|
+
* Positions outside the footprint sample the clamped UV (sampler clamps).
|
|
214
|
+
* @param {number} px body-local x
|
|
215
|
+
* @param {number} py body-local y
|
|
216
|
+
* @param {number} pz body-local z
|
|
217
|
+
* @returns {number}
|
|
218
|
+
*/
|
|
219
|
+
sample_height_at_position(px, py, pz) {
|
|
220
|
+
this._ensure_basis();
|
|
221
|
+
|
|
222
|
+
const b = this._basis;
|
|
223
|
+
|
|
224
|
+
// project onto basis u and basis v
|
|
225
|
+
const u_coord = b[0] * px + b[1] * py + b[2] * pz;
|
|
226
|
+
const v_coord = b[3] * px + b[4] * py + b[5] * pz;
|
|
227
|
+
|
|
228
|
+
const u01 = u_coord / this.size[0] + 0.5;
|
|
229
|
+
const v01 = v_coord / this.size[2] + 0.5;
|
|
230
|
+
|
|
231
|
+
return this.sample_height_at_uv(u01, v01);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
compute_bounding_box(result) {
|
|
235
|
+
this._ensure_basis();
|
|
236
|
+
|
|
237
|
+
const b = this._basis;
|
|
238
|
+
|
|
239
|
+
const sx = this.size[0];
|
|
240
|
+
const sy = this.size[1];
|
|
241
|
+
const sz = this.size[2];
|
|
242
|
+
|
|
243
|
+
const half_u = sx * 0.5;
|
|
244
|
+
const half_v = sz * 0.5;
|
|
245
|
+
|
|
246
|
+
// For each body axis k (0=x, 1=y, 2=z):
|
|
247
|
+
// body[k] = b[0+k]*u + b[3+k]*v + b[6+k]*h
|
|
248
|
+
//
|
|
249
|
+
// The (u,v) footprint contribution is symmetric (u ∈ [-half_u, +half_u], v ∈ [-half_v, +half_v])
|
|
250
|
+
// The height contribution is asymmetric (h ∈ [0, sy])
|
|
251
|
+
for (let k = 0; k < 3; k++) {
|
|
252
|
+
const cu = b[k];
|
|
253
|
+
const cv = b[3 + k];
|
|
254
|
+
const ch = b[6 + k];
|
|
255
|
+
|
|
256
|
+
const uv_extent = Math.abs(cu) * half_u + Math.abs(cv) * half_v;
|
|
257
|
+
|
|
258
|
+
const h_lo = ch < 0 ? ch * sy : 0;
|
|
259
|
+
const h_hi = ch > 0 ? ch * sy : 0;
|
|
260
|
+
|
|
261
|
+
result[k] = -uv_extent + h_lo;
|
|
262
|
+
result[k + 3] = uv_extent + h_hi;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
contains_point(point) {
|
|
267
|
+
const px = point[0];
|
|
268
|
+
const py = point[1];
|
|
269
|
+
const pz = point[2];
|
|
270
|
+
|
|
271
|
+
this._ensure_basis();
|
|
272
|
+
|
|
273
|
+
const b = this._basis;
|
|
274
|
+
|
|
275
|
+
// project into heightmap-local frame (u, v, h)
|
|
276
|
+
const u_coord = b[0] * px + b[1] * py + b[2] * pz;
|
|
277
|
+
const v_coord = b[3] * px + b[4] * py + b[5] * pz;
|
|
278
|
+
const h_coord = b[6] * px + b[7] * py + b[8] * pz;
|
|
279
|
+
|
|
280
|
+
const half_u = this.size[0] * 0.5;
|
|
281
|
+
const half_v = this.size[2] * 0.5;
|
|
282
|
+
|
|
283
|
+
if (u_coord <= -half_u || u_coord >= half_u) return false;
|
|
284
|
+
if (v_coord <= -half_v || v_coord >= half_v) return false;
|
|
285
|
+
if (h_coord <= 0 || h_coord >= this.size[1]) return false;
|
|
286
|
+
|
|
287
|
+
const u01 = u_coord / this.size[0] + 0.5;
|
|
288
|
+
const v01 = v_coord / this.size[2] + 0.5;
|
|
289
|
+
|
|
290
|
+
const surface_h = this.sample_height_at_uv(u01, v01);
|
|
291
|
+
|
|
292
|
+
return h_coord < surface_h;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Approximate signed distance: difference between the point's height
|
|
297
|
+
* (along orientation) and the surface height sampled at the point's
|
|
298
|
+
* footprint UV. POSITIVE = above surface, NEGATIVE = below.
|
|
299
|
+
*
|
|
300
|
+
* Locally correct when the surface is flat; biased when the surface
|
|
301
|
+
* has significant slope. Sufficient for cling/raycast queries that
|
|
302
|
+
* walk down to the surface.
|
|
303
|
+
*/
|
|
304
|
+
signed_distance_at_point(point) {
|
|
305
|
+
const px = point[0];
|
|
306
|
+
const py = point[1];
|
|
307
|
+
const pz = point[2];
|
|
308
|
+
|
|
309
|
+
this._ensure_basis();
|
|
310
|
+
|
|
311
|
+
const b = this._basis;
|
|
312
|
+
|
|
313
|
+
const u_coord = b[0] * px + b[1] * py + b[2] * pz;
|
|
314
|
+
const v_coord = b[3] * px + b[4] * py + b[5] * pz;
|
|
315
|
+
const h_coord = b[6] * px + b[7] * py + b[8] * pz;
|
|
316
|
+
|
|
317
|
+
const u01 = u_coord / this.size[0] + 0.5;
|
|
318
|
+
const v01 = v_coord / this.size[2] + 0.5;
|
|
319
|
+
|
|
320
|
+
const surface_h = this.sample_height_at_uv(u01, v01);
|
|
321
|
+
|
|
322
|
+
return h_coord - surface_h;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Project a reference point onto the surface along the orientation axis.
|
|
327
|
+
* The footprint UV is clamped, so points outside the footprint produce
|
|
328
|
+
* the nearest edge-of-footprint surface sample (approximate).
|
|
329
|
+
*/
|
|
330
|
+
nearest_point_on_surface(result, reference) {
|
|
331
|
+
const rx = reference[0];
|
|
332
|
+
const ry = reference[1];
|
|
333
|
+
const rz = reference[2];
|
|
334
|
+
|
|
335
|
+
this._ensure_basis();
|
|
336
|
+
|
|
337
|
+
const b = this._basis;
|
|
338
|
+
|
|
339
|
+
const u_coord = b[0] * rx + b[1] * ry + b[2] * rz;
|
|
340
|
+
const v_coord = b[3] * rx + b[4] * ry + b[5] * rz;
|
|
341
|
+
|
|
342
|
+
const half_u = this.size[0] * 0.5;
|
|
343
|
+
const half_v = this.size[2] * 0.5;
|
|
344
|
+
|
|
345
|
+
const u_clamped = clamp(u_coord, -half_u, half_u);
|
|
346
|
+
const v_clamped = clamp(v_coord, -half_v, half_v);
|
|
347
|
+
|
|
348
|
+
const u01 = u_clamped / this.size[0] + 0.5;
|
|
349
|
+
const v01 = v_clamped / this.size[2] + 0.5;
|
|
350
|
+
|
|
351
|
+
const surface_h = this.sample_height_at_uv(u01, v01);
|
|
352
|
+
|
|
353
|
+
// compose body-local point: u_axis*u_clamped + v_axis*v_clamped + n_axis*surface_h
|
|
354
|
+
result[0] = b[0] * u_clamped + b[3] * v_clamped + b[6] * surface_h;
|
|
355
|
+
result[1] = b[1] * u_clamped + b[4] * v_clamped + b[7] * surface_h;
|
|
356
|
+
result[2] = b[2] * u_clamped + b[5] * v_clamped + b[8] * surface_h;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Heightmaps are non-convex; GJK/EPA cannot work against them directly.
|
|
361
|
+
* The physics narrowphase must dispatch a grid-traversal path that
|
|
362
|
+
* decomposes the heightmap into per-cell triangle pairs and tests each
|
|
363
|
+
* against the other shape (analogous to Bullet's btHeightfieldTerrainShape
|
|
364
|
+
* × btConcaveShape interface).
|
|
365
|
+
*
|
|
366
|
+
* This throws rather than returning a degenerate result so the call
|
|
367
|
+
* site is forced to handle heightmaps explicitly.
|
|
368
|
+
*/
|
|
369
|
+
support(result, result_offset, direction_x, direction_y, direction_z) {
|
|
370
|
+
throw new Error("HeightMapShape3D.support: heightmaps are non-convex; the narrowphase must dispatch grid-traversal instead.");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
sample_random_point_in_volume(result, result_offset, random) {
|
|
374
|
+
const u01 = random();
|
|
375
|
+
const v01 = random();
|
|
376
|
+
|
|
377
|
+
const u_coord = (u01 - 0.5) * this.size[0];
|
|
378
|
+
const v_coord = (v01 - 0.5) * this.size[2];
|
|
379
|
+
|
|
380
|
+
const surface_h = this.sample_height_at_uv(u01, v01);
|
|
381
|
+
const h_coord = random() * surface_h;
|
|
382
|
+
|
|
383
|
+
this._ensure_basis();
|
|
384
|
+
|
|
385
|
+
const b = this._basis;
|
|
386
|
+
|
|
387
|
+
result[result_offset] = b[0] * u_coord + b[3] * v_coord + b[6] * h_coord;
|
|
388
|
+
result[result_offset + 1] = b[1] * u_coord + b[4] * v_coord + b[7] * h_coord;
|
|
389
|
+
result[result_offset + 2] = b[2] * u_coord + b[5] * v_coord + b[8] * h_coord;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Sum of sampler heights × per-cell footprint area. This is the
|
|
394
|
+
* piecewise-constant approximation of the integral ∫h(u,v) dA over
|
|
395
|
+
* the footprint — exact when h is constant per cell, biased when
|
|
396
|
+
* h is smooth.
|
|
397
|
+
*/
|
|
398
|
+
get volume() {
|
|
399
|
+
const sampler = this.sampler;
|
|
400
|
+
|
|
401
|
+
if (sampler === null) {
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const w = sampler.width;
|
|
406
|
+
const h = sampler.height;
|
|
407
|
+
|
|
408
|
+
if (w === 0 || h === 0) {
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const cell_area = (this.size[0] / w) * (this.size[2] / h);
|
|
413
|
+
|
|
414
|
+
let total = 0;
|
|
415
|
+
|
|
416
|
+
for (let y = 0; y < h; y++) {
|
|
417
|
+
for (let x = 0; x < w; x++) {
|
|
418
|
+
total += sampler.readChannel(x, y, 0);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return total * cell_area;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Returns just the footprint area. A true heightmap surface area
|
|
427
|
+
* requires integrating sqrt(1 + (∂h/∂u)² + (∂h/∂v)²) over the grid;
|
|
428
|
+
* the footprint area is a lower bound and is sufficient for the
|
|
429
|
+
* physics inertia-tensor seam (heightmaps are static anyway).
|
|
430
|
+
*/
|
|
431
|
+
get surface_area() {
|
|
432
|
+
return this.size[0] * this.size[2];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* @param {HeightMapShape3D} other
|
|
437
|
+
* @returns {boolean}
|
|
438
|
+
*/
|
|
439
|
+
equals(other) {
|
|
440
|
+
if (!super.equals(other)) return false;
|
|
441
|
+
|
|
442
|
+
if (!this.orientation.equals(other.orientation)) return false;
|
|
443
|
+
if (!this.size.equals(other.size)) return false;
|
|
444
|
+
if (this.tessellation !== other.tessellation) return false;
|
|
445
|
+
|
|
446
|
+
// strict identity is enough for sampler equality in the common case;
|
|
447
|
+
// fall through to value equality so two shapes built from independent
|
|
448
|
+
// but identical sampler instances still compare equal
|
|
449
|
+
if (this.sampler === other.sampler) return true;
|
|
450
|
+
|
|
451
|
+
if (this.sampler === null || other.sampler === null) return false;
|
|
452
|
+
|
|
453
|
+
return this.sampler.equals(other.sampler);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
hash() {
|
|
457
|
+
const a = this.orientation.hash();
|
|
458
|
+
const b = this.size.hash();
|
|
459
|
+
const c = this.sampler !== null ? this.sampler.hash() : 0;
|
|
460
|
+
|
|
461
|
+
let h = (a * 31 + b) | 0;
|
|
462
|
+
h = (h * 31 + c) | 0;
|
|
463
|
+
h = (h * 31 + this.tessellation) | 0;
|
|
464
|
+
|
|
465
|
+
return h;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Fast type-check marker, matching the pattern on every other concrete
|
|
471
|
+
* AbstractShape3D subclass. The physics narrowphase reads this to dispatch
|
|
472
|
+
* the heightmap-vs-X grid-traversal path.
|
|
473
|
+
* @readonly
|
|
474
|
+
* @type {boolean}
|
|
475
|
+
*/
|
|
476
|
+
HeightMapShape3D.prototype.isHeightMapShape3D = true;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Heightmaps are non-convex: the solid volume bounded by an arbitrary
|
|
480
|
+
* height-field has valleys and overhangs that break GJK's convex-Minkowski
|
|
481
|
+
* precondition. The narrowphase must use grid traversal + per-triangle
|
|
482
|
+
* GJK instead of feeding this shape's {@link support} into pair tests.
|
|
483
|
+
* @readonly
|
|
484
|
+
* @type {boolean}
|
|
485
|
+
*/
|
|
486
|
+
HeightMapShape3D.prototype.is_convex = false;
|