spoint 0.1.658 → 0.1.660
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/netcode/SnapshotBinFormat.js +87 -0
- package/src/netcode/SnapshotEncoder.js +8 -82
- package/src/physics/VehiclePhysics.js +337 -0
- package/src/physics/World.js +682 -991
package/src/physics/World.js
CHANGED
|
@@ -1,991 +1,682 @@
|
|
|
1
|
-
import { extractMeshFromGLB, extractMeshFromGLBAsync } from './GLBLoader.js'
|
|
2
|
-
import { CharacterManager } from './CharacterManager.js'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// no
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
objFilter
|
|
64
|
-
|
|
65
|
-
bpI
|
|
66
|
-
bpI.MapObjectToBroadPhaseLayer(
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
settings
|
|
70
|
-
settings.
|
|
71
|
-
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
this.
|
|
81
|
-
this._activationListener
|
|
82
|
-
this._activationListener.
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
ps
|
|
93
|
-
ps.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
J.
|
|
106
|
-
|
|
107
|
-
if (opts.
|
|
108
|
-
if (opts.
|
|
109
|
-
if (opts.
|
|
110
|
-
if (opts.
|
|
111
|
-
if (opts.
|
|
112
|
-
if (opts.
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// pool
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
else if (shapeType === '
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const
|
|
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
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
let
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
this._trimeshInflight.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
settings.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
heights.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
bi.
|
|
340
|
-
|
|
341
|
-
entity.
|
|
342
|
-
entity.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
bi.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
this.
|
|
389
|
-
this.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
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
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.bodyInterface.DeactivateBody
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
//
|
|
468
|
-
// '
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
settings
|
|
484
|
-
settings.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
settings
|
|
488
|
-
settings.
|
|
489
|
-
|
|
490
|
-
if (opts.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
settings
|
|
494
|
-
settings.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
settings.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
settings
|
|
501
|
-
settings.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this._constraints.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
return
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// RemoveConstraint's own native destructor chain drops the constraint's ref (which in turn drops
|
|
684
|
-
// its ref on the collision tester it holds) -- live-witnessed (this session's probe): a manual
|
|
685
|
-
// J.destroy(v.constraint) or J.destroy(v.tester) AFTER RemoveConstraint has already run is a real
|
|
686
|
-
// use-after-free ("memory access out of bounds" / "table index is out of bounds" WASM traps, not
|
|
687
|
-
// a benign double-free warning). Do NOT call J.destroy on either -- RemoveConstraint alone is the
|
|
688
|
-
// complete, correct teardown for both.
|
|
689
|
-
this.physicsSystem.RemoveConstraint(v.constraint)
|
|
690
|
-
} catch (e) { console.error('[physics] removeVehicle cleanup error:', e?.message || e) }
|
|
691
|
-
this._vehicles.delete(vehicleId)
|
|
692
|
-
return true
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// Real Jolt TrackedVehicleController -- vehicles-tracked-controller-follow-up, sibling to
|
|
696
|
-
// createWheeledVehicle above. jolt-physics 1.1.0's compiled WASM build was probed live this session
|
|
697
|
-
// (Object.keys(Jolt) against a real `import('jolt-physics/wasm-compat')`) and confirmed to export the
|
|
698
|
-
// full TrackedVehicleController/TrackedVehicleControllerSettings/VehicleTrack/VehicleTrackSettings
|
|
699
|
-
// surface, same undocumented-in-.d.ts situation as the wheeled case.
|
|
700
|
-
//
|
|
701
|
-
// MATERIAL DIFFERENCE from the wheeled settings shape (the row's own instruction to audit before
|
|
702
|
-
// assuming 1:1 parity): TrackedVehicleControllerSettings.mTracks is NOT a push_back-able vector like
|
|
703
|
-
// WheeledVehicleControllerSettings.mDifferentials -- it is a fixed C++ array of exactly 2
|
|
704
|
-
// VehicleTrackSettings (upstream Jolt: `VehicleTrackSettings mTracks[2]`), and VehicleTrackSettings
|
|
705
|
-
// itself has no public constructor (`new J.VehicleTrackSettings()` throws "no constructor in IDL").
|
|
706
|
-
// The embind wrapper exposes this as get_mTracks(index)/set_mTracks(index, value) COPY-semantics
|
|
707
|
-
// accessors (live-probed): get_mTracks(0) returns an independent mutable copy of track 0, mutating
|
|
708
|
-
// that copy does NOT affect get_mTracks(1)'s copy, and the mutated copy must be written back via
|
|
709
|
-
// set_mTracks(index, track) to take effect -- get-mutate-set, not get-and-keep-reference. Track index
|
|
710
|
-
// 0 = left, 1 = right (matches Jolt's own sample/doc convention and this wrapper's driveIdx classification
|
|
711
|
-
// below). Each track's mWheels IS a real push_back-able vector of wheel INDEXES (into mWheels on the
|
|
712
|
-
// parent VehicleConstraintSettings, same indexing convention as the wheeled mDifferentials wheel refs).
|
|
713
|
-
//
|
|
714
|
-
// wheelDefs: array of {position:[x,y,z] chassis-local, radius, width, suspensionMin, suspensionMax,
|
|
715
|
-
// maxBrakeTorque, side:'left'|'right', driven:bool}. Wheels use WheelSettingsTV (Tracked Vehicle) not
|
|
716
|
-
// WheelSettingsWV (Wheeled) -- no steer angle field (tracks steer via differential left/right ratio,
|
|
717
|
-
// not wheel-turn angle). At least one wheel per side must have driven:true set as that side's
|
|
718
|
-
// mDrivenWheel (the wheel index the engine torque/track tension is actually applied through) --
|
|
719
|
-
// otherwise, mirroring the wheeled mDifferentials-empty gotcha, a side with no explicit driven wheel
|
|
720
|
-
// silently defaults mDrivenWheel to 0 (this wrapper's own first-wheel-of-side fallback below), so a
|
|
721
|
-
// caller SHOULD mark one wheel per side driven:true rather than relying on the fallback.
|
|
722
|
-
createTrackedVehicle(chassisBodyId, wheelDefs, opts = {}) {
|
|
723
|
-
if (!this.physicsSystem) return null
|
|
724
|
-
const chassis = this._getBody(chassisBodyId); if (!chassis) return null
|
|
725
|
-
if (!Array.isArray(wheelDefs) || wheelDefs.length === 0) return null
|
|
726
|
-
const J = this.Jolt
|
|
727
|
-
let vcs = null, wheelSettingsList = [], constraint = null, tester = null, stepListener = null
|
|
728
|
-
try {
|
|
729
|
-
vcs = new J.VehicleConstraintSettings()
|
|
730
|
-
const up = opts.up || [0, 1, 0], fwd = opts.forward || [0, 0, 1]
|
|
731
|
-
vcs.mUp = new J.Vec3(up[0], up[1], up[2])
|
|
732
|
-
vcs.mForward = new J.Vec3(fwd[0], fwd[1], fwd[2])
|
|
733
|
-
if (opts.maxPitchRollAngle != null) vcs.mMaxPitchRollAngle = opts.maxPitchRollAngle
|
|
734
|
-
|
|
735
|
-
const wheelsArr = vcs.mWheels
|
|
736
|
-
const leftIdx = [], rightIdx = [], leftDrivenIdx = [], rightDrivenIdx = []
|
|
737
|
-
for (let i = 0; i < wheelDefs.length; i++) {
|
|
738
|
-
const w = wheelDefs[i] || {}
|
|
739
|
-
const ws = new J.WheelSettingsTV()
|
|
740
|
-
const p = w.position || [0, 0, 0]
|
|
741
|
-
ws.mPosition = new J.Vec3(p[0], p[1], p[2])
|
|
742
|
-
if (w.suspensionDirection) { const sd = w.suspensionDirection; ws.mSuspensionDirection = new J.Vec3(sd[0], sd[1], sd[2]) }
|
|
743
|
-
ws.mRadius = w.radius ?? 0.35
|
|
744
|
-
ws.mWidth = w.width ?? 0.4
|
|
745
|
-
ws.mSuspensionMinLength = w.suspensionMin ?? 0.3
|
|
746
|
-
ws.mSuspensionMaxLength = w.suspensionMax ?? 0.5
|
|
747
|
-
if (w.maxBrakeTorque != null) ws.mMaxBrakeTorque = w.maxBrakeTorque
|
|
748
|
-
wheelSettingsList.push(ws)
|
|
749
|
-
wheelsArr.push_back(ws)
|
|
750
|
-
// side classified explicitly (w.side) if given, else by local-X sign (negative = left, matching
|
|
751
|
-
// createWheeledVehicle's own left/right convention) -- same fallback discipline as the wheeled case.
|
|
752
|
-
const side = w.side || (p[0] < 0 ? 'left' : 'right')
|
|
753
|
-
if (side === 'left') { leftIdx.push(i); if (w.driven) leftDrivenIdx.push(i) }
|
|
754
|
-
else { rightIdx.push(i); if (w.driven) rightDrivenIdx.push(i) }
|
|
755
|
-
}
|
|
756
|
-
vcs.mWheels = wheelsArr
|
|
757
|
-
|
|
758
|
-
const controllerSettings = new J.TrackedVehicleControllerSettings()
|
|
759
|
-
if (opts.engine) {
|
|
760
|
-
if (opts.engine.maxTorque != null) controllerSettings.mEngine.mMaxTorque = opts.engine.maxTorque
|
|
761
|
-
if (opts.engine.maxRPM != null) controllerSettings.mEngine.mMaxRPM = opts.engine.maxRPM
|
|
762
|
-
if (opts.engine.minRPM != null) controllerSettings.mEngine.mMinRPM = opts.engine.minRPM
|
|
763
|
-
}
|
|
764
|
-
// opts.tracks lets a caller fully hand-author both tracks (explicit wheel-index lists / driven
|
|
765
|
-
// wheel / brake torque), matching createWheeledVehicle's opts.differentials override pattern.
|
|
766
|
-
// Default: classify by side above, driven wheel = first driven:true wheel on that side, or the
|
|
767
|
-
// side's first wheel if none was marked driven (fallback documented in the header comment).
|
|
768
|
-
// VehicleTrackSettings has NO public constructor (`new J.VehicleTrackSettings()` throws "no
|
|
769
|
-
// constructor in IDL", live-confirmed) -- the only way to get one is get_mTracks(index), which
|
|
770
|
-
// returns an independent mutable COPY (not a live reference; live-confirmed mutating index 0's
|
|
771
|
-
// copy does not affect index 1's), mutated in place then written back via set_mTracks(index, t).
|
|
772
|
-
const buildTrack = (trackIndex, idxList, drivenList, override) => {
|
|
773
|
-
const t = controllerSettings.get_mTracks(trackIndex)
|
|
774
|
-
const wv = t.mWheels
|
|
775
|
-
const list = (override && Array.isArray(override.wheels)) ? override.wheels : idxList
|
|
776
|
-
for (const wi of list) wv.push_back(wi)
|
|
777
|
-
t.mWheels = wv
|
|
778
|
-
const drivenWheel = override && override.drivenWheel != null ? override.drivenWheel : (drivenList[0] ?? idxList[0] ?? 0)
|
|
779
|
-
t.mDrivenWheel = drivenWheel
|
|
780
|
-
if ((override && override.maxBrakeTorque != null)) t.mMaxBrakeTorque = override.maxBrakeTorque
|
|
781
|
-
if ((override && override.differentialRatio != null)) t.mDifferentialRatio = override.differentialRatio
|
|
782
|
-
return t
|
|
783
|
-
}
|
|
784
|
-
const leftOverride = opts.tracks && opts.tracks.left
|
|
785
|
-
const rightOverride = opts.tracks && opts.tracks.right
|
|
786
|
-
const leftTrack = buildTrack(0, leftIdx, leftDrivenIdx, leftOverride)
|
|
787
|
-
controllerSettings.set_mTracks(0, leftTrack)
|
|
788
|
-
const rightTrack = buildTrack(1, rightIdx, rightDrivenIdx, rightOverride)
|
|
789
|
-
controllerSettings.set_mTracks(1, rightTrack)
|
|
790
|
-
// Deliberately NOT calling J.destroy(leftTrack)/J.destroy(rightTrack) here -- live-probed this
|
|
791
|
-
// session: destroying either track handle AFTER set_mTracks has copied it in corrupts Jolt's WASM
|
|
792
|
-
// state, surfacing as a "memory access out of bounds" RuntimeError on the NEXT physicsSystem.Step()
|
|
793
|
-
// call (not immediately, making it easy to misattribute) -- same failure-mode CLASS as the
|
|
794
|
-
// documented trimesh-ShapeResult and vehicle-constraint-teardown use-after-free lessons above, but
|
|
795
|
-
// here the correct fix is the opposite of those: never destroy at all rather than destroy-after-use,
|
|
796
|
-
// since get_mTracks(index) copy semantics mean these two small JS wrapper handles have no Jolt-side
|
|
797
|
-
// ref to release (unlike the constraint/tester RefTarget objects, which DO need RemoveConstraint).
|
|
798
|
-
vcs.mController = controllerSettings
|
|
799
|
-
|
|
800
|
-
constraint = new J.VehicleConstraint(chassis, vcs)
|
|
801
|
-
tester = new J.VehicleCollisionTesterRay(LAYER_DYNAMIC, new J.Vec3(up[0], up[1], up[2]))
|
|
802
|
-
constraint.SetVehicleCollisionTester(tester)
|
|
803
|
-
this.physicsSystem.AddConstraint(constraint)
|
|
804
|
-
stepListener = new J.VehicleConstraintStepListener(constraint)
|
|
805
|
-
this.physicsSystem.AddStepListener(stepListener)
|
|
806
|
-
|
|
807
|
-
const controller = J.castObject(constraint.GetController(), J.TrackedVehicleController)
|
|
808
|
-
const vid = (this._nextVehicleId = (this._nextVehicleId || 0) + 1)
|
|
809
|
-
if (!this._vehicles) this._vehicles = new Map()
|
|
810
|
-
this._vehicles.set(vid, { constraint, controller, tester, stepListener, chassisBodyId, wheelCount: wheelDefs.length, tracked: true })
|
|
811
|
-
return vid
|
|
812
|
-
} catch (e) {
|
|
813
|
-
console.error('[physics] createTrackedVehicle failed:', e?.message || e)
|
|
814
|
-
return null
|
|
815
|
-
} finally { if (vcs) J.destroy(vcs) }
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// driverInput for a tracked vehicle: forward in -1..1 (throttle/reverse), leftRatio/rightRatio in
|
|
819
|
-
// -1..1 (per-track power ratio -- equal ratios drive straight, differing ratios steer/pivot, matching
|
|
820
|
-
// Jolt's own TrackedVehicleController::SetDriverInput(forward, leftRatio, rightRatio, brake) signature
|
|
821
|
-
// live-confirmed via the probe this session), brake in 0..1. Deliberately a DIFFERENT shape from
|
|
822
|
-
// setVehicleDriverInput's forward/right/brake/handbrake (the row's own instruction: tracks steer via
|
|
823
|
-
// per-side power ratio, not a wheel-turn angle, so a shared signature would be misleading).
|
|
824
|
-
setTrackedVehicleDriverInput(vehicleId, forward, leftRatio, rightRatio, brake = 0) {
|
|
825
|
-
const v = this._vehicles && this._vehicles.get(vehicleId); if (!v || !v.tracked) return false
|
|
826
|
-
// Same sleeping-body wake fix as setVehicleDriverInput above -- see that method's header comment
|
|
827
|
-
// for the full live-reproduced finding (a settled/sleeping vehicle ignores driver input silently
|
|
828
|
-
// with zero error until its body is explicitly reactivated).
|
|
829
|
-
if ((forward || leftRatio || rightRatio || brake) && this.bodyInterface?.ActivateBody) {
|
|
830
|
-
const b = this._getBody(v.chassisBodyId); if (b) this.bodyInterface.ActivateBody(b.GetID())
|
|
831
|
-
}
|
|
832
|
-
v.controller.SetDriverInput(forward, leftRatio, rightRatio, brake)
|
|
833
|
-
return true
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
enqueueAdd(shapeType, params, position, motionType, opts, onAdded) {
|
|
837
|
-
this._bodyQueue.push({ op: 'add', shapeType, params, position, motionType, opts: opts || {}, onAdded })
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
enqueueRemove(id, force = false) {
|
|
841
|
-
this._bodyQueue.push({ op: 'remove', id, force })
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// drainBodyQueue must run before physics.step() each tick: adds before removes.
|
|
845
|
-
drainBodyQueue() {
|
|
846
|
-
const q = this._bodyQueue
|
|
847
|
-
if (q.length === 0) return 0
|
|
848
|
-
this._bodyQueue = []
|
|
849
|
-
let applied = 0
|
|
850
|
-
for (let i = 0; i < q.length; i++) {
|
|
851
|
-
const r = q[i]; if (r.op !== 'add') continue
|
|
852
|
-
try { const id = this.addBody(r.shapeType, r.params, r.position, r.motionType, r.opts); if (r.onAdded) r.onAdded(id); applied++ }
|
|
853
|
-
catch (e) { console.error('[physics] queued add error:', e?.message || e); if (r.onAdded) try { r.onAdded(null) } catch (_) {} }
|
|
854
|
-
}
|
|
855
|
-
for (let i = 0; i < q.length; i++) {
|
|
856
|
-
const r = q[i]; if (r.op !== 'remove') continue
|
|
857
|
-
try { this.removeBody(r.id, r.force); applied++ }
|
|
858
|
-
catch (e) { console.error('[physics] queued remove error:', e?.message || e) }
|
|
859
|
-
}
|
|
860
|
-
return applied
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
get bodyQueueLength() { return this._bodyQueue.length }
|
|
864
|
-
|
|
865
|
-
setTrunkColliderIds(set) { return (this._trunkColliderIds = set) }
|
|
866
|
-
getTrunkColliderIds() { return this._trunkColliderIds }
|
|
867
|
-
setRockColliderIds(set) { return (this._rockColliderIds = set) }
|
|
868
|
-
getRockColliderIds() { return this._rockColliderIds }
|
|
869
|
-
setTerrainBodyId(id) { return (this._terrainBodyId = id) }
|
|
870
|
-
getTerrainBodyId() { return this._terrainBodyId ?? null }
|
|
871
|
-
setTerrainHeightSource(fn, frame, offsetY = 0) { this._terrainHeightAt = fn; this._planetFrame = frame; this._terrainOffsetY = offsetY }
|
|
872
|
-
getTerrainHeightFn() { return this._terrainHeightAt }
|
|
873
|
-
getTerrainOffsetY() { return this._terrainOffsetY || 0 }
|
|
874
|
-
terrainHeightAt(x, z) { return typeof this._terrainHeightAt === 'function' ? this._terrainHeightAt(x, z) + (this._terrainOffsetY || 0) : null }
|
|
875
|
-
|
|
876
|
-
// collisionSteps is Jolt's own real Step(deltaTime, inCollisionSteps) sub-stepping parameter --
|
|
877
|
-
// more collision steps per physics tick catch fast-moving bodies that would otherwise tunnel
|
|
878
|
-
// through thin colliders within a single tick's motion. Default stays 2 (unchanged from before
|
|
879
|
-
// this option existed) since quadrupling it unconditionally for every world would be a real,
|
|
880
|
-
// needless per-tick cost for the common case (most bodies are slow enough that 2 is already
|
|
881
|
-
// sufficient) -- a caller with genuinely fast projectiles/characters (the CCD-policy-per-entity-
|
|
882
|
-
// class need this pairs with) passes a higher value explicitly instead.
|
|
883
|
-
step(dt, collisionSteps = 2) { if (this.jolt) this.jolt.Step(dt, collisionSteps) }
|
|
884
|
-
|
|
885
|
-
removeBody(id, force = false) {
|
|
886
|
-
const b = this._getBody(id); if (!b) return
|
|
887
|
-
const sk = !force && this._bodyShapeKey.get(id)
|
|
888
|
-
if (sk) {
|
|
889
|
-
// Force-deactivate a DYNAMIC body on park (see addBody's pool-hit revive comment above for the
|
|
890
|
-
// measured cost of NOT doing this): merely repositioning with DontActivate does not stop an
|
|
891
|
-
// already-active body from continuing to simulate/fall at the park position for however long it
|
|
892
|
-
// sits pooled. Static/kinematic park (the pool's original terrain-collider use case) is unaffected
|
|
893
|
-
// -- those never simulate dynamics regardless of active/inactive state.
|
|
894
|
-
const isDynamic = this.bodyMeta.get(id)?.type === 'dynamic'
|
|
895
|
-
this._repositionBody(id, _PARK_POS, null, isDynamic ? false : null)
|
|
896
|
-
if (isDynamic) { this.setBodyVelocity(id, [0, 0, 0]); this.setBodyAngularVelocity(id, [0, 0, 0]) }
|
|
897
|
-
let free = this._bodyPool.get(sk); if (!free) this._bodyPool.set(sk, free = [])
|
|
898
|
-
free.push(id)
|
|
899
|
-
return
|
|
900
|
-
}
|
|
901
|
-
this.bodyInterface.RemoveBody(b.GetID()); this.bodyInterface.DestroyBody(b.GetID())
|
|
902
|
-
this.bodies.delete(id); this.bodyMeta.delete(id); this.bodyIds.delete(id); this._bodyShapeKey.delete(id)
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
asyncQuery(queries) {
|
|
906
|
-
if (!Array.isArray(queries) || queries.length === 0) return Promise.resolve([])
|
|
907
|
-
return new Promise(resolve => {
|
|
908
|
-
if (!this._asyncQueryQueue) this._asyncQueryQueue = []
|
|
909
|
-
if (!this._asyncQueryResolves) this._asyncQueryResolves = []
|
|
910
|
-
const idx = this._asyncQueryQueue.length
|
|
911
|
-
this._asyncQueryQueue.push(queries)
|
|
912
|
-
this._asyncQueryResolves.push(resolve)
|
|
913
|
-
if (!this._asyncQueryScheduled) {
|
|
914
|
-
this._asyncQueryScheduled = true
|
|
915
|
-
Promise.resolve().then(() => {
|
|
916
|
-
this._asyncQueryScheduled = false
|
|
917
|
-
const batch = this._asyncQueryQueue.splice(0)
|
|
918
|
-
const resolvers = this._asyncQueryResolves.splice(0)
|
|
919
|
-
const results = batch.map(qs => qs.map(q => {
|
|
920
|
-
try {
|
|
921
|
-
return this.raycast(q.origin, q.direction, q.maxDistance || 1000, q.excludeBodyId)
|
|
922
|
-
} catch (e) {
|
|
923
|
-
return { hit: false, distance: q.maxDistance || 1000, body: null, position: null, error: e.message }
|
|
924
|
-
}
|
|
925
|
-
}))
|
|
926
|
-
for (let i = 0; i < resolvers.length; i++) resolvers[i](results[i])
|
|
927
|
-
})
|
|
928
|
-
}
|
|
929
|
-
})
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
raycast(origin, direction, maxDistance = 1000, excludeBodyId = null) {
|
|
933
|
-
if (!this.physicsSystem) return { hit: false, distance: maxDistance, body: null, position: null }
|
|
934
|
-
const J = this.Jolt
|
|
935
|
-
const len = Math.hypot(direction[0], direction[1], direction[2])
|
|
936
|
-
const dir = len > 0 ? [direction[0]/len, direction[1]/len, direction[2]/len] : direction
|
|
937
|
-
const ray = new J.RRayCast(new J.RVec3(origin[0], origin[1], origin[2]), new J.Vec3(dir[0]*maxDistance, dir[1]*maxDistance, dir[2]*maxDistance))
|
|
938
|
-
const rs = new J.RayCastSettings(), col = new J.CastRayClosestHitCollisionCollector()
|
|
939
|
-
const bp = new J.DefaultBroadPhaseLayerFilter(this.jolt.GetObjectVsBroadPhaseLayerFilter(), LAYER_DYNAMIC)
|
|
940
|
-
const ol = new J.DefaultObjectLayerFilter(this.jolt.GetObjectLayerPairFilter(), LAYER_DYNAMIC)
|
|
941
|
-
const eb = excludeBodyId != null ? this._getBody(excludeBodyId) : null
|
|
942
|
-
const bf = eb ? new J.IgnoreSingleBodyFilter(eb.GetID()) : new J.BodyFilter()
|
|
943
|
-
const sf = new J.ShapeFilter()
|
|
944
|
-
this.physicsSystem.GetNarrowPhaseQuery().CastRay(ray, rs, col, bp, ol, bf, sf)
|
|
945
|
-
let result
|
|
946
|
-
if (col.HadHit()) {
|
|
947
|
-
const hit = col.get_mHit()
|
|
948
|
-
const dist = hit.mFraction * maxDistance
|
|
949
|
-
const position = [origin[0]+dir[0]*dist, origin[1]+dir[1]*dist, origin[2]+dir[2]*dist]
|
|
950
|
-
// Resolve the hit body back to a World body id -- the World id IS the Jolt
|
|
951
|
-
// GetIndexAndSequenceNumber() (see addBody), so this keys the same bodyMeta / the runtime's
|
|
952
|
-
// _physicsBodyToEntityId reverse map directly. Callers get an ATTRIBUTED hit (which entity/body),
|
|
953
|
-
// not just a point -- this is the primitive that makes shoot/click-a-target games authorable.
|
|
954
|
-
let bodyId = null, normal = null
|
|
955
|
-
try {
|
|
956
|
-
const bid = hit.mBodyID
|
|
957
|
-
if (bid) bodyId = bid.GetIndexAndSequenceNumber()
|
|
958
|
-
// Surface normal at the hit point (world space), for oriented decals / bounce / aim feedback.
|
|
959
|
-
const b = bodyId != null ? this._getBody(bodyId) : null
|
|
960
|
-
if (b) {
|
|
961
|
-
this._tmpRVec3.Set(position[0], position[1], position[2])
|
|
962
|
-
const n = b.GetWorldSpaceSurfaceNormal(hit.mSubShapeID2, this._tmpRVec3)
|
|
963
|
-
normal = [n.GetX(), n.GetY(), n.GetZ()]
|
|
964
|
-
J.destroy(n)
|
|
965
|
-
}
|
|
966
|
-
} catch (_) { /* normal/body extraction is best-effort; position always returns */ }
|
|
967
|
-
result = { hit: true, distance: dist, body: null, bodyId, normal, position }
|
|
968
|
-
} else result = { hit: false, distance: maxDistance, body: null, bodyId: null, normal: null, position: null }
|
|
969
|
-
J.destroy(ray); J.destroy(rs); J.destroy(col); J.destroy(bp); J.destroy(ol); J.destroy(bf); J.destroy(sf)
|
|
970
|
-
return result
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
destroy() {
|
|
974
|
-
if (!this.Jolt) return
|
|
975
|
-
this._charMgr.destroy()
|
|
976
|
-
if (this._vehicles) for (const [id] of this._vehicles) this.removeVehicle(id)
|
|
977
|
-
for (const [id] of this.bodies) this.removeBody(id, true)
|
|
978
|
-
this._bodyPool.clear(); this._bodyShapeKey.clear()
|
|
979
|
-
this._trimeshCache.clear(); this._trimeshInflight.clear()
|
|
980
|
-
const J = this.Jolt
|
|
981
|
-
if (this._tmpVec3) { J.destroy(this._tmpVec3); this._tmpVec3 = null }
|
|
982
|
-
if (this._tmpRVec3) { J.destroy(this._tmpRVec3); this._tmpRVec3 = null }
|
|
983
|
-
if (this._tmpQuat) { J.destroy(this._tmpQuat); this._tmpQuat = null }
|
|
984
|
-
if (this._bulkOutP) { J.destroy(this._bulkOutP); this._bulkOutP = null }
|
|
985
|
-
if (this._bulkOutR) { J.destroy(this._bulkOutR); this._bulkOutR = null }
|
|
986
|
-
if (this._bulkOutLV) { J.destroy(this._bulkOutLV); this._bulkOutLV = null }
|
|
987
|
-
if (this._bulkOutAV) { J.destroy(this._bulkOutAV); this._bulkOutAV = null }
|
|
988
|
-
if (this.jolt) { J.destroy(this.jolt); this.jolt = null }
|
|
989
|
-
this.physicsSystem = null; this.bodyInterface = null
|
|
990
|
-
}
|
|
991
|
-
}
|
|
1
|
+
import { extractMeshFromGLB, extractMeshFromGLBAsync } from './GLBLoader.js'
|
|
2
|
+
import { CharacterManager } from './CharacterManager.js'
|
|
3
|
+
import { installVehiclePhysics } from './VehiclePhysics.js'
|
|
4
|
+
import { buildConvexShape, buildTrimeshShape } from './ShapeBuilder.js'
|
|
5
|
+
|
|
6
|
+
const LAYER_STATIC = 0, LAYER_DYNAMIC = 1, NUM_LAYERS = 2
|
|
7
|
+
const _PARK_POS = [0, -100000, 0]
|
|
8
|
+
let joltInstance = null
|
|
9
|
+
export async function getJolt() {
|
|
10
|
+
if (!joltInstance) {
|
|
11
|
+
// Edge-target seam (edge-cf-durable-object-transport-adapter-real-websocketpair): a Cloudflare
|
|
12
|
+
// Durable Object has no node:fs (so the Node branch's `jolt-physics/wasm-compat` import is right
|
|
13
|
+
// for AppRuntime.js's own isNode checks generally, but jolt-physics's OWN bundled Emscripten glue
|
|
14
|
+
// independently re-checks `process.versions.node` and crashes on `createRequire(import.meta.url)`
|
|
15
|
+
// when `nodejs_compat` is enabled -- live-reproduced via a real `wrangler dev` workerd instance)
|
|
16
|
+
// and no static URL to fetch the browser branch's `/node_modules/...` path from either (workerd has
|
|
17
|
+
// no filesystem route to serve that string, live-reproduced as a bundler-time unresolvable dynamic
|
|
18
|
+
// import). Real fix (proven live against workerd): the edge worker imports jolt-physics/wasm's
|
|
19
|
+
// native `.wasm` module binding at BUILD TIME (the only embedder-allowed way to get compiled Wasm
|
|
20
|
+
// into a Worker -- ahead-of-time compiled, not runtime `WebAssembly.instantiate()` from raw bytes,
|
|
21
|
+
// which workerd's embedder policy blocks outright) and instantiates it itself via Emscripten's
|
|
22
|
+
// standard `Module.instantiateWasm` hook (checked before either of Jolt's own broken internal
|
|
23
|
+
// branches run), then stashes the resulting live Jolt module here before any PhysicsWorld boots --
|
|
24
|
+
// see edge/cf-do/spoint-do.js's initJoltForEdge(). This is a pure opt-in: unset in every existing
|
|
25
|
+
// Node/browser boot path, so both of those branches are byte-unchanged from before this fix.
|
|
26
|
+
if (typeof globalThis.__SPOINT_EDGE_JOLT__ !== 'undefined') {
|
|
27
|
+
joltInstance = await globalThis.__SPOINT_EDGE_JOLT__
|
|
28
|
+
return joltInstance
|
|
29
|
+
}
|
|
30
|
+
const _isNode = typeof process !== 'undefined' && process.versions?.node
|
|
31
|
+
// Specifier built at runtime (not a literal passed straight to import()) so an edge/DO bundler
|
|
32
|
+
// build (esbuild via wrangler) never tries to statically resolve the browser-only absolute
|
|
33
|
+
// '/node_modules/...' path -- it isn't reachable there anyway (see the __SPOINT_EDGE_JOLT__
|
|
34
|
+
// early-return above), but a bundler's static import-graph walk doesn't know that; it fails the
|
|
35
|
+
// WHOLE build on an unresolvable literal specifier regardless of runtime reachability. Zero
|
|
36
|
+
// behavior change for Node/browser: same two real specifiers, same ternary choice, just built as
|
|
37
|
+
// a string first (live-confirmed via a real wrangler --dry-run build that this defeats esbuild's
|
|
38
|
+
// static resolution while a literal ternary-in-import() does not).
|
|
39
|
+
const _joltSpec = _isNode ? 'jolt-physics/wasm-compat' : ('/node_modules/' + 'jolt-physics/dist/jolt-physics.wasm.js')
|
|
40
|
+
const { default: init } = await import(_joltSpec)
|
|
41
|
+
joltInstance = await init()
|
|
42
|
+
}
|
|
43
|
+
return joltInstance
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class PhysicsWorld {
|
|
47
|
+
constructor(config = {}) {
|
|
48
|
+
this.gravity = config.gravity || [0, -9.81, 0]
|
|
49
|
+
this.Jolt = null; this.jolt = null; this.physicsSystem = null; this.bodyInterface = null
|
|
50
|
+
this.bodies = new Map(); this.bodyMeta = new Map(); this.bodyIds = new Map()
|
|
51
|
+
this._objFilter = null; this._ovbp = null
|
|
52
|
+
this._shapeCache = new Map(); this._convexQueue = Promise.resolve()
|
|
53
|
+
this._trimeshCache = new Map(); this._trimeshInflight = new Map()
|
|
54
|
+
this._bodyPool = new Map(); this._bodyShapeKey = new Map()
|
|
55
|
+
this._bodyQueue = []
|
|
56
|
+
this._tmpVec3 = null; this._tmpRVec3 = null
|
|
57
|
+
this._bulkOutP = null; this._bulkOutR = null; this._bulkOutLV = null; this._bulkOutAV = null
|
|
58
|
+
this._charMgr = new CharacterManager(this.gravity, config.crouchHalfHeight || 0.45)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async init() {
|
|
62
|
+
const J = await getJolt(); this.Jolt = J
|
|
63
|
+
const objFilter = new J.ObjectLayerPairFilterTable(NUM_LAYERS)
|
|
64
|
+
objFilter.EnableCollision(LAYER_STATIC, LAYER_DYNAMIC); objFilter.EnableCollision(LAYER_DYNAMIC, LAYER_DYNAMIC)
|
|
65
|
+
const bpI = new J.BroadPhaseLayerInterfaceTable(NUM_LAYERS, 2)
|
|
66
|
+
bpI.MapObjectToBroadPhaseLayer(LAYER_STATIC, new J.BroadPhaseLayer(0))
|
|
67
|
+
bpI.MapObjectToBroadPhaseLayer(LAYER_DYNAMIC, new J.BroadPhaseLayer(1))
|
|
68
|
+
const ovbp = new J.ObjectVsBroadPhaseLayerFilterTable(bpI, 2, objFilter, NUM_LAYERS)
|
|
69
|
+
const settings = new J.JoltSettings()
|
|
70
|
+
settings.mObjectLayerPairFilter = objFilter; settings.mBroadPhaseLayerInterface = bpI
|
|
71
|
+
settings.mObjectVsBroadPhaseLayerFilter = ovbp
|
|
72
|
+
this._objFilter = objFilter; this._ovbp = ovbp
|
|
73
|
+
this.jolt = new J.JoltInterface(settings); J.destroy(settings)
|
|
74
|
+
this.physicsSystem = this.jolt.GetPhysicsSystem(); this.bodyInterface = this.physicsSystem.GetBodyInterface()
|
|
75
|
+
this._tmpVec3 = new J.Vec3(0, 0, 0); this._tmpRVec3 = new J.RVec3(0, 0, 0); this._tmpQuat = new J.Quat(0, 0, 0, 1)
|
|
76
|
+
this._bulkOutP = new J.RVec3(0, 0, 0); this._bulkOutR = new J.Quat(0, 0, 0, 1)
|
|
77
|
+
this._bulkOutLV = new J.Vec3(0, 0, 0); this._bulkOutAV = new J.Vec3(0, 0, 0)
|
|
78
|
+
const [gx, gy, gz] = this.gravity
|
|
79
|
+
const gv = new J.Vec3(gx, gy, gz); this.physicsSystem.SetGravity(gv); J.destroy(gv)
|
|
80
|
+
this._heap32 = new Int32Array(J.HEAP8.buffer)
|
|
81
|
+
this._activationListener = new J.BodyActivationListenerJS()
|
|
82
|
+
this._activationListener.OnBodyActivated = (ptr) => { if (this.onBodyActivated) this.onBodyActivated(this._heap32[ptr >> 2]) }
|
|
83
|
+
this._activationListener.OnBodyDeactivated = (ptr) => { if (this.onBodyDeactivated) this.onBodyDeactivated(this._heap32[ptr >> 2]) }
|
|
84
|
+
this.physicsSystem.SetBodyActivationListener(this._activationListener)
|
|
85
|
+
// Aggressive body-sleep tuning: Jolt's own defaults (mTimeBeforeSleep=0.5s, mPointVelocitySleepThreshold=0.03)
|
|
86
|
+
// were left untouched -- for a large scene of mostly-static-once-settled dynamic props (the 30k-model
|
|
87
|
+
// budget target) a shorter settle time + slightly higher velocity floor means far more of the active-body
|
|
88
|
+
// set self-sleeps via Jolt's own island-based sleep logic BEFORE the hard-activation-ring/global-budget
|
|
89
|
+
// logic in AppRuntimePhysics even has to intervene -- the two mechanisms are complementary, not redundant:
|
|
90
|
+
// this lowers the steady-state active count, the ring/budget logic bounds the worst case under load.
|
|
91
|
+
if (typeof this.physicsSystem.GetPhysicsSettings === 'function' && typeof this.physicsSystem.SetPhysicsSettings === 'function') {
|
|
92
|
+
const ps = this.physicsSystem.GetPhysicsSettings()
|
|
93
|
+
ps.mTimeBeforeSleep = 0.25 // was Jolt default 0.5s -- settle twice as fast
|
|
94
|
+
ps.mPointVelocitySleepThreshold = 0.05 // was Jolt default 0.03 -- sleep at a slightly higher residual jitter
|
|
95
|
+
this.physicsSystem.SetPhysicsSettings(ps)
|
|
96
|
+
}
|
|
97
|
+
this._charMgr.init(J, this.jolt, this.physicsSystem)
|
|
98
|
+
return this
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_addBody(shape, position, motionType, layer, opts = {}) {
|
|
102
|
+
const J = this.Jolt
|
|
103
|
+
const pos = new J.RVec3(position[0], position[1], position[2])
|
|
104
|
+
const rot = opts.rotation ? new J.Quat(...opts.rotation) : new J.Quat(0, 0, 0, 1)
|
|
105
|
+
const cs = new J.BodyCreationSettings(shape, pos, rot, motionType, layer)
|
|
106
|
+
J.destroy(pos); J.destroy(rot)
|
|
107
|
+
if (opts.mass) { cs.mMassPropertiesOverride.mMass = opts.mass; cs.mOverrideMassProperties = J.EOverrideMassProperties_CalculateInertia }
|
|
108
|
+
if (opts.friction !== undefined) cs.mFriction = opts.friction
|
|
109
|
+
if (opts.restitution !== undefined) cs.mRestitution = opts.restitution // bounciness 0..1
|
|
110
|
+
if (opts.gravityFactor !== undefined) cs.mGravityFactor = opts.gravityFactor // 0 = float, <0 = anti-gravity
|
|
111
|
+
if (opts.linearDamping !== undefined) cs.mLinearDamping = opts.linearDamping
|
|
112
|
+
if (opts.angularDamping !== undefined) cs.mAngularDamping = opts.angularDamping
|
|
113
|
+
if (opts.linearCast) cs.mMotionQuality = J.EMotionQuality_LinearCast
|
|
114
|
+
const activate = motionType === J.EMotionType_Static ? J.EActivation_DontActivate : J.EActivation_Activate
|
|
115
|
+
const body = this.bodyInterface.CreateBody(cs); this.bodyInterface.AddBody(body.GetID(), activate)
|
|
116
|
+
J.destroy(cs)
|
|
117
|
+
this._createCount = (this._createCount | 0) + 1
|
|
118
|
+
const id = body.GetID().GetIndexAndSequenceNumber()
|
|
119
|
+
this.bodies.set(id, body); this.bodyMeta.set(id, opts.meta || {}); this.bodyIds.set(id, body.GetID())
|
|
120
|
+
if (opts.shapeKey) this._bodyShapeKey.set(id, opts.shapeKey)
|
|
121
|
+
return id
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
addStaticBox(halfExtents, position, rotation) {
|
|
125
|
+
const J = this.Jolt
|
|
126
|
+
const hv = new J.Vec3(halfExtents[0], halfExtents[1], halfExtents[2])
|
|
127
|
+
const bs = new J.BoxShape(hv, 0.05, null); J.destroy(hv)
|
|
128
|
+
return this._addBody(bs, position, J.EMotionType_Static, LAYER_STATIC, { rotation, meta: { type: 'static', shape: 'box' } })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// activate: null (default) = EActivation_DontActivate (original behavior, correct for the STATIC
|
|
132
|
+
// shapeKey pool users this was written for -- terrain colliders etc, which never simulate dynamics
|
|
133
|
+
// either way). Pass true/false explicitly to force-activate or force-deactivate a DYNAMIC body being
|
|
134
|
+
// parked/revived through the pool -- see removeBody/addBody's pool paths below, and the header comment
|
|
135
|
+
// on why a dynamic body needs this (a merely-repositioned park with DontActivate does NOT deactivate an
|
|
136
|
+
// already-active body -- it keeps simulating/falling forever at the park position, a real measured
|
|
137
|
+
// per-tick cost live-witnessed while pooling destructible debris: a "parked" dynamic body fell
|
|
138
|
+
// continuously the whole time it sat in the pool, 6.46m of drift across 1s of ticks in one probe).
|
|
139
|
+
_repositionBody(id, position, rotation, activate = null) {
|
|
140
|
+
const b = this._getBody(id); if (!b) return
|
|
141
|
+
this._tmpRVec3.Set(position[0], position[1], position[2])
|
|
142
|
+
const act = activate === true ? this.Jolt.EActivation_Activate : this.Jolt.EActivation_DontActivate
|
|
143
|
+
if (rotation) {
|
|
144
|
+
this._tmpQuat.Set(rotation[0], rotation[1], rotation[2], rotation[3])
|
|
145
|
+
this.bodyInterface.SetPositionAndRotation(b.GetID(), this._tmpRVec3, this._tmpQuat, act)
|
|
146
|
+
} else {
|
|
147
|
+
this.bodyInterface.SetPosition(b.GetID(), this._tmpRVec3, act)
|
|
148
|
+
}
|
|
149
|
+
if (activate === false && this.bodyInterface.DeactivateBody) this.bodyInterface.DeactivateBody(b.GetID())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
addBody(shapeType, params, position, motionType, opts = {}) {
|
|
153
|
+
const J = this.Jolt; let shape
|
|
154
|
+
const sk = opts.shapeKey || null
|
|
155
|
+
if (sk) {
|
|
156
|
+
const free = this._bodyPool.get(sk)
|
|
157
|
+
if (free && free.length) {
|
|
158
|
+
const id = free.pop()
|
|
159
|
+
// Dynamic revive: reactivate + wipe stale linear/angular velocity from the piece's PREVIOUS life
|
|
160
|
+
// (live-witnessed carrying over: a body removeBody'd mid-fall at -4.8m/s kept that exact velocity
|
|
161
|
+
// into its next life at a totally different position, a real correctness bug for pooled debris --
|
|
162
|
+
// a freshly "destroyed" piece would otherwise inherit whatever momentum the last occupant of this
|
|
163
|
+
// pool slot happened to have when it despawned). Static/kinematic reuse (terrain colliders, the
|
|
164
|
+
// pool's original use case) is unaffected since motionType there is never 'dynamic'.
|
|
165
|
+
const isDynamic = motionType === 'dynamic'
|
|
166
|
+
this._repositionBody(id, position, opts.rotation, isDynamic ? true : null)
|
|
167
|
+
if (isDynamic) {
|
|
168
|
+
this.setBodyVelocity(id, [0, 0, 0])
|
|
169
|
+
this.setBodyAngularVelocity(id, [0, 0, 0])
|
|
170
|
+
}
|
|
171
|
+
return id
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (shapeType === 'box') {
|
|
175
|
+
const bk = opts.shapeKey || null
|
|
176
|
+
if (bk && this._shapeCache.has(bk)) shape = this._shapeCache.get(bk)
|
|
177
|
+
else { const cr = Math.min(0.05, Math.min(params[0], params[1], params[2]) * 0.1); const bv = new J.Vec3(params[0], params[1], params[2]); shape = new J.BoxShape(bv, cr, null); J.destroy(bv); if (bk) this._shapeCache.set(bk, shape) }
|
|
178
|
+
}
|
|
179
|
+
else if (shapeType === 'sphere') shape = new J.SphereShape(params)
|
|
180
|
+
else if (shapeType === 'capsule') {
|
|
181
|
+
const ck = opts.shapeKey || null
|
|
182
|
+
if (ck && this._shapeCache.has(ck)) shape = this._shapeCache.get(ck)
|
|
183
|
+
else { shape = new J.CapsuleShape(params[1], params[0]); if (ck) this._shapeCache.set(ck, shape) }
|
|
184
|
+
}
|
|
185
|
+
else if (shapeType === 'convex') {
|
|
186
|
+
// sr must outlive the _addBody call that consumes cvxShape -- see ShapeBuilder.js's buildConvexShape
|
|
187
|
+
// header comment (a real, live-reproduced WASM state-corruption bug found+fixed while wiring
|
|
188
|
+
// destructibles-fractured-glb-shape-wiring's dynamic convex debris bodies).
|
|
189
|
+
const { shape: cvxShape, sr } = buildConvexShape(J, params, this._shapeCache, opts.shapeKey || null)
|
|
190
|
+
const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
|
|
191
|
+
const id = this._addBody(cvxShape, position, mt, motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC, { ...opts, meta: { type: motionType, shape: shapeType } })
|
|
192
|
+
if (sr) J.destroy(sr)
|
|
193
|
+
return id
|
|
194
|
+
}
|
|
195
|
+
else return null
|
|
196
|
+
const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
|
|
197
|
+
return this._addBody(shape, position, mt, motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC, { ...opts, meta: { type: motionType, shape: shapeType } })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
preallocatePool(shapeType, params, shapeKey, count) {
|
|
201
|
+
if (!this.bodyInterface || !shapeKey || !(count > 0)) return 0
|
|
202
|
+
let free = this._bodyPool.get(shapeKey); if (!free) this._bodyPool.set(shapeKey, free = [])
|
|
203
|
+
const need = count - free.length
|
|
204
|
+
if (need <= 0) return 0
|
|
205
|
+
const ids = []
|
|
206
|
+
for (let i = 0; i < need; i++) {
|
|
207
|
+
const id = this.addBody(shapeType, params, _PARK_POS, 'static', { shapeKey })
|
|
208
|
+
if (id == null) break
|
|
209
|
+
ids.push(id)
|
|
210
|
+
}
|
|
211
|
+
for (const id of ids) { this._repositionBody(id, _PARK_POS, null); free.push(id) }
|
|
212
|
+
return ids.length
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
addConvexBodyAsync(params, position, motionType, opts = {}) {
|
|
216
|
+
const J = this.Jolt, cacheKey = opts.shapeKey || null
|
|
217
|
+
if (cacheKey && this._shapeCache.has(cacheKey)) {
|
|
218
|
+
const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
|
|
219
|
+
return Promise.resolve(this._addBody(this._shapeCache.get(cacheKey), position, mt, motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC, { ...opts, meta: { type: motionType, shape: 'convex' } }))
|
|
220
|
+
}
|
|
221
|
+
const result = this._convexQueue.then(() => {
|
|
222
|
+
// sr must outlive the _addBody call below -- see ShapeBuilder.js's buildConvexShape header comment.
|
|
223
|
+
const { shape, sr } = buildConvexShape(J, params, this._shapeCache, cacheKey)
|
|
224
|
+
const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
|
|
225
|
+
const id = this._addBody(shape, position, mt, motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC, { ...opts, meta: { type: motionType, shape: 'convex' } })
|
|
226
|
+
if (sr) J.destroy(sr)
|
|
227
|
+
return id
|
|
228
|
+
})
|
|
229
|
+
this._convexQueue = result.then(() => {}, () => {}); return result
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Shape caching/welding: a static trimesh cooked from a GLB (extractAllMeshesFromGLBAsync + Jolt
|
|
233
|
+
// MeshShapeSettings.Create()) is real, measurable per-call cost -- full mesh extraction plus native
|
|
234
|
+
// triangle-list construction -- yet maps commonly place the SAME model many times (rocks, crates,
|
|
235
|
+
// barrels, props). Every prior call re-extracted and re-cooked from scratch even for an identical
|
|
236
|
+
// glbPath+scale pair. Cache key is glbPath+scale (buildTrimeshShape pre-scales vertices into world
|
|
237
|
+
// space, so two different scales of the same model genuinely need two different cooked shapes; a
|
|
238
|
+
// rotation-only difference does NOT, since rotation is applied at the body level via _addBody's
|
|
239
|
+
// BodyCreationSettings, not baked into the shape). The cached Shape is a real Jolt-side ref-counted
|
|
240
|
+
// object (Shape.AddRef/Release/GetRefCount, confirmed in jolt-physics.wasm-compat.d.ts) -- sharing one
|
|
241
|
+
// cooked shape across many bodies is Jolt's own supported "welding" pattern, same trust level as the
|
|
242
|
+
// pre-existing box/capsule/convex shapeKey cache in addBody/buildConvexShape above (which also never
|
|
243
|
+
// destroys a cached shape, relying on Jolt's own refcounting under each BodyCreationSettings/body).
|
|
244
|
+
// In-flight dedupe (_trimeshInflight) additionally prevents two concurrent placements of the same
|
|
245
|
+
// model+scale from racing two independent cook operations before either populates the cache.
|
|
246
|
+
async addStaticTrimeshAsync(glbPath, meshIndex = 0, position = [0, 0, 0], scale = [1, 1, 1], rotation = [0, 0, 0, 1]) {
|
|
247
|
+
if (!glbPath) throw new Error('addStaticTrimeshAsync: no glbPath (resolveAssetPath rejected or returned an empty path)')
|
|
248
|
+
const J = this.Jolt
|
|
249
|
+
const key = `${glbPath}|${scale[0]},${scale[1]},${scale[2]}`
|
|
250
|
+
let shape = this._trimeshCache.get(key)
|
|
251
|
+
let srToDestroyAfterFirstUse = null
|
|
252
|
+
if (!shape) {
|
|
253
|
+
let inflight = this._trimeshInflight.get(key)
|
|
254
|
+
if (!inflight) {
|
|
255
|
+
inflight = buildTrimeshShape(J, glbPath, scale).then(built => {
|
|
256
|
+
this._trimeshCache.set(key, built.shape)
|
|
257
|
+
this._trimeshInflight.delete(key)
|
|
258
|
+
return built
|
|
259
|
+
}, err => { this._trimeshInflight.delete(key); throw err })
|
|
260
|
+
this._trimeshInflight.set(key, inflight)
|
|
261
|
+
}
|
|
262
|
+
const built = await inflight
|
|
263
|
+
shape = built.shape
|
|
264
|
+
// Live-witnessed hard rule (WASM "null function or function signature mismatch" crash on the
|
|
265
|
+
// NEXT distinct trimesh add otherwise): the ShapeResult (`sr`) must be destroyed only AFTER a
|
|
266
|
+
// real _addBody call has consumed/reffed the Shape it wraps -- destroying it any earlier (e.g.
|
|
267
|
+
// inside the .then() before the first body exists) corrupts Jolt's WASM state for subsequent
|
|
268
|
+
// shape creation, even though `shape` itself looks like a valid JS object at that point.
|
|
269
|
+
// `built` is the SAME object handed to every concurrent `await inflight` caller (a resolved
|
|
270
|
+
// Promise shares its value, it does not clone it) -- when N callers raced the same fresh key
|
|
271
|
+
// (the in-flight-dedupe case _trimeshInflight exists for), naively checking `built.sr` would
|
|
272
|
+
// have every one of them see it truthy and each call J.destroy(built.sr), a double-destroy of
|
|
273
|
+
// the same native object. Null it out on first claim so only ONE of the N awaiters (whichever
|
|
274
|
+
// microtask runs first, harmless which) actually owns and performs the destroy.
|
|
275
|
+
if (built.sr) { srToDestroyAfterFirstUse = built.sr; built.sr = null }
|
|
276
|
+
}
|
|
277
|
+
const id = this._addBody(shape, position, J.EMotionType_Static, LAYER_STATIC, { rotation, meta: { type: 'static', shape: 'trimesh', shapeKey: key } })
|
|
278
|
+
if (srToDestroyAfterFirstUse) J.destroy(srToDestroyAfterFirstUse)
|
|
279
|
+
return id
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
addHeightField(samples, sampleCount, scale, position) {
|
|
283
|
+
const J = this.Jolt
|
|
284
|
+
const settings = new J.HeightFieldShapeSettings()
|
|
285
|
+
const offset = new J.Vec3(0, 0, 0); settings.set_mOffset(offset); J.destroy(offset)
|
|
286
|
+
const sv = new J.Vec3(scale[0], scale[1], scale[2]); settings.set_mScale(sv); J.destroy(sv)
|
|
287
|
+
settings.set_mSampleCount(sampleCount)
|
|
288
|
+
if (typeof settings.set_mBlockSize === 'function') settings.set_mBlockSize(2)
|
|
289
|
+
const heights = settings.get_mHeightSamples()
|
|
290
|
+
heights.resize(samples.length)
|
|
291
|
+
let bulkOk = false
|
|
292
|
+
if (typeof heights.data === 'function' && typeof J.getPointer === 'function' && J.HEAPF32) {
|
|
293
|
+
const ref = heights.data()
|
|
294
|
+
const ptr = J.getPointer(ref)
|
|
295
|
+
if (ptr) {
|
|
296
|
+
const view = samples instanceof Float32Array ? samples : Float32Array.from(samples)
|
|
297
|
+
J.HEAPF32.set(view, ptr >> 2)
|
|
298
|
+
bulkOk = true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!bulkOk) {
|
|
302
|
+
heights.clear(); heights.reserve(samples.length)
|
|
303
|
+
for (let i = 0; i < samples.length; i++) heights.push_back(samples[i])
|
|
304
|
+
}
|
|
305
|
+
const sr = settings.Create()
|
|
306
|
+
if (!sr.IsValid()) { console.error('[heightfield] shape invalid:', sr.GetError()); J.destroy(settings); J.destroy(sr); return null }
|
|
307
|
+
const shape = sr.Get()
|
|
308
|
+
const id = this._addBody(shape, position, J.EMotionType_Static, LAYER_STATIC, { meta: { type: 'static', shape: 'heightfield' } })
|
|
309
|
+
J.destroy(settings); J.destroy(sr)
|
|
310
|
+
return id
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
addStaticTrimeshFromData(entityId,v,ix,pos,rot=[0,0,0,1]){const J=this.Jolt,tc=ix.length/3,tl=new J.TriangleList(),f3=new J.Float3(0,0,0);tl.resize(tc);for(let t=0;t<tc;t++){const tri=tl.at(t);for(let k=0;k<3;k++){const i=ix[t*3+k];f3.x=v[i*3];f3.y=v[i*3+1];f3.z=v[i*3+2];tri.set_mV(k,f3)}}const ms=new J.MeshShapeSettings(tl),sr=ms.Create();if(!sr.IsValid()){console.error('[trimesh] shape invalid for',entityId,sr.GetError());J.destroy(f3);J.destroy(tl);J.destroy(ms);return null}const shape=sr.Get();J.destroy(f3);J.destroy(tl);const id=this._addBody(shape,pos,J.EMotionType_Static,LAYER_STATIC,{rotation:rot,meta:{type:'static',shape:'trimesh'}});J.destroy(ms);J.destroy(sr);console.log('[trimesh] body created for',entityId,'id='+id,'tris='+tc);return id}
|
|
314
|
+
|
|
315
|
+
addPlayerCharacter(radius, halfHeight, position, mass) { return this._charMgr.addCharacter(radius, halfHeight, position, mass) }
|
|
316
|
+
setCharacterCrouch(id, v) { this._charMgr.setCrouch(id, v) }
|
|
317
|
+
updateCharacter(id, dt) { this._charMgr.update(id, dt) }
|
|
318
|
+
getCharacterPosition(id) { return this._charMgr.getPosition(id) }
|
|
319
|
+
readCharacterPosition(id, out) { this._charMgr.readPosition(id, out) }
|
|
320
|
+
getCharacterVelocity(id) { return this._charMgr.getVelocity(id) }
|
|
321
|
+
readCharacterVelocity(id, out) { this._charMgr.readVelocity(id, out) }
|
|
322
|
+
setCharacterVelocity(id, v) { this._charMgr.setVelocity(id, v) }
|
|
323
|
+
setCharacterPosition(id, p) { this._charMgr.setPosition(id, p) }
|
|
324
|
+
getCharacterGroundState(id) { return this._charMgr.getGroundState(id) }
|
|
325
|
+
removeCharacter(id) { this._charMgr.removeCharacter(id) }
|
|
326
|
+
get characters() { return this._charMgr.characters }
|
|
327
|
+
// Rollback-netcode primitive, character-body half of snapshotBodies/restoreBodies (players use
|
|
328
|
+
// CharacterVirtual, not regular Jolt bodies -- see CharacterManager.js's own snapshotAll/restoreAll
|
|
329
|
+
// header comment for why only position+velocity round-trip).
|
|
330
|
+
snapshotCharacters() { return this._charMgr.snapshotAll() }
|
|
331
|
+
restoreCharacters(snap) { this._charMgr.restoreAll(snap) }
|
|
332
|
+
|
|
333
|
+
_getBody(id) { return this.bodies.get(id) }
|
|
334
|
+
isBodyActive(id) { const b = this._getBody(id); return b ? b.IsActive() : false }
|
|
335
|
+
|
|
336
|
+
syncDynamicBody(bodyId, entity) {
|
|
337
|
+
const b = this._getBody(bodyId); if (!b || !b.IsActive()) return false
|
|
338
|
+
const id = this.bodyIds.get(bodyId), bi = this.bodyInterface
|
|
339
|
+
bi.GetPositionAndRotation(id, this._bulkOutP, this._bulkOutR)
|
|
340
|
+
bi.GetLinearAndAngularVelocity(id, this._bulkOutLV, this._bulkOutAV)
|
|
341
|
+
entity.position[0] = this._bulkOutP.GetX(); entity.position[1] = this._bulkOutP.GetY(); entity.position[2] = this._bulkOutP.GetZ()
|
|
342
|
+
entity.rotation[0] = this._bulkOutR.GetX(); entity.rotation[1] = this._bulkOutR.GetY(); entity.rotation[2] = this._bulkOutR.GetZ(); entity.rotation[3] = this._bulkOutR.GetW()
|
|
343
|
+
entity.velocity[0] = this._bulkOutLV.GetX(); entity.velocity[1] = this._bulkOutLV.GetY(); entity.velocity[2] = this._bulkOutLV.GetZ()
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Rollback-netcode primitive (rollback-netcode-ggpo-style-input-rollback first slice): capture every
|
|
348
|
+
// non-static body's full dynamics state (position, rotation, linear+angular velocity) for later exact
|
|
349
|
+
// restore, the save/rewind half of a GGPO-style save-state -> resimulate-forward loop. Static bodies
|
|
350
|
+
// (terrain, placed props with autoTrimesh, etc) are skipped entirely -- by construction a static body
|
|
351
|
+
// never moves under simulation, so capturing/restoring it is pure waste on every single rollback save,
|
|
352
|
+
// which per this row's own architecture happens on a tight per-tick budget. Uses the SAME
|
|
353
|
+
// GetPositionAndRotation/GetLinearAndAngularVelocity bulk-read convention syncDynamicBody already
|
|
354
|
+
// proved safe every tick in production (see the getBodyPosition/getBodyRotation header comment above
|
|
355
|
+
// for why the two single-field getters are NOT safe to call back-to-back -- this reuses the safe path).
|
|
356
|
+
snapshotBodies() {
|
|
357
|
+
const out = new Map()
|
|
358
|
+
const bi = this.bodyInterface
|
|
359
|
+
for (const [id, meta] of this.bodyMeta) {
|
|
360
|
+
if (meta && meta.type === 'static') continue
|
|
361
|
+
const jid = this.bodyIds.get(id); if (!jid) continue
|
|
362
|
+
bi.GetPositionAndRotation(jid, this._bulkOutP, this._bulkOutR)
|
|
363
|
+
bi.GetLinearAndAngularVelocity(jid, this._bulkOutLV, this._bulkOutAV)
|
|
364
|
+
out.set(id, {
|
|
365
|
+
position: [this._bulkOutP.GetX(), this._bulkOutP.GetY(), this._bulkOutP.GetZ()],
|
|
366
|
+
rotation: [this._bulkOutR.GetX(), this._bulkOutR.GetY(), this._bulkOutR.GetZ(), this._bulkOutR.GetW()],
|
|
367
|
+
velocity: [this._bulkOutLV.GetX(), this._bulkOutLV.GetY(), this._bulkOutLV.GetZ()],
|
|
368
|
+
angularVelocity: [this._bulkOutAV.GetX(), this._bulkOutAV.GetY(), this._bulkOutAV.GetZ()],
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
return out
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Restores exactly the bodies present in `snap` (a Map from snapshotBodies, or a plain object with the
|
|
375
|
+
// same per-entry shape for a wire-deserialized snapshot). A body present in `snap` but since removed
|
|
376
|
+
// from the live world (removeBody'd between save and rollback -- e.g. a debris piece that despawned) is
|
|
377
|
+
// silently skipped, matching CharacterManager.restoreAll's same-set assumption: a rollback caller always
|
|
378
|
+
// restores against the identical body population it saved, so this is a defensive skip, not a real path.
|
|
379
|
+
// EActivation_Activate: a rolled-back body must be simulating again even if the pre-restore Jolt state
|
|
380
|
+
// happened to have it asleep (a resimulate pass needs every body live for the physics.step() calls that
|
|
381
|
+
// follow, or Jolt will not integrate a sleeping body and the resimulation silently diverges from a truly
|
|
382
|
+
// deterministic replay where that body was awake throughout).
|
|
383
|
+
restoreBodies(snap) {
|
|
384
|
+
const bi = this.bodyInterface, J = this.Jolt
|
|
385
|
+
const entries = snap instanceof Map ? snap.entries() : Object.entries(snap)
|
|
386
|
+
for (const [idKey, s] of entries) {
|
|
387
|
+
const id = typeof idKey === 'number' ? idKey : Number(idKey)
|
|
388
|
+
const jid = this.bodyIds.get(id); if (!jid) continue
|
|
389
|
+
this._bulkOutP.Set(s.position[0], s.position[1], s.position[2])
|
|
390
|
+
this._bulkOutR.Set(s.rotation[0], s.rotation[1], s.rotation[2], s.rotation[3])
|
|
391
|
+
bi.SetPositionAndRotation(jid, this._bulkOutP, this._bulkOutR, J.EActivation_Activate)
|
|
392
|
+
this._bulkOutLV.Set(s.velocity[0], s.velocity[1], s.velocity[2])
|
|
393
|
+
this._bulkOutAV.Set(s.angularVelocity[0], s.angularVelocity[1], s.angularVelocity[2])
|
|
394
|
+
bi.SetLinearAndAngularVelocity(jid, this._bulkOutLV, this._bulkOutAV)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// NOTE: routed through GetPositionAndRotation + the pre-allocated, never-destroyed _bulkOutP/_bulkOutR
|
|
399
|
+
// scratch pair (the same buffers syncDynamicBody already used safely), NOT the single-field
|
|
400
|
+
// GetPosition/GetRotation calls the two used to make independently. Real bug found+fixed this session
|
|
401
|
+
// (deterministic-simulation-jolt-fixed-point-rollback probe): calling getBodyPosition(id) then
|
|
402
|
+
// getBodyRotation(id) for the same body in the same tick -- in EITHER order, even across two separate
|
|
403
|
+
// loops over the same body set (not just interleaved per-body) -- crashed with a real, 100% reproducible
|
|
404
|
+
// "RuntimeError: memory access out of bounds" WASM trap, live-isolated down to a single dynamic body,
|
|
405
|
+
// first tick, fresh process (not a multi-world/heap-accumulation artifact). Root cause: GetPosition's and
|
|
406
|
+
// GetRotation's own embind wrappers each return a value via an embind by-value-return convention that,
|
|
407
|
+
// like the already-documented GetAngularVelocity buffer below, is NOT safe to Jolt.destroy() when a sibling
|
|
408
|
+
// getter's return value is live in the same synchronous scope -- calling BOTH getters (each individually
|
|
409
|
+
// safe when called alone, confirmed via a 600-tick isolation run) then destroying either return value
|
|
410
|
+
// corrupts shared WASM-side state the other getter's wrapper also touches. GetPositionAndRotation's own
|
|
411
|
+
// out-param convention was already proven safe under the identical 24-body/600-tick stress (syncDynamicBody
|
|
412
|
+
// uses it every tick in production) -- reusing it here fixes both getters without changing either's public
|
|
413
|
+
// signature or return shape. This is a real fix, not exemption: nothing new is heap-allocated per call to
|
|
414
|
+
// the reused _bulkOutP/_bulkOutR pair, same discipline as getBodyAngularVelocity's no-destroy fix.
|
|
415
|
+
getBodyPosition(id) { const b = this._getBody(id); if (!b) return [0,0,0]; this.bodyInterface.GetPositionAndRotation(b.GetID(), this._bulkOutP, this._bulkOutR); return [this._bulkOutP.GetX(),this._bulkOutP.GetY(),this._bulkOutP.GetZ()] }
|
|
416
|
+
getBodyRotation(id) { const b = this._getBody(id); if (!b) return [0,0,0,1]; this.bodyInterface.GetPositionAndRotation(b.GetID(), this._bulkOutP, this._bulkOutR); return [this._bulkOutR.GetX(),this._bulkOutR.GetY(),this._bulkOutR.GetZ(),this._bulkOutR.GetW()] }
|
|
417
|
+
getBodyVelocity(id) { const b = this._getBody(id); if (!b) return [0,0,0]; const v = this.bodyInterface.GetLinearVelocity(b.GetID()); const r=[v.GetX(),v.GetY(),v.GetZ()]; this.Jolt.destroy(v); return r }
|
|
418
|
+
// NOTE: deliberately does NOT Jolt.destroy() the returned Vec3, unlike every sibling getter above.
|
|
419
|
+
// Live-reproduced real bug (destructibles-debris-lifetime-lod session): BodyInterface.GetAngularVelocity's
|
|
420
|
+
// embind wrapper returns a reference into a Jolt-internal reusable temp buffer (not a fresh heap Vec3 the
|
|
421
|
+
// way GetPosition/GetRotation/GetLinearVelocity's OWN return values behave when called in isolation) --
|
|
422
|
+
// destroying it here, then calling GetLinearVelocity (or GetAngularVelocity again) in the SAME tick during
|
|
423
|
+
// a body's collision-response step, corrupted that shared buffer: a real "RuntimeError: memory access out
|
|
424
|
+
// of bounds" WASM trap, deterministically reproduced at the exact tick a falling body first contacts the
|
|
425
|
+
// ground (collision resolution touches the same internal velocity buffer Jolt is about to hand back out).
|
|
426
|
+
// Isolated via paired probes: GetLinearVelocity-only (destroyed every tick) survives 500 ticks fine;
|
|
427
|
+
// GetAngularVelocity-only (destroyed every tick) ALSO survives fine; only the INTERLEAVED linear+angular
|
|
428
|
+
// sequence in one tick crashes -- and skipping the destroy() on angular's result alone (leaving linear's
|
|
429
|
+
// existing destroy() untouched) fully fixes it. A one-time-per-call skipped destroy on a reused Jolt-side
|
|
430
|
+
// temp buffer is not a real leak (nothing new is allocated per call to begin with).
|
|
431
|
+
getBodyAngularVelocity(id) { const b = this._getBody(id); if (!b || !this.bodyInterface.GetAngularVelocity) return [0,0,0]; const v = this.bodyInterface.GetAngularVelocity(b.GetID()); return [v.GetX(),v.GetY(),v.GetZ()] }
|
|
432
|
+
setBodyFriction(id, f) { const b = this._getBody(id); if (!b || !this.bodyInterface.SetFriction) return false; this.bodyInterface.SetFriction(b.GetID(), f); return true }
|
|
433
|
+
setBodyRestitution(id, r) { const b = this._getBody(id); if (!b || !this.bodyInterface.SetRestitution) return false; this.bodyInterface.SetRestitution(b.GetID(), r); return true }
|
|
434
|
+
setBodyPosition(id, p) { const b = this._getBody(id); if (!b) return; this._tmpRVec3.Set(p[0],p[1],p[2]); this.bodyInterface.SetPosition(b.GetID(), this._tmpRVec3, this.Jolt.EActivation_Activate) }
|
|
435
|
+
// Flip an existing body's Jolt motion type in place (Dynamic<->Kinematic) for the hard-activation-ring
|
|
436
|
+
// 30-100m tier -- reuses the same body/shape rather than destroy+recreate, so a ring crossing is one
|
|
437
|
+
// Jolt call instead of a full shape rebuild. EActivation_DontActivate: caller decides activation separately.
|
|
438
|
+
setBodyMotionType(id, motionType) {
|
|
439
|
+
const b = this._getBody(id); if (!b || !this.bodyInterface.SetMotionType) return false
|
|
440
|
+
const J = this.Jolt
|
|
441
|
+
const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
|
|
442
|
+
this.bodyInterface.SetMotionType(b.GetID(), mt, J.EActivation_DontActivate)
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
// Proximity-priority sleep: put an active body to sleep without destroying it (cheap to reactivate,
|
|
446
|
+
// unlike removeBody which frees the Jolt shape). DeactivateBody fires the same OnBodyDeactivated
|
|
447
|
+
// listener a natural velocity-threshold sleep would, so AppRuntimePhysics' bookkeeping (active/sleeping
|
|
448
|
+
// sets) stays correct via the existing listener, no separate code path needed downstream.
|
|
449
|
+
deactivateBody(id) {
|
|
450
|
+
const b = this._getBody(id); if (!b || !this.bodyInterface.DeactivateBody) return false
|
|
451
|
+
this.bodyInterface.DeactivateBody(b.GetID())
|
|
452
|
+
return true
|
|
453
|
+
}
|
|
454
|
+
setBodyVelocity(id, v) { const b = this._getBody(id); if (!b) return; this._tmpVec3.Set(v[0],v[1],v[2]); this.bodyInterface.SetLinearVelocity(b.GetID(), this._tmpVec3) }
|
|
455
|
+
// Undocumented-in-.d.ts but real, compiled-WASM-confirmed binding (same class of gap as the
|
|
456
|
+
// Vehicle* surface -- see project/vehicles-jolt-constraint-available-not-just-twobody in AGENTS.md).
|
|
457
|
+
// Needed to fully reset a REUSED dynamic body (pooled debris revival): SetLinearVelocity alone leaves
|
|
458
|
+
// stale angular velocity/spin from the body's PREVIOUS life on the pooled Jolt body, since Jolt does
|
|
459
|
+
// not reset angular velocity as a side effect of SetPosition/SetLinearVelocity.
|
|
460
|
+
setBodyAngularVelocity(id, v) { const b = this._getBody(id); if (!b || !this.bodyInterface.SetAngularVelocity) return false; this._tmpVec3.Set(v[0],v[1],v[2]); this.bodyInterface.SetAngularVelocity(b.GetID(), this._tmpVec3); return true }
|
|
461
|
+
addForce(id, f) { const b = this._getBody(id); if (!b) return; this._tmpVec3.Set(f[0],f[1],f[2]); this.bodyInterface.AddForce(b.GetID(), this._tmpVec3) }
|
|
462
|
+
// Optional worldPoint applies the impulse OFF-CENTRE (Jolt AddImpulse(id, impulse, point)) so it
|
|
463
|
+
// imparts spin/torque -- a ball curves, a kick tumbles a prop. Without it the impulse is centre-of-mass.
|
|
464
|
+
addImpulse(id, im, worldPoint) { const b = this._getBody(id); if (!b) return; this._tmpVec3.Set(im[0],im[1],im[2]); if (worldPoint) { this._tmpRVec3.Set(worldPoint[0],worldPoint[1],worldPoint[2]); this.bodyInterface.AddImpulse(b.GetID(), this._tmpVec3, this._tmpRVec3) } else this.bodyInterface.AddImpulse(b.GetID(), this._tmpVec3) }
|
|
465
|
+
setBodyGravityFactor(id, f) { const b = this._getBody(id); if (!b || typeof f !== 'number' || !Number.isFinite(f)) return; this.bodyInterface.SetGravityFactor(b.GetID(), f) }
|
|
466
|
+
|
|
467
|
+
// Join two bodies with a Jolt TwoBodyConstraint. type: 'fixed' (weld -- lock relative transform),
|
|
468
|
+
// 'point' (ball joint -- share a point, free rotation), 'distance' (rigid rod between anchors),
|
|
469
|
+
// 'hinge' (rotate about an axis). anchorA/anchorB are WORLD-space attach points (default both bodies'
|
|
470
|
+
// current positions). Returns a constraintId for removeConstraint, or null if a body is unknown.
|
|
471
|
+
addConstraint(bodyIdA, bodyIdB, opts = {}) {
|
|
472
|
+
if (!this.physicsSystem) return null
|
|
473
|
+
const ba = this._getBody(bodyIdA), bb = this._getBody(bodyIdB)
|
|
474
|
+
if (!ba || !bb) return null
|
|
475
|
+
const J = this.Jolt, type = opts.type || 'fixed'
|
|
476
|
+
const pa = this.bodyInterface.GetPosition(ba.GetID()), pb = this.bodyInterface.GetPosition(bb.GetID())
|
|
477
|
+
const aA = opts.anchorA || [pa.GetX(), pa.GetY(), pa.GetZ()]
|
|
478
|
+
const aB = opts.anchorB || [pb.GetX(), pb.GetY(), pb.GetZ()]
|
|
479
|
+
J.destroy(pa); J.destroy(pb)
|
|
480
|
+
let settings = null
|
|
481
|
+
try {
|
|
482
|
+
if (type === 'point') {
|
|
483
|
+
settings = new J.PointConstraintSettings()
|
|
484
|
+
settings.mSpace = J.EConstraintSpace_WorldSpace
|
|
485
|
+
settings.mPoint1 = new J.RVec3(aA[0], aA[1], aA[2]); settings.mPoint2 = new J.RVec3(aB[0], aB[1], aB[2])
|
|
486
|
+
} else if (type === 'distance') {
|
|
487
|
+
settings = new J.DistanceConstraintSettings()
|
|
488
|
+
settings.mSpace = J.EConstraintSpace_WorldSpace
|
|
489
|
+
settings.mPoint1 = new J.RVec3(aA[0], aA[1], aA[2]); settings.mPoint2 = new J.RVec3(aB[0], aB[1], aB[2])
|
|
490
|
+
if (opts.minDistance != null) settings.mMinDistance = opts.minDistance
|
|
491
|
+
if (opts.maxDistance != null) settings.mMaxDistance = opts.maxDistance
|
|
492
|
+
} else if (type === 'hinge') {
|
|
493
|
+
settings = new J.HingeConstraintSettings()
|
|
494
|
+
settings.mSpace = J.EConstraintSpace_WorldSpace
|
|
495
|
+
settings.mPoint1 = new J.RVec3(aA[0], aA[1], aA[2]); settings.mPoint2 = new J.RVec3(aB[0], aB[1], aB[2])
|
|
496
|
+
const ax = opts.axis || [0, 1, 0]
|
|
497
|
+
settings.mHingeAxis1 = new J.Vec3(ax[0], ax[1], ax[2]); settings.mHingeAxis2 = new J.Vec3(ax[0], ax[1], ax[2])
|
|
498
|
+
settings.mNormalAxis1 = new J.Vec3(1, 0, 0); settings.mNormalAxis2 = new J.Vec3(1, 0, 0)
|
|
499
|
+
} else { // fixed / weld
|
|
500
|
+
settings = new J.FixedConstraintSettings()
|
|
501
|
+
settings.mSpace = J.EConstraintSpace_WorldSpace
|
|
502
|
+
settings.mPoint1 = new J.RVec3(aA[0], aA[1], aA[2]); settings.mPoint2 = new J.RVec3(aB[0], aB[1], aB[2])
|
|
503
|
+
}
|
|
504
|
+
const c = settings.Create(ba, bb)
|
|
505
|
+
this.physicsSystem.AddConstraint(c)
|
|
506
|
+
const cid = (this._nextConstraintId = (this._nextConstraintId || 0) + 1)
|
|
507
|
+
if (!this._constraints) this._constraints = new Map()
|
|
508
|
+
this._constraints.set(cid, c)
|
|
509
|
+
return cid
|
|
510
|
+
} catch (e) { console.error('[physics] addConstraint failed:', e?.message || e); return null }
|
|
511
|
+
finally { if (settings) J.destroy(settings) }
|
|
512
|
+
}
|
|
513
|
+
removeConstraint(constraintId) {
|
|
514
|
+
const c = this._constraints && this._constraints.get(constraintId)
|
|
515
|
+
if (!c || !this.physicsSystem) return false
|
|
516
|
+
this.physicsSystem.RemoveConstraint(c); this.Jolt.destroy(c); this._constraints.delete(constraintId)
|
|
517
|
+
return true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
// Vehicle constraint methods (createWheeledVehicle, createTrackedVehicle, driver-input, wheel
|
|
522
|
+
// accessors, removeVehicle) live in VehiclePhysics.js, mixed onto this prototype below the class
|
|
523
|
+
// body -- see that file's header comment for the full WASM-crash-avoidance rationale.
|
|
524
|
+
|
|
525
|
+
enqueueAdd(shapeType, params, position, motionType, opts, onAdded) {
|
|
526
|
+
this._bodyQueue.push({ op: 'add', shapeType, params, position, motionType, opts: opts || {}, onAdded })
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
enqueueRemove(id, force = false) {
|
|
530
|
+
this._bodyQueue.push({ op: 'remove', id, force })
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// drainBodyQueue must run before physics.step() each tick: adds before removes.
|
|
534
|
+
drainBodyQueue() {
|
|
535
|
+
const q = this._bodyQueue
|
|
536
|
+
if (q.length === 0) return 0
|
|
537
|
+
this._bodyQueue = []
|
|
538
|
+
let applied = 0
|
|
539
|
+
for (let i = 0; i < q.length; i++) {
|
|
540
|
+
const r = q[i]; if (r.op !== 'add') continue
|
|
541
|
+
try { const id = this.addBody(r.shapeType, r.params, r.position, r.motionType, r.opts); if (r.onAdded) r.onAdded(id); applied++ }
|
|
542
|
+
catch (e) { console.error('[physics] queued add error:', e?.message || e); if (r.onAdded) try { r.onAdded(null) } catch (_) {} }
|
|
543
|
+
}
|
|
544
|
+
for (let i = 0; i < q.length; i++) {
|
|
545
|
+
const r = q[i]; if (r.op !== 'remove') continue
|
|
546
|
+
try { this.removeBody(r.id, r.force); applied++ }
|
|
547
|
+
catch (e) { console.error('[physics] queued remove error:', e?.message || e) }
|
|
548
|
+
}
|
|
549
|
+
return applied
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
get bodyQueueLength() { return this._bodyQueue.length }
|
|
553
|
+
|
|
554
|
+
setTrunkColliderIds(set) { return (this._trunkColliderIds = set) }
|
|
555
|
+
getTrunkColliderIds() { return this._trunkColliderIds }
|
|
556
|
+
setRockColliderIds(set) { return (this._rockColliderIds = set) }
|
|
557
|
+
getRockColliderIds() { return this._rockColliderIds }
|
|
558
|
+
setTerrainBodyId(id) { return (this._terrainBodyId = id) }
|
|
559
|
+
getTerrainBodyId() { return this._terrainBodyId ?? null }
|
|
560
|
+
setTerrainHeightSource(fn, frame, offsetY = 0) { this._terrainHeightAt = fn; this._planetFrame = frame; this._terrainOffsetY = offsetY }
|
|
561
|
+
getTerrainHeightFn() { return this._terrainHeightAt }
|
|
562
|
+
getTerrainOffsetY() { return this._terrainOffsetY || 0 }
|
|
563
|
+
terrainHeightAt(x, z) { return typeof this._terrainHeightAt === 'function' ? this._terrainHeightAt(x, z) + (this._terrainOffsetY || 0) : null }
|
|
564
|
+
|
|
565
|
+
// collisionSteps is Jolt's own real Step(deltaTime, inCollisionSteps) sub-stepping parameter --
|
|
566
|
+
// more collision steps per physics tick catch fast-moving bodies that would otherwise tunnel
|
|
567
|
+
// through thin colliders within a single tick's motion. Default stays 2 (unchanged from before
|
|
568
|
+
// this option existed) since quadrupling it unconditionally for every world would be a real,
|
|
569
|
+
// needless per-tick cost for the common case (most bodies are slow enough that 2 is already
|
|
570
|
+
// sufficient) -- a caller with genuinely fast projectiles/characters (the CCD-policy-per-entity-
|
|
571
|
+
// class need this pairs with) passes a higher value explicitly instead.
|
|
572
|
+
step(dt, collisionSteps = 2) { if (this.jolt) this.jolt.Step(dt, collisionSteps) }
|
|
573
|
+
|
|
574
|
+
removeBody(id, force = false) {
|
|
575
|
+
const b = this._getBody(id); if (!b) return
|
|
576
|
+
const sk = !force && this._bodyShapeKey.get(id)
|
|
577
|
+
if (sk) {
|
|
578
|
+
// Force-deactivate a DYNAMIC body on park (see addBody's pool-hit revive comment above for the
|
|
579
|
+
// measured cost of NOT doing this): merely repositioning with DontActivate does not stop an
|
|
580
|
+
// already-active body from continuing to simulate/fall at the park position for however long it
|
|
581
|
+
// sits pooled. Static/kinematic park (the pool's original terrain-collider use case) is unaffected
|
|
582
|
+
// -- those never simulate dynamics regardless of active/inactive state.
|
|
583
|
+
const isDynamic = this.bodyMeta.get(id)?.type === 'dynamic'
|
|
584
|
+
this._repositionBody(id, _PARK_POS, null, isDynamic ? false : null)
|
|
585
|
+
if (isDynamic) { this.setBodyVelocity(id, [0, 0, 0]); this.setBodyAngularVelocity(id, [0, 0, 0]) }
|
|
586
|
+
let free = this._bodyPool.get(sk); if (!free) this._bodyPool.set(sk, free = [])
|
|
587
|
+
free.push(id)
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
this.bodyInterface.RemoveBody(b.GetID()); this.bodyInterface.DestroyBody(b.GetID())
|
|
591
|
+
this.bodies.delete(id); this.bodyMeta.delete(id); this.bodyIds.delete(id); this._bodyShapeKey.delete(id)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
asyncQuery(queries) {
|
|
595
|
+
if (!Array.isArray(queries) || queries.length === 0) return Promise.resolve([])
|
|
596
|
+
return new Promise(resolve => {
|
|
597
|
+
if (!this._asyncQueryQueue) this._asyncQueryQueue = []
|
|
598
|
+
if (!this._asyncQueryResolves) this._asyncQueryResolves = []
|
|
599
|
+
const idx = this._asyncQueryQueue.length
|
|
600
|
+
this._asyncQueryQueue.push(queries)
|
|
601
|
+
this._asyncQueryResolves.push(resolve)
|
|
602
|
+
if (!this._asyncQueryScheduled) {
|
|
603
|
+
this._asyncQueryScheduled = true
|
|
604
|
+
Promise.resolve().then(() => {
|
|
605
|
+
this._asyncQueryScheduled = false
|
|
606
|
+
const batch = this._asyncQueryQueue.splice(0)
|
|
607
|
+
const resolvers = this._asyncQueryResolves.splice(0)
|
|
608
|
+
const results = batch.map(qs => qs.map(q => {
|
|
609
|
+
try {
|
|
610
|
+
return this.raycast(q.origin, q.direction, q.maxDistance || 1000, q.excludeBodyId)
|
|
611
|
+
} catch (e) {
|
|
612
|
+
return { hit: false, distance: q.maxDistance || 1000, body: null, position: null, error: e.message }
|
|
613
|
+
}
|
|
614
|
+
}))
|
|
615
|
+
for (let i = 0; i < resolvers.length; i++) resolvers[i](results[i])
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
raycast(origin, direction, maxDistance = 1000, excludeBodyId = null) {
|
|
622
|
+
if (!this.physicsSystem) return { hit: false, distance: maxDistance, body: null, position: null }
|
|
623
|
+
const J = this.Jolt
|
|
624
|
+
const len = Math.hypot(direction[0], direction[1], direction[2])
|
|
625
|
+
const dir = len > 0 ? [direction[0]/len, direction[1]/len, direction[2]/len] : direction
|
|
626
|
+
const ray = new J.RRayCast(new J.RVec3(origin[0], origin[1], origin[2]), new J.Vec3(dir[0]*maxDistance, dir[1]*maxDistance, dir[2]*maxDistance))
|
|
627
|
+
const rs = new J.RayCastSettings(), col = new J.CastRayClosestHitCollisionCollector()
|
|
628
|
+
const bp = new J.DefaultBroadPhaseLayerFilter(this.jolt.GetObjectVsBroadPhaseLayerFilter(), LAYER_DYNAMIC)
|
|
629
|
+
const ol = new J.DefaultObjectLayerFilter(this.jolt.GetObjectLayerPairFilter(), LAYER_DYNAMIC)
|
|
630
|
+
const eb = excludeBodyId != null ? this._getBody(excludeBodyId) : null
|
|
631
|
+
const bf = eb ? new J.IgnoreSingleBodyFilter(eb.GetID()) : new J.BodyFilter()
|
|
632
|
+
const sf = new J.ShapeFilter()
|
|
633
|
+
this.physicsSystem.GetNarrowPhaseQuery().CastRay(ray, rs, col, bp, ol, bf, sf)
|
|
634
|
+
let result
|
|
635
|
+
if (col.HadHit()) {
|
|
636
|
+
const hit = col.get_mHit()
|
|
637
|
+
const dist = hit.mFraction * maxDistance
|
|
638
|
+
const position = [origin[0]+dir[0]*dist, origin[1]+dir[1]*dist, origin[2]+dir[2]*dist]
|
|
639
|
+
// Resolve the hit body back to a World body id -- the World id IS the Jolt
|
|
640
|
+
// GetIndexAndSequenceNumber() (see addBody), so this keys the same bodyMeta / the runtime's
|
|
641
|
+
// _physicsBodyToEntityId reverse map directly. Callers get an ATTRIBUTED hit (which entity/body),
|
|
642
|
+
// not just a point -- this is the primitive that makes shoot/click-a-target games authorable.
|
|
643
|
+
let bodyId = null, normal = null
|
|
644
|
+
try {
|
|
645
|
+
const bid = hit.mBodyID
|
|
646
|
+
if (bid) bodyId = bid.GetIndexAndSequenceNumber()
|
|
647
|
+
// Surface normal at the hit point (world space), for oriented decals / bounce / aim feedback.
|
|
648
|
+
const b = bodyId != null ? this._getBody(bodyId) : null
|
|
649
|
+
if (b) {
|
|
650
|
+
this._tmpRVec3.Set(position[0], position[1], position[2])
|
|
651
|
+
const n = b.GetWorldSpaceSurfaceNormal(hit.mSubShapeID2, this._tmpRVec3)
|
|
652
|
+
normal = [n.GetX(), n.GetY(), n.GetZ()]
|
|
653
|
+
J.destroy(n)
|
|
654
|
+
}
|
|
655
|
+
} catch (_) { /* normal/body extraction is best-effort; position always returns */ }
|
|
656
|
+
result = { hit: true, distance: dist, body: null, bodyId, normal, position }
|
|
657
|
+
} else result = { hit: false, distance: maxDistance, body: null, bodyId: null, normal: null, position: null }
|
|
658
|
+
J.destroy(ray); J.destroy(rs); J.destroy(col); J.destroy(bp); J.destroy(ol); J.destroy(bf); J.destroy(sf)
|
|
659
|
+
return result
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
destroy() {
|
|
663
|
+
if (!this.Jolt) return
|
|
664
|
+
this._charMgr.destroy()
|
|
665
|
+
if (this._vehicles) for (const [id] of this._vehicles) this.removeVehicle(id)
|
|
666
|
+
for (const [id] of this.bodies) this.removeBody(id, true)
|
|
667
|
+
this._bodyPool.clear(); this._bodyShapeKey.clear()
|
|
668
|
+
this._trimeshCache.clear(); this._trimeshInflight.clear()
|
|
669
|
+
const J = this.Jolt
|
|
670
|
+
if (this._tmpVec3) { J.destroy(this._tmpVec3); this._tmpVec3 = null }
|
|
671
|
+
if (this._tmpRVec3) { J.destroy(this._tmpRVec3); this._tmpRVec3 = null }
|
|
672
|
+
if (this._tmpQuat) { J.destroy(this._tmpQuat); this._tmpQuat = null }
|
|
673
|
+
if (this._bulkOutP) { J.destroy(this._bulkOutP); this._bulkOutP = null }
|
|
674
|
+
if (this._bulkOutR) { J.destroy(this._bulkOutR); this._bulkOutR = null }
|
|
675
|
+
if (this._bulkOutLV) { J.destroy(this._bulkOutLV); this._bulkOutLV = null }
|
|
676
|
+
if (this._bulkOutAV) { J.destroy(this._bulkOutAV); this._bulkOutAV = null }
|
|
677
|
+
if (this.jolt) { J.destroy(this.jolt); this.jolt = null }
|
|
678
|
+
this.physicsSystem = null; this.bodyInterface = null
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
installVehiclePhysics(PhysicsWorld)
|