Needle Engine

Changes between version 3.4.0-alpha and 3.5.0-alpha
Files changed (23) hide show
  1. plugins/vite/config.js +2 -1
  2. plugins/vite/editor-connection.js +37 -39
  3. plugins/vite/index.js +5 -1
  4. src/engine/codegen/register_types.js +2 -2
  5. plugins/vite/reload.js +3 -1
  6. src/engine/api.ts +1 -0
  7. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +51 -9
  8. src/engine-components/Collider.ts +6 -6
  9. src/engine/engine_context_registry.ts +13 -6
  10. src/engine/engine_context.ts +32 -23
  11. src/engine/engine_element_loading.ts +2 -3
  12. src/engine/engine_element.ts +2 -1
  13. src/engine/engine_input.ts +2 -2
  14. src/engine/engine_physics.ts +25 -1020
  15. src/engine/engine_physics.types.ts +1 -3
  16. src/engine/engine_types.ts +66 -4
  17. src/engine-components/Joints.ts +2 -2
  18. src/engine-components/ui/RectTransform.ts +3 -2
  19. src/engine-components/RigidBody.ts +24 -31
  20. src/engine-components/export/usdz/ThreeUSDZExporter.ts +62 -30
  21. plugins/vite/defines.js +30 -0
  22. plugins/vite/dependency-watcher.js +173 -0
  23. src/engine/engine_physics_rapier.ts +1127 -0
plugins/vite/config.js CHANGED
@@ -53,4 +53,5 @@
53
53
  /** "assets" -> the directory name inside the output directory to put e.g. glb files into */
54
54
  export function builtAssetsDirectory(){
55
55
  return "assets";
56
- }
56
+ }
57
+
plugins/vite/editor-connection.js CHANGED
@@ -73,48 +73,46 @@
73
73
  },
74
74
 
75
75
  configureServer(server) {
76
- server.ws.on('connection', (socket, _request) => {
77
-
78
- // console.log("Send editor sync status: " + isInstalled);
79
- const reply = {
80
- type: "needle:editor-sync:installation-status",
81
- data: isInstalled
82
- }
83
- socket.send(JSON.stringify(reply));
84
-
85
- socket.on('message', async (bytes) => {
86
- if (bytes?.length < 50) {
87
- const message = Buffer.from(bytes).toString();
88
- if (message === "needle:editor:restart") {
89
- console.log("Received request for a soft restart of the vite server... restarting in 1 second")
90
- setTimeout(() => {
76
+ try
77
+ {
78
+ server.ws.on('connection', (socket, _request) => {
79
+
80
+ // console.log("Send editor sync status: " + isInstalled);
81
+ const reply = {
82
+ type: "needle:editor-sync:installation-status",
83
+ data: isInstalled
84
+ }
85
+ socket.send(JSON.stringify(reply));
86
+
87
+ socket.on('message', async (bytes) => {
88
+ if (bytes?.length < 50) {
89
+ const message = Buffer.from(bytes).toString();
90
+ if (message === "needle:editor:restart") {
91
+ console.log("Received request for a soft restart of the vite server... ")
91
92
  // This just restarts the vite server
92
93
  server.restart();
93
- // TODO: restart isnt recommended right now because e.g. Unity doesnt properly find the new process to display it in the progress bar
94
- // spawn(process.argv.shift(), process.argv, {
95
- // cwd: process.cwd(),
96
- // detached: true,
97
- // stdio: "inherit"
98
- // });
99
- // process.exit();
100
- }, 1000);
94
+ }
95
+ else if (message === "needle:editor:stop") {
96
+ process.exit();
97
+ }
98
+ else if (message === `{"type":"ping"}`) {
99
+ socket.send(JSON.stringify({ type: "pong" }));
100
+ }
101
+ else if (message === "needle:editor:editor-sync-enabled") {
102
+ console.log("Editor sync enabled")
103
+ editorSyncEnabled = true;
104
+ }
105
+ else if (message === "needle:editor:editor-sync-disabled") {
106
+ editorSyncEnabled = false;
107
+ }
101
108
  }
102
- else if (message === "needle:editor:stop") {
103
- process.exit();
104
- }
105
- else if (message === `{"type":"ping"}`) {
106
- socket.send(JSON.stringify({ type: "pong" }));
107
- }
108
- else if (message === "needle:editor:editor-sync-enabled") {
109
- console.log("Editor sync enabled")
110
- editorSyncEnabled = true;
111
- }
112
- else if (message === "needle:editor:editor-sync-disabled") {
113
- editorSyncEnabled = false;
114
- }
115
- }
116
- })
117
- });
109
+ })
110
+ });
111
+ }
112
+ catch(err){
113
+ console.error("Error in needle-editor-connection")
114
+ console.error(err)
115
+ }
118
116
  }
119
117
 
120
118
  }
plugins/vite/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { needleDefines } from "./defines.js";
1
2
  import { needleBuild } from "./build.js";
2
3
  import { needleMeta } from "./meta.js"
3
4
  import { needlePoster } from "./poster.js"
@@ -9,6 +10,7 @@
9
10
  import { needleTransformCodegen } from "./transform-codegen.js";
10
11
  import { needleLicense } from "./license.js";
11
12
  import { needlePeerjs } from "./peer.js";
13
+ import { needleDependencyWatcher } from "./dependency-watcher.js";
12
14
 
13
15
  export * from "./gzip.js";
14
16
  export * from "./config.js";
@@ -22,6 +24,7 @@
22
24
  // ensure we have user settings initialized with defaults
23
25
  userSettings = { ...defaultUserSettings, ...userSettings }
24
26
  const array = [
27
+ needleDefines(command, config, userSettings),
25
28
  needleLicense(command, config, userSettings),
26
29
  needleViteAlias(command, config, userSettings),
27
30
  needleMeta(command, config, userSettings),
@@ -31,7 +34,8 @@
31
34
  needleCopyFiles(command, config, userSettings),
32
35
  needleTransformCodegen(command, config, userSettings),
33
36
  needleDrop(command, config, userSettings),
34
- needlePeerjs(command, config, userSettings)
37
+ needlePeerjs(command, config, userSettings),
38
+ needleDependencyWatcher(command, config, userSettings)
35
39
  ];
36
40
  array.push(await editorConnection(command, config, userSettings, array));
37
41
  return array;
src/engine/codegen/register_types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
@@ -214,7 +214,7 @@
214
214
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering";
215
215
  import { XRRig } from "../../engine-components/webxr/WebXRRig";
216
216
  import { XRState } from "../../engine-components/XRFlag";
217
-
217
+
218
218
  // Register types
219
219
  TypeStore.add("__Ignore", __Ignore);
220
220
  TypeStore.add("ActionBuilder", ActionBuilder);
plugins/vite/reload.js CHANGED
@@ -14,7 +14,9 @@
14
14
  export const needleReload = (command, config, userSettings) => {
15
15
  if (command === "build") return;
16
16
 
17
+ if (userSettings?.noReload === true) return;
17
18
 
19
+
18
20
  let isUpdatingConfig = false;
19
21
  const updateConfig = async () => {
20
22
  if (isUpdatingConfig) return;
@@ -45,7 +47,7 @@
45
47
  else if (!config.server.watch.ignored) config.server.watch.ignored = [];
46
48
  for (const pattern of ignorePatterns)
47
49
  config.server.watch.ignored.push(pattern);
48
- if(config?.debug === true || userSettings?.debug === true)
50
+ if (config?.debug === true || userSettings?.debug === true)
49
51
  setTimeout(() => console.log("Updated server ignore patterns: ", config.server.watch.ignored), 100);
50
52
  },
51
53
  handleHotUpdate(args) {
src/engine/api.ts CHANGED
@@ -27,6 +27,7 @@
27
27
  export * from "./engine_playerview"
28
28
  export * from "./engine_physics"
29
29
  export * from "./engine_physics.types"
30
+ export * from "./engine_physics_rapier"
30
31
  export * from "./engine_scenelighting"
31
32
  export * from "./engine_input";
32
33
  export * from "./engine_math";
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  import { RegisteredAnimationInfo, UsdzAnimation } from "../Animation";
7
7
  import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils";
8
8
 
9
- import { Object3D, Material, Vector3, Quaternion } from "three";
9
+ import { Object3D, Material, Vector3, Quaternion, AnimationAction } from "three";
10
10
  import { USDObject } from "../../ThreeUSDZExporter";
11
11
 
12
12
  import { BehaviorExtension, UsdzBehaviour } from "./Behaviour";
13
- import { ActionBuilder, ActionModel, BehaviorModel, MotionType, Space, TriggerBuilder } from "./BehavioursBuilder";
13
+ import { ActionBuilder, ActionModel, BehaviorModel, IBehaviorElement, MotionType, Space, TriggerBuilder } from "./BehavioursBuilder";
14
14
 
15
15
  export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
16
16
 
@@ -417,7 +417,13 @@
417
417
 
418
418
  @serializable()
419
419
  stateName?: string;
420
+
421
+ @serializable()
422
+ stateNameAfterPlaying?: string;
420
423
 
424
+ @serializable()
425
+ loopAfterPlaying: boolean = false;
426
+
421
427
  onPointerClick() {
422
428
  if (!this.target) return;
423
429
  if (this.stateName)
@@ -425,22 +431,53 @@
425
431
  }
426
432
 
427
433
  private selfModel: any;
428
- private registeredAnimationModel: any;
429
- private registeredAnimation?: RegisteredAnimationInfo;
430
434
 
435
+ private stateAnimationModel: any;
436
+ private stateAnimation?: RegisteredAnimationInfo;
437
+
438
+ private stateAfterPlayingAnimationModel: any;
439
+ private stateAfterPlayingAnimation?: RegisteredAnimationInfo;
440
+
431
441
  createBehaviours(_ext, model, _context) {
432
442
  if (model.uuid === this.gameObject.uuid)
433
443
  this.selfModel = model;
434
444
  }
435
445
 
446
+ private static animationActions: ActionModel[] = [];
447
+
448
+ onAfterHierarchy() {
449
+ PlayAnimationOnClick.animationActions = [];
450
+ }
451
+
436
452
  afterCreateDocument(ext, context) {
437
- if (!this.registeredAnimation || !this.registeredAnimationModel) return;
453
+ if (!this.stateAnimation || !this.stateAnimationModel) return;
438
454
  const document = context.document;
439
455
  document.traverse(model => {
440
- if (model.uuid === this.target?.uuid && this.registeredAnimation) {
456
+ // TODO we should probably check if a startAnimationAction already exists, and not have duplicates of identical ones;
457
+ // looks like otherwise we're getting some animation overlap that doesn't look good.
458
+ if (model.uuid === this.target?.uuid && this.stateAnimation) {
459
+ const sequence: IBehaviorElement[] = [];
460
+ let startAction = PlayAnimationOnClick.animationActions.find(a => a.affectedObjects == model && a.start == this.stateAnimation!.start && a.duration == this.stateAnimation!.duration);
461
+ if (!startAction) {
462
+ startAction = ActionBuilder.startAnimationAction(model, this.stateAnimation.start, this.stateAnimation.duration) as ActionModel;
463
+ PlayAnimationOnClick.animationActions.push(startAction);
464
+ }
465
+ sequence.push(startAction);
466
+
467
+ if (this.stateAfterPlayingAnimation && this.stateAfterPlayingAnimationModel) {
468
+ let endAction = PlayAnimationOnClick.animationActions.find(a => a.affectedObjects == model && a.start == this.stateAfterPlayingAnimation!.start && a.duration == this.stateAfterPlayingAnimation!.duration);
469
+ if (!endAction) {
470
+ endAction = ActionBuilder.startAnimationAction(model, this.stateAfterPlayingAnimation.start, this.stateAfterPlayingAnimation.duration) as ActionModel;
471
+ PlayAnimationOnClick.animationActions.push(endAction);
472
+ }
473
+ const idleAnim = ActionBuilder.sequence(endAction);
474
+ if (this.loopAfterPlaying)
475
+ idleAnim.makeLooping();
476
+ sequence.push(idleAnim);
477
+ }
441
478
  const playAnimationOnTap = new BehaviorModel("tap " + this.name + " for " + this.stateName + " on " + this.target?.name,
442
479
  TriggerBuilder.tapTrigger(this.selfModel),
443
- ActionBuilder.startAnimationAction(model, this.registeredAnimation.start, this.registeredAnimation.duration)
480
+ ActionBuilder.sequence(...sequence)
444
481
  );
445
482
  ext.addBehavior(playAnimationOnTap);
446
483
  }
@@ -449,9 +486,14 @@
449
486
 
450
487
  createAnimation(ext, model, _context) {
451
488
  if (this.target && this.animator) {
489
+
452
490
  const state = this.animator?.runtimeAnimatorController?.findState(this.stateName);
453
- this.registeredAnimationModel = model;
454
- this.registeredAnimation = ext.registerAnimation(this.target, state?.motion.clip);
491
+ this.stateAnimationModel = model;
492
+ this.stateAnimation = ext.registerAnimation(this.target, state?.motion.clip);
493
+
494
+ const stateAfter = this.animator?.runtimeAnimatorController?.findState(this.stateNameAfterPlaying);
495
+ this.stateAfterPlayingAnimationModel = model;
496
+ this.stateAfterPlayingAnimation = ext.registerAnimation(this.target, stateAfter?.motion.clip);
455
497
  }
456
498
  }
457
499
 
src/engine-components/Collider.ts CHANGED
@@ -40,7 +40,7 @@
40
40
  }
41
41
 
42
42
  onDisable() {
43
- this.context.physics.removeBody(this);
43
+ this.context.physics.engine?.removeBody(this);
44
44
  }
45
45
 
46
46
  }
@@ -55,7 +55,7 @@
55
55
 
56
56
  onEnable() {
57
57
  super.onEnable();
58
- this.context.physics.addSphereCollider(this, this.center, this.radius);
58
+ this.context.physics.engine?.addSphereCollider(this, this.center, this.radius);
59
59
  }
60
60
  }
61
61
 
@@ -68,7 +68,7 @@
68
68
 
69
69
  onEnable() {
70
70
  super.onEnable();
71
- this.context.physics.addBoxCollider(this, this.center, this.size);
71
+ this.context.physics.engine?.addBoxCollider(this, this.center, this.size);
72
72
  }
73
73
  }
74
74
 
@@ -90,7 +90,7 @@
90
90
  }
91
91
  }
92
92
  if (this.sharedMesh?.isMesh) {
93
- this.context.physics.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
93
+ this.context.physics.engine?.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
94
94
  }
95
95
  else {
96
96
  const group = this.sharedMesh as any as Group;
@@ -99,7 +99,7 @@
99
99
  for (const ch in group.children) {
100
100
  const child = group.children[ch] as Mesh;
101
101
  if (child.isMesh) {
102
- this.context.physics.addMeshCollider(this, child, this.convex, getWorldScale(this.gameObject));
102
+ this.context.physics.engine?.addMeshCollider(this, child, this.convex, getWorldScale(this.gameObject));
103
103
  }
104
104
  }
105
105
  }
@@ -119,7 +119,7 @@
119
119
 
120
120
  onEnable() {
121
121
  super.onEnable();
122
- this.context.physics.addCapsuleCollider(this, this.center, this.height, this.radius);
122
+ this.context.physics.engine?.addCapsuleCollider(this, this.center, this.height, this.radius);
123
123
  }
124
124
 
125
125
  }
src/engine/engine_context_registry.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { IContext } from "./engine_types";
1
+ import { IComponent, IContext } from "./engine_types";
2
2
 
3
3
  export enum ContextEvent {
4
4
  /** called when the context is registered to the registry, the context is not fully initialized at this point */
5
5
  ContextRegistered = "ContextRegistered",
6
+ /** called before the first glb is loaded, can be used to initialize physics engine, is awaited */
7
+ ContextCreationStart = "ContextCreationStart",
6
8
  ContextCreated = "ContextCreated",
7
9
  ContextDestroyed = "ContextDestroyed",
8
10
  MissingCamera = "MissingCamera",
@@ -13,11 +15,11 @@
13
15
  context: IContext;
14
16
  }
15
17
 
16
- export type ContextCallback = (evt: ContextEventArgs) => void;
18
+ export type ContextCallback = (evt: ContextEventArgs) => void | Promise<any> | IComponent;
17
19
 
18
20
  export class ContextRegistry {
19
21
 
20
- static get Current(): IContext{
22
+ static get Current(): IContext {
21
23
  return globalThis["NeedleEngine.Context.Current"]
22
24
  }
23
25
  static set Current(ctx: IContext) {
@@ -51,10 +53,15 @@
51
53
  this._callbacks[evt].splice(index, 1);
52
54
  }
53
55
 
54
- static dispatchCallback(evt: ContextEvent, context:IContext) {
55
- if (!this._callbacks[evt]) return;
56
+ static dispatchCallback(evt: ContextEvent, context: IContext) {
57
+ if (!this._callbacks[evt]) return true;
56
58
  const args = { event: evt, context }
57
- this._callbacks[evt].forEach(cb => cb(args));
59
+ const promises = new Array<Promise<any>>();
60
+ this._callbacks[evt].forEach(cb => {
61
+ const res = cb(args)
62
+ if (res instanceof Promise) promises.push(res);
63
+ });
64
+ return Promise.all(promises);
58
65
  }
59
66
 
60
67
  static addContextCreatedCallback(callback: ContextCallback) {
src/engine/engine_context.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { BufferGeometry, Camera, Clock, Color, DepthTexture, Group,
2
- Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
3
- PerspectiveCamera, RGBAFormat, Scene, sRGBEncoding,
4
- Texture, WebGLRenderer, WebGLRenderTarget
1
+ import {
2
+ BufferGeometry, Camera, Clock, Color, DepthTexture, Group,
3
+ Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
4
+ PerspectiveCamera, RGBAFormat, Scene, sRGBEncoding,
5
+ Texture, WebGLRenderer, WebGLRenderTarget
5
6
  } from 'three'
6
7
  import { Input } from './engine_input';
7
8
  import { Physics } from './engine_physics';
@@ -27,6 +28,7 @@
27
28
  import { CoroutineData, ICamera, IComponent, IContext, ILight } from "./engine_types"
28
29
  import { destroy, foreachComponent } from './engine_gameobject';
29
30
  import { ContextEvent, ContextRegistry } from './engine_context_registry';
31
+ import { delay } from './engine_utils';
30
32
  // import { createCameraWithOrbitControl } from '../engine-components/CameraUtils';
31
33
 
32
34
 
@@ -251,8 +253,8 @@
251
253
  this.isManagedExternally = true;
252
254
  }
253
255
  else {
254
- this.renderer = new WebGLRenderer({
255
- antialias: true
256
+ this.renderer = new WebGLRenderer({
257
+ antialias: true
256
258
  });
257
259
 
258
260
  // some tonemapping other than "NONE" is required for adjusting exposure with EXR environments
@@ -346,12 +348,13 @@
346
348
  camera.updateProjectionMatrix();
347
349
  }
348
350
 
349
- onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
351
+ async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
350
352
  if (this._isCreated) {
351
353
  console.warn("Context already created");
352
354
  return null;
353
355
  }
354
356
  this._isCreated = true;
357
+ await delay(1);
355
358
  return this.internalOnCreate(buildScene, opts);
356
359
  }
357
360
 
@@ -498,8 +501,8 @@
498
501
 
499
502
  private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
500
503
 
501
- // TODO: we could configure if we need physics
502
- await this.physics.createWorld();
504
+ Context.Current = this;
505
+ await ContextRegistry.dispatchCallback(ContextEvent.ContextCreationStart, this);
503
506
 
504
507
  // load and create scene
505
508
  let prepare_succeeded = true;
@@ -512,7 +515,7 @@
512
515
  console.error(err);
513
516
  prepare_succeeded = false;
514
517
  }
515
- if (!prepare_succeeded) return;
518
+ if (!prepare_succeeded) return false;
516
519
 
517
520
  this.internalOnUpdateVisible();
518
521
 
@@ -523,6 +526,9 @@
523
526
 
524
527
  Context.Current = this;
525
528
 
529
+ // TODO: we could configure if we need physics
530
+ // await this.physics.engine?.initialize();
531
+
526
532
  // Setup
527
533
  Context.Current = this;
528
534
  for (let i = 0; i < this.new_scripts.length; i++) {
@@ -609,13 +615,13 @@
609
615
  if (debug)
610
616
  logHierarchy(this.scene, true);
611
617
 
612
- ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this);
618
+ return ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this);
613
619
  }
614
620
 
615
621
  private _accumulatedTime = 0;
616
622
  private _framerateClock = new Clock();
617
623
 
618
- private render(_, frame : XRFrame) {
624
+ private render(_, frame: XRFrame) {
619
625
  this._xrFrame = frame;
620
626
 
621
627
  this._currentFrameEvent = FrameEvent.Undefined;
@@ -627,7 +633,7 @@
627
633
  }
628
634
  this._accumulatedTime = 0;
629
635
  }
630
-
636
+
631
637
  this._stats?.begin();
632
638
 
633
639
  Context.Current = this;
@@ -696,16 +702,19 @@
696
702
  this.executeCoroutines(FrameEvent.LateUpdate);
697
703
  if (this.onHandlePaused()) return;
698
704
 
699
- const physicsSteps = 1;
700
- const dt = this.time.deltaTime / physicsSteps;
701
- for (let i = 0; i < physicsSteps; i++) {
702
- this._currentFrameEvent = FrameEvent.PrePhysicsStep;
703
- this.executeCoroutines(FrameEvent.PrePhysicsStep);
704
- this.physics.step(dt);
705
- this._currentFrameEvent = FrameEvent.PostPhysicsStep;
706
- this.executeCoroutines(FrameEvent.PostPhysicsStep);
705
+ if (this.physics.engine) {
706
+ const physicsSteps = 1;
707
+ const dt = this.time.deltaTime / physicsSteps;
708
+ for (let i = 0; i < physicsSteps; i++) {
709
+ this._currentFrameEvent = FrameEvent.PrePhysicsStep;
710
+ this.executeCoroutines(FrameEvent.PrePhysicsStep);
711
+ this.physics.engine.step(dt);
712
+ this._currentFrameEvent = FrameEvent.PostPhysicsStep;
713
+ this.executeCoroutines(FrameEvent.PostPhysicsStep);
714
+ }
715
+ this.physics.engine.postStep();
707
716
  }
708
- this.physics.postStep();
717
+
709
718
  if (this.onHandlePaused()) return;
710
719
 
711
720
  if (this.isVisibleToUser) {
@@ -781,7 +790,7 @@
781
790
  this.renderRequiredTextures();
782
791
  // if (camera === this.mainCameraComponent?.cam) {
783
792
  // if (this.mainCameraComponent.activeTexture) {
784
-
793
+
785
794
  // }
786
795
  // }
787
796
  if (this.composer && !this.isInXR) {
src/engine/engine_element_loading.ts CHANGED
@@ -73,7 +73,7 @@
73
73
  }
74
74
 
75
75
  onLoadingBegin(message?: string) {
76
- if (debug) console.log("Begin Loading")
76
+ if (debug) console.warn("Begin Loading")
77
77
  if (!this._loadingElement) {
78
78
  for (let i = 0; i < this._element.children.length; i++) {
79
79
  const el = this._element.children[i] as HTMLElement;
@@ -98,9 +98,8 @@
98
98
  }
99
99
 
100
100
  onLoadingUpdate(progress: LoadingProgressArgs | ProgressEvent | number, message?: string) {
101
- // if the element has no parent we want to add it
102
101
  if (!this._loadingElement?.parentElement) {
103
- this.onLoadingBegin(message);
102
+ return;
104
103
  }
105
104
  // console.log(callback.name, callback.progress.loaded / callback.progress.total, callback.index + "/" + callback.count);
106
105
  let total01 = 0;
src/engine/engine_element.ts CHANGED
@@ -164,6 +164,7 @@
164
164
  return;
165
165
  }
166
166
 
167
+
167
168
  this._previousSrc = src;
168
169
 
169
170
  // Set the source attribute so codegen doesnt try to re-assign it again and we communicate to the outside which root files are being loaded
@@ -221,7 +222,7 @@
221
222
  if (!url.includes(".glb") && !url.includes(".gltf")) {
222
223
  const warning = `Needle Engine: found suspicious src "${url}"`;
223
224
  console.warn(warning);
224
- if(isLocalNetwork()) showBalloonWarning(warning);
225
+ if (isLocalNetwork()) showBalloonWarning(warning);
225
226
  }
226
227
  const fileName = getNameFromUrl(url);
227
228
  const progress: LoadingProgressArgs = {
src/engine/engine_input.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { showBalloonMessage, showBalloonWarning } from './debug/debug';
3
3
  import { assign } from './engine_serialization_core';
4
4
  import { Context } from './engine_setup';
5
- import { Vec2 } from './engine_types';
5
+ import { IInput, Vec2 } from './engine_types';
6
6
  import { getParam } from './engine_utils';
7
7
 
8
8
  const debug = getParam("debuginput");
@@ -52,7 +52,7 @@
52
52
  // }
53
53
  // }
54
54
 
55
- export class Input extends EventTarget {
55
+ export class Input extends EventTarget implements IInput {
56
56
 
57
57
  _doubleClickTimeThreshold = .2;
58
58
  _longPressTimeThreshold = 1;
src/engine/engine_physics.ts CHANGED
@@ -1,47 +1,11 @@
1
- import { BasicDepthPacking, Box3, BufferAttribute, BufferGeometry, Camera, Intersection, Layers, LineBasicMaterial, LineSegments, Matrix4, Mesh, NormalAnimationBlendMode, NumberKeyframeTrack, Object3D, Quaternion, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
1
+ import { Box3, Camera, Intersection, Layers, Mesh, Object3D, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
2
2
  import { Context } from './engine_setup';
3
- import { CircularBuffer, getParam } from "./engine_utils"
4
- import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPositionXYZ, setWorldQuaternion, setWorldQuaternionXYZW } from "./engine_three_utils"
5
- import {
6
- IComponent,
7
- ICollider,
8
- IRigidbody,
9
- Collision,
10
- ContactPoint,
11
- Vec3,
12
- IGameObject,
13
- Vec2,
14
- } from './engine_types';
15
- import { InstancingUtil } from './engine_instancing';
16
- import { foreachComponent } from './engine_gameobject';
3
+ import { getParam } from "./engine_utils"
4
+ import { getWorldPosition } from "./engine_three_utils"
5
+ import { Vec2, Vec3, } from './engine_types';
6
+ import { IPhysicsEngine } from './engine_types';
17
7
 
18
- import RAPIER, { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World } from '@dimforge/rapier3d-compat';
19
- import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types';
20
- import { Gizmos } from './engine_gizmos';
21
- import { Mathf } from './engine_math';
22
- import { Layer } from './extensions/NEEDLE_animator_controller_model';
23
- export type Rapier = typeof RAPIER;
24
-
25
-
26
8
  const debugPhysics = getParam("debugphysics");
27
- const debugColliderPlacement = getParam("debugphysicscolliders");
28
- const debugCollisions = getParam("debugcollisions");
29
- const showColliders = getParam("showcolliders");
30
- const noPhysics = getParam("nophysics");
31
-
32
-
33
- declare type PhysicsBody = {
34
- translation(): { x: number, y: number, z: number }
35
- rotation(): { x: number, y: number, z: number, w: number }
36
- }
37
-
38
- /** on physics body and references the needle component */
39
- const $componentKey = Symbol("needle component");
40
- /** on needle component and references physics body */
41
- const $bodyKey = Symbol("physics body");
42
- const $colliderRigidbody = Symbol("rigidbody");
43
- // const $removed = Symbol("removed");
44
-
45
9
  const layerMaskHelper: Layers = new Layers();
46
10
 
47
11
  export class RaycastOptions {
@@ -94,26 +58,32 @@
94
58
  }
95
59
  }
96
60
 
97
- export class SphereOverlapResult {
98
- object: Object3D;
99
- collider: ICollider;
100
- constructor(object: Object3D, collider: ICollider) {
101
- this.object = object;
102
- this.collider = collider;
61
+ export class Physics {
62
+
63
+ /**@deprecated use this.context.physics.engine.raycast */
64
+ public raycastPhysicsFast(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true) {
65
+ return this.context.physics.engine?.raycast(origin, direction, maxDistance, solid) ?? null;
103
66
  }
104
- }
105
67
 
106
- declare type PhysicsRaycastResult = {
107
- point: Vector3,
108
- normal?: Vector3,
109
- collider?: ICollider
110
- }
68
+ /**@deprecated use this.context.physics.engine.raycastAndGetNormal */
69
+ public raycastPhysicsFastAndGetNormal(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true) {
70
+ return this.context.physics.engine?.raycastAndGetNormal(origin, direction, maxDistance, solid) ?? null;
71
+ }
111
72
 
73
+ /**@deprecated use this.context.physics.engine.sphereOverlap */
74
+ public sphereOverlapPhysics(point: Vector3, radius: number) {
75
+ return this.context.physics.engine?.sphereOverlap(point, radius) ?? null;
76
+ }
112
77
 
113
- export class Physics {
114
78
 
79
+ private readonly context: Context;
80
+ engine?: IPhysicsEngine;
81
+
82
+ constructor(context: Context) {
83
+ this.context = context;
84
+ }
85
+
115
86
  // raycasting
116
-
117
87
  private readonly raycaster: Raycaster = new Raycaster();
118
88
  private readonly defaultRaycastOptions: RaycastOptions = new RaycastOptions();
119
89
  private readonly targetBuffer: Array<Object3D> = new Array<Object3D>(1);
@@ -248,969 +218,4 @@
248
218
  }
249
219
  return results;
250
220
  }
251
-
252
- private rapierRay = new RAPIER.Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 });
253
- private raycastVectorsBuffer = new CircularBuffer(() => new Vector3(), 10);
254
- /** Fast raycast against physics colliders
255
- * @param origin ray origin in screen or worldspace
256
- * @param direction ray direction in worldspace
257
- * @param maxDistance max distance to raycast
258
- * @param solid if true it will also hit the collider if origin is already inside it
259
- */
260
- public raycastPhysicsFast(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true)
261
- : null | { point: Vector3, collider: ICollider } {
262
-
263
- const ray = this.getPhysicsRay(this.rapierRay, origin, direction);
264
- if (!ray) return null;
265
-
266
- const hit = this.world?.castRay(ray, maxDistance, solid, undefined, undefined, undefined, undefined, (c) => {
267
- // ignore objects in the IgnoreRaycast=2 layer
268
- return !c[$componentKey]?.gameObject.layers.isEnabled(2);
269
- });
270
- if (hit) {
271
- const point = ray.pointAt(hit.toi);
272
- const vec = this.raycastVectorsBuffer.get();
273
- vec.set(point.x, point.y, point.z);
274
- return { point: vec, collider: hit.collider[$componentKey] };
275
- }
276
-
277
- return null;
278
- }
279
-
280
- public raycastPhysicsFastAndGetNormal(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true)
281
- : null | { point: Vector3, normal: Vector3, collider: ICollider } {
282
-
283
- const ray = this.getPhysicsRay(this.rapierRay, origin, direction);
284
- if (!ray) return null;
285
-
286
- const hit = this.world?.castRayAndGetNormal(ray, maxDistance, solid, undefined, undefined, undefined, undefined, (c) => {
287
- // ignore objects in the IgnoreRaycast=2 layer
288
- return !c[$componentKey]?.gameObject.layers.isEnabled(2);
289
- });
290
- if (hit) {
291
- const point = ray.pointAt(hit.toi);
292
- const normal = hit.normal;
293
- const vec = this.raycastVectorsBuffer.get();
294
- const nor = this.raycastVectorsBuffer.get();
295
- vec.set(point.x, point.y, point.z);
296
- nor.set(normal.x, normal.y, normal.z);
297
- return { point: vec, normal: nor, collider: hit.collider[$componentKey] };
298
- }
299
- return null;
300
- }
301
-
302
- private getPhysicsRay(ray: RAPIER.Ray, origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined): RAPIER.Ray | null {
303
- const cam = this.context.mainCamera;
304
- // if we get origin in 2d space we need to project it to 3d space
305
- if (origin["z"] === undefined) {
306
- if (!cam) {
307
- console.error("Can not perform raycast from 2d point - no main camera found");
308
- return null;
309
- }
310
- const vec3 = this.raycastVectorsBuffer.get();
311
- vec3.x = origin.x;
312
- vec3.y = origin.y;
313
- vec3.z = 0;
314
- // if the origin is in screen space we need to convert it to raycaster space
315
- if (vec3.x > 1 || vec3.y > 1 || vec3.y < -1 || vec3.x < -1) {
316
- this.context.input.convertScreenspaceToRaycastSpace(vec3);
317
- }
318
- vec3.unproject(cam);
319
- origin = vec3;
320
- }
321
-
322
- const o = origin as Vec3;
323
-
324
- ray.origin.x = o.x;
325
- ray.origin.y = o.y;
326
- ray.origin.z = o.z;
327
- const vec = this.raycastVectorsBuffer.get();
328
- if (direction)
329
- vec.set(direction.x, direction.y, direction.z);
330
- else {
331
- if (!cam) {
332
- console.error("Can not perform raycast - no camera found");
333
- return null;
334
- }
335
- vec.set(ray.origin.x, ray.origin.y, ray.origin.z);
336
- const camPosition = getWorldPosition(cam);
337
- vec.sub(camPosition);
338
- }
339
- // we need to normalize the ray because our input is a max travel length and the direction may be not normalized
340
- vec.normalize();
341
- ray.dir.x = vec.x;
342
- ray.dir.y = vec.y;
343
- ray.dir.z = vec.z;
344
- // Gizmos.DrawRay(ray.origin, ray.dir, 0xff0000, Infinity);
345
- return ray;
346
- }
347
-
348
-
349
- private rapierSphere: RAPIER.Ball | null = null;
350
- private rapierColliderArray: Array<SphereOverlapResult> = [];
351
- private readonly rapierIdentityRotation = { x: 0, y: 0, z: 0, w: 1 };
352
- private readonly rapierForwardVector = { x: 0, y: 0, z: 1 };
353
- /** Precice sphere overlap detection using rapier against colliders
354
- * @param point center of the sphere in worldspace
355
- * @param radius radius of the sphere
356
- * @returns array of colliders that overlap with the sphere. Note: they currently only contain the collider and the gameobject
357
- */
358
- public sphereOverlapPhysics(point: Vector3, radius: number): Array<SphereOverlapResult> {
359
- this.rapierColliderArray.length = 0;
360
- if (!this.world) return this.rapierColliderArray;
361
- if (!this.rapierSphere)
362
- this.rapierSphere = new RAPIER.Ball(radius);
363
- this.rapierSphere.radius = radius;
364
-
365
- this.world.intersectionsWithShape(point, this.rapierIdentityRotation, this.rapierSphere, col => {
366
- const collider = col[$componentKey] as ICollider
367
- // if (collider.gameObject.layers.isEnabled(2)) return true;
368
- const intersection = new SphereOverlapResult(collider.gameObject, collider);
369
- this.rapierColliderArray.push(intersection);
370
- return true; // Return `false` instead if we want to stop searching for other colliders that contain this point.
371
- }, QueryFilterFlags.EXCLUDE_SENSORS, undefined, undefined, undefined,
372
- col => {
373
- const collider = col[$componentKey] as ICollider
374
- return collider.gameObject.layers.isEnabled(2) == false
375
- }
376
- );
377
- return this.rapierColliderArray;
378
-
379
-
380
- // TODO: this only returns one hit
381
- // let filterGroups = 0xffffffff;
382
- // filterGroups &= ~(1 << 2);
383
- // const hit: ShapeColliderTOI | null = this.world.castShape(point,
384
- // this.rapierIdentityRotation,
385
- // this.rapierForwardVector,
386
- // this.rapierSphere,
387
- // 0,
388
- // QueryFilterFlags.EXCLUDE_SENSORS,
389
- // // filterGroups,
390
- // );
391
- // // console.log(hit);
392
- // if (hit) {
393
- // const collider = hit.collider[$componentKey] as ICollider
394
- // const intersection = new SphereOverlapResult(collider.gameObject);
395
- // this.rapierColliderArray.push(intersection);
396
- // // const localpt = hit.witness2;
397
- // // // const normal = hit.normal2;
398
- // // const hitPoint = new Vector3(localpt.x, localpt.y, localpt.z);
399
- // // // collider.gameObject.localToWorld(hitPoint);
400
- // // // const normalPt = new Vector3(normal.x, normal.y, normal.z);
401
- // // // const mat = new Matrix4().setPosition(point).scale(new Vector3(radius, radius, radius));
402
- // // // hitPoint.applyMatrix4(mat);
403
- // // console.log(hit.witness2)
404
- // // // hitPoint.add(point);
405
- // // const dist = hitPoint.distanceTo(point);
406
- // }
407
-
408
- // return this.rapierColliderArray;
409
- }
410
-
411
-
412
-
413
-
414
- // physics simulation
415
-
416
- enabled: boolean = true;
417
-
418
- private _tempPosition: Vector3 = new Vector3();
419
- private _tempQuaternion: Quaternion = new Quaternion();
420
- private _tempScale: Vector3 = new Vector3();
421
- private _tempMatrix: Matrix4 = new Matrix4();
422
-
423
- private static _didLoadPhysicsEngine: boolean = false;
424
-
425
- private _isUpdatingPhysicsWorld: boolean = false;
426
- get isUpdating(): boolean { return this._isUpdatingPhysicsWorld; }
427
-
428
-
429
- private context: Context;
430
- private world?: World;
431
- private _hasCreatedWorld: boolean = false;
432
- private eventQueue?: EventQueue;
433
- private collisionHandler?: PhysicsCollisionHandler;
434
-
435
-
436
- private objects: IComponent[] = [];
437
- private bodies: PhysicsBody[] = [];
438
-
439
- private _meshCache: Map<string, Float32Array> = new Map<string, Float32Array>();
440
-
441
-
442
- constructor(context: Context) {
443
- this.context = context;
444
- }
445
-
446
- async createWorld() {
447
- if (this._hasCreatedWorld) {
448
- console.error("Invalid call to create physics world: world is already created");
449
- return;
450
- }
451
- this._hasCreatedWorld = true;
452
- if (!Physics._didLoadPhysicsEngine) {
453
- await RAPIER.init().then(() => RAPIER)
454
- Physics._didLoadPhysicsEngine = true;
455
- }
456
- this.world = new World(this._gravity);
457
- if (noPhysics) this.enabled = false;
458
- }
459
-
460
- private _gravity = { x: 0.0, y: -9.81, z: 0.0 };
461
-
462
- get gravity() {
463
- return this.world?.gravity ?? this._gravity;
464
- }
465
-
466
- set gravity(value: Vec3) {
467
- if (this.world) {
468
- this.world.gravity = value;
469
- }
470
- else {
471
- this._gravity = value;
472
- }
473
- }
474
-
475
- clearCaches() {
476
- this._meshCache.clear();
477
- }
478
-
479
- addBoxCollider(collider: ICollider, center: Vector3, size: Vector3) {
480
- if (!this.enabled) {
481
- if (debugPhysics) console.warn("Physics are disabled");
482
- return;
483
- }
484
- const obj = collider.gameObject;
485
- const scale = getWorldScale(obj, this._tempPosition).multiply(size);
486
- scale.multiplyScalar(0.5);
487
-
488
- // prevent negative scale
489
- if (scale.x < 0)
490
- scale.x = Math.abs(scale.x);
491
- if (scale.y < 0)
492
- scale.y = Math.abs(scale.y);
493
- if (scale.z < 0)
494
- scale.z = Math.abs(scale.z);
495
-
496
- // prevent zero scale - seems normals are flipped otherwise
497
- if (scale.x == 0) scale.x = 0.0000001;
498
- if (scale.y == 0) scale.y = 0.0000001;
499
- if (scale.z == 0) scale.z = 0.0000001;
500
-
501
- const desc = ColliderDesc.cuboid(scale.x, scale.y, scale.z);
502
- // const objectLayerMask = collider.gameObject.layers.mask;
503
- // const mask = objectLayerMask & ~2;
504
- // TODO: https://rapier.rs/docs/user_guides/javascript/colliders/#collision-groups-and-solver-groups
505
- // desc.setCollisionGroups(objectLayerMask);
506
- this.createCollider(collider, desc, center);
507
- }
508
-
509
- addSphereCollider(collider: ICollider, center: Vector3, radius: number) {
510
- if (!this.enabled) {
511
- if (debugPhysics) console.warn("Physics are disabled");
512
- return;
513
- }
514
- const obj = collider.gameObject;
515
- const scale = getWorldScale(obj, this._tempPosition).multiplyScalar(radius);
516
- // Prevent negative scales
517
- scale.x = Math.abs(scale.x);
518
- const desc = ColliderDesc.ball(scale.x);
519
- this.createCollider(collider, desc, center);
520
- }
521
-
522
- addCapsuleCollider(collider: ICollider, center: Vector3, height: number, radius: number) {
523
- if (!this.enabled) {
524
- if (debugPhysics) console.warn("Physics are disabled");
525
- return;
526
- }
527
- const obj = collider.gameObject;
528
- const scale = getWorldScale(obj, this._tempPosition);
529
- // Prevent negative scales
530
- scale.x = Math.abs(scale.x);
531
- scale.y = Math.abs(scale.y);
532
- const desc = ColliderDesc.capsule(height * .5 * scale.y - radius, radius * scale.x);
533
- this.createCollider(collider, desc, center);
534
- }
535
-
536
- addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale: Vector3) {
537
- if (!this.enabled) {
538
- if (debugPhysics) console.warn("Physics are disabled");
539
- return;
540
- }
541
- const geo = mesh.geometry;
542
- if (!geo) {
543
- if (debugPhysics) console.warn("Missing mesh geometry", mesh.name);
544
- return;
545
- }
546
-
547
- let positions = geo.getAttribute("position").array as Float32Array;
548
- const indices = geo.index?.array as Uint32Array;
549
-
550
- // console.log(geo.center())
551
-
552
- // scaling seems not supported yet https://github.com/dimforge/rapier/issues/243
553
- if (Math.abs(scale.x - 1) > 0.0001 || Math.abs(scale.y - 1) > 0.0001 || Math.abs(scale.z - 1) > 0.0001) {
554
- const key = geo.uuid + "_" + scale.x + "_" + scale.y + "_" + scale.z + "_" + convex;
555
- if (this._meshCache.has(key)) {
556
- positions = this._meshCache.get(key)!;
557
- }
558
- else {
559
- console.warn("Your model is using scaled mesh colliders which is not optimal for performance", mesh.name, Object.assign({}, scale), mesh);
560
- // showBalloonWarning("Your model is using scaled mesh colliders which is not optimal for performance: " + mesh.name + ", consider using unscaled objects");
561
- const scaledPositions = new Float32Array(positions.length);
562
- for (let i = 0; i < positions.length; i += 3) {
563
- scaledPositions[i] = positions[i] * scale.x;
564
- scaledPositions[i + 1] = positions[i + 1] * scale.y;
565
- scaledPositions[i + 2] = positions[i + 2] * scale.z;
566
- }
567
- positions = scaledPositions;
568
- this._meshCache.set(key, scaledPositions);
569
- }
570
- }
571
- const desc = convex ? ColliderDesc.convexMesh(positions) : ColliderDesc.trimesh(positions, indices);
572
- if (desc) {
573
- const col = this.createCollider(collider, desc);
574
- col.setMassProperties(1, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 });
575
- // rb?.setTranslation({ x: 0, y: 2, z: 0 });
576
- // col.setTranslationWrtParent(new Vector3(0,2,0));
577
-
578
- }
579
- }
580
-
581
- private createCollider(collider: ICollider, desc: ColliderDesc, center?: Vector3) {
582
- if (!this.world) throw new Error("Physics world not initialized");
583
- const matrix = this._tempMatrix;
584
- const {
585
- rigidBody,
586
- useExplicitMassProperties
587
- } = this.getRigidbody(collider, this._tempMatrix);
588
-
589
- matrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
590
- getWorldScale(collider.gameObject, this._tempScale);
591
- if (center) {
592
- center.multiply(this._tempScale);
593
- this._tempPosition.x -= center.x;
594
- this._tempPosition.y += center.y;
595
- this._tempPosition.z += center.z;
596
- }
597
- desc.setTranslation(this._tempPosition.x, this._tempPosition.y, this._tempPosition.z);
598
- desc.setRotation(this._tempQuaternion);
599
- desc.setSensor(collider.isTrigger);
600
-
601
- // TODO: we might want to update this if the material changes
602
- const physicsMaterial = collider.sharedMaterial;
603
- if (physicsMaterial) {
604
- CoefficientCombineRule
605
- desc.setRestitution(physicsMaterial.bounciness);
606
- switch (physicsMaterial.bounceCombine) {
607
- case PhysicsMaterialCombine.Average:
608
- desc.setRestitutionCombineRule(CoefficientCombineRule.Average);
609
- break;
610
- case PhysicsMaterialCombine.Maximum:
611
- desc.setRestitutionCombineRule(CoefficientCombineRule.Max);
612
- break;
613
- case PhysicsMaterialCombine.Minimum:
614
- desc.setRestitutionCombineRule(CoefficientCombineRule.Min);
615
- break;
616
- case PhysicsMaterialCombine.Multiply:
617
- desc.setRestitutionCombineRule(CoefficientCombineRule.Multiply);
618
- break;
619
- }
620
- desc.setFriction(physicsMaterial.dynamicFriction);
621
- switch (physicsMaterial.frictionCombine) {
622
- case PhysicsMaterialCombine.Average:
623
- desc.setFrictionCombineRule(CoefficientCombineRule.Average);
624
- break;
625
- case PhysicsMaterialCombine.Maximum:
626
- desc.setFrictionCombineRule(CoefficientCombineRule.Max);
627
- break;
628
- case PhysicsMaterialCombine.Minimum:
629
- desc.setFrictionCombineRule(CoefficientCombineRule.Min);
630
- break;
631
- case PhysicsMaterialCombine.Multiply:
632
- desc.setFrictionCombineRule(CoefficientCombineRule.Multiply);
633
- break;
634
- }
635
- }
636
-
637
- // if we want to use explicit mass properties, we need to set the collider density to 0
638
- // otherwise rapier will compute the mass properties based on the collider shape and density
639
- // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties
640
- if (useExplicitMassProperties) {
641
- // desc.setDensity(0);
642
- }
643
-
644
- const col = this.world.createCollider(desc, rigidBody);
645
- col[$componentKey] = collider;
646
- collider[$bodyKey] = col;
647
- col.setActiveEvents(ActiveEvents.COLLISION_EVENTS);
648
- // We want to receive collisitons between two triggers too
649
- col.setActiveCollisionTypes(ActiveCollisionTypes.ALL);
650
-
651
- // const objectLayerMask = collider.gameObject.layers.mask;
652
- // const mask = objectLayerMask & ~2;
653
- // col.setCollisionGroups(objectLayerMask);
654
- this.objects.push(collider);
655
- this.bodies.push(col);
656
- return col;
657
- }
658
-
659
- private getRigidbody(collider: ICollider, _matrix: Matrix4): { rigidBody: RigidBody, useExplicitMassProperties: boolean } {
660
-
661
- if (!this.world) throw new Error("Physics world not initialized");
662
- let rigidBody: RigidBody | null = null;
663
- let useExplicitMassProperties = false;
664
-
665
- if (collider.attachedRigidbody) {
666
-
667
- const rb = collider.attachedRigidbody;
668
- rigidBody = rb[$bodyKey];
669
- useExplicitMassProperties = true;
670
- if (!rigidBody) {
671
- const kinematic = rb.isKinematic && !debugColliderPlacement;
672
- if (debugPhysics)
673
- console.log("Create rigidbody", kinematic);
674
- const rigidBodyDesc = kinematic ? RAPIER.RigidBodyDesc.kinematicPositionBased() : RAPIER.RigidBodyDesc.dynamic();
675
- const pos = getWorldPosition(collider.attachedRigidbody.gameObject);
676
- rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z);
677
- rigidBodyDesc.setRotation(getWorldQuaternion(collider.attachedRigidbody.gameObject));
678
- rigidBody = this.world.createRigidBody(rigidBodyDesc);
679
- this.bodies.push(rigidBody);
680
- this.objects.push(rb);
681
- }
682
- rigidBody[$componentKey] = rb;
683
- rb[$bodyKey] = rigidBody;
684
- this.internalUpdateProperties(rb, rigidBody);
685
- this.getRigidbodyRelativeMatrix(collider.gameObject, rb.gameObject, _matrix);
686
-
687
- }
688
- else {
689
-
690
- const rigidBodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased();
691
- const pos = getWorldPosition(collider.gameObject);
692
- rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z);
693
- rigidBodyDesc.setRotation(getWorldQuaternion(collider.gameObject));
694
- rigidBody = this.world.createRigidBody(rigidBodyDesc);
695
- _matrix.identity();
696
- rigidBody[$componentKey] = null;
697
-
698
- }
699
-
700
- collider[$colliderRigidbody] = rigidBody;
701
-
702
- return { rigidBody: rigidBody, useExplicitMassProperties: useExplicitMassProperties };
703
- }
704
-
705
- removeBody(obj: IComponent) {
706
- const body = obj[$bodyKey];
707
- obj[$bodyKey] = null;
708
- if (body && this.world) {
709
- const index = this.objects.findIndex(o => o === obj);
710
- if (index >= 0) {
711
- const body = this.bodies[index];
712
- this.bodies.splice(index, 1);
713
- this.objects.splice(index, 1);
714
-
715
- if (body instanceof Collider) {
716
- const collider = body as Collider;
717
- this.world?.removeCollider(collider, true);
718
-
719
- // remove the rigidbody if it doesnt have colliders anymore
720
- const rb = collider.parent();
721
- if (rb && rb.numColliders() <= 0) {
722
- this.world?.removeRigidBody(rb);
723
- }
724
- }
725
- else if (body instanceof RigidBody) {
726
- // TODO: running this code below causes a crash in rapier
727
- // const rb = body as RigidBody;
728
- // console.log("colliders", rb.numColliders())
729
- // for (let i = 0; i < rb.numColliders(); i++) {
730
- // const col = rb.collider(i);
731
- // this.world?.removeCollider(col, true);
732
- // }
733
- // console.log("colliders", rb.numColliders(), rb)
734
- // console.log(rb.handle, rb.userData);
735
- // if (rb.userData === undefined)
736
- // this.world?.removeRigidBody(rb);
737
- }
738
-
739
- // check if we need to remove the rigidbody too
740
- // const col = obj as ICollider;
741
- // if (col.isCollider && col.attachedRigidbody) {
742
- // const rb = col.attachedRigidbody[$bodyKey] as RigidBody;
743
- // if (rb && rb.numColliders() <= 0) {
744
- // // this.world?.removeRigidBody(rb);
745
- // }
746
- // }
747
- }
748
- }
749
- }
750
-
751
- updateBody(comp: ICollider | IRigidbody, translation: boolean, rotation: boolean) {
752
- if (!this.enabled) return;
753
- if (comp.destroyed || !comp.gameObject) return;
754
- if (!translation && !rotation) return;
755
-
756
- if ((comp as ICollider).isCollider === true) {
757
- // const collider = comp as ICollider;
758
- console.warn("TODO: implement updating collider position");
759
- }
760
- else {
761
- const rigidbody = comp as IRigidbody;
762
- const body = rigidbody[$bodyKey];
763
- if (body) {
764
- this.syncPhysicsBody(rigidbody.gameObject, body, translation, rotation);
765
- }
766
- }
767
- }
768
-
769
- updateProperties(rigidbody: IRigidbody) {
770
- const physicsBody = rigidbody[$bodyKey]
771
- if (physicsBody) {
772
- this.internalUpdateProperties(rigidbody, physicsBody);
773
- }
774
- }
775
-
776
- internal_getRigidbody(rb: IRigidbody): RigidBody | null {
777
- return rb[$bodyKey] as RigidBody;
778
- }
779
-
780
- private internalUpdateProperties(rb: IRigidbody, rigidbody: RigidBody) {
781
- // continuous collision detection
782
- // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#continuous-collision-detection
783
- rigidbody.enableCcd(rb.collisionDetectionMode !== CollisionDetectionMode.Discrete);
784
- rigidbody.setLinearDamping(rb.drag);
785
- rigidbody.setAngularDamping(rb.angularDrag);
786
- rigidbody.setGravityScale(rb.useGravity ? rb.gravityScale : 0, true);
787
-
788
- // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties
789
- // rigidbody.setAdditionalMass(rb.mass, true);
790
- // for (let i = 0; i < rigidbody.numColliders(); i++) {
791
- // const collider = rigidbody.collider(i);
792
- // if (collider) {
793
- // collider.setMass(rb.mass);
794
- // // const density = rb.mass / collider.shape.computeMassProperties().mass;
795
- // }
796
- // }
797
-
798
- // lock rotations
799
- rigidbody.setEnabledRotations(!rb.lockRotationX, !rb.lockRotationY, !rb.lockRotationZ, true);
800
- rigidbody.setEnabledTranslations(!rb.lockPositionX, !rb.lockPositionY, !rb.lockPositionZ, true);
801
-
802
- if (rb.isKinematic) {
803
- rigidbody.setBodyType(RAPIER.RigidBodyType.KinematicPositionBased);
804
- }
805
- else {
806
- rigidbody.setBodyType(RAPIER.RigidBodyType.Dynamic);
807
- }
808
- }
809
-
810
- // private _lastStepTime: number | undefined = 0;
811
- private lines?: LineSegments;
812
-
813
- public step(dt?: number) {
814
- if (!this.world) return;
815
- if (!this.enabled) return;
816
- this._isUpdatingPhysicsWorld = true;
817
- if (!this.eventQueue) {
818
- this.eventQueue = new EventQueue(false);
819
- }
820
- if (dt) {
821
- // if we make to sudden changes to the timestep the physics can get unstable
822
- // https://rapier.rs/docs/user_guides/javascript/integration_parameters/#dt
823
- this.world.timestep = Mathf.lerp(this.world.timestep, dt, 0.8);
824
- }
825
- this.world.step(this.eventQueue);
826
- this._isUpdatingPhysicsWorld = false;
827
- this.updateDebugRendering(this.world);
828
- }
829
-
830
- private updateDebugRendering(world: World) {
831
- if (debugPhysics || debugColliderPlacement || showColliders) {
832
- if (!this.lines) {
833
- const material = new LineBasicMaterial({
834
- color: 0x227700,
835
- // vertexColors: THREE.VertexColors
836
- });
837
- const geometry = new BufferGeometry();
838
- this.lines = new LineSegments(geometry, material);
839
- this.context.scene.add(this.lines);
840
- }
841
- const buffers = world.debugRender();
842
- this.lines.geometry.setAttribute('position', new BufferAttribute(buffers.vertices, 3));
843
- this.lines.geometry.setAttribute('color', new BufferAttribute(buffers.colors, 4));
844
- }
845
- }
846
-
847
- public postStep() {
848
- if (!this.world) return;
849
- if (!this.enabled) return;
850
- this._isUpdatingPhysicsWorld = true;
851
- this.syncObjects();
852
- this._isUpdatingPhysicsWorld = false;
853
-
854
- if (this.eventQueue && !this.collisionHandler) {
855
- this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue);
856
- }
857
- if (this.collisionHandler) {
858
- this.collisionHandler.handleCollisionEvents();
859
- this.collisionHandler.update();
860
- }
861
- }
862
-
863
- /** sync rendered objects with physics world (except for colliders without rigidbody) */
864
- private syncObjects() {
865
- if (debugColliderPlacement) return;
866
- for (let i = 0; i < this.bodies.length; i++) {
867
- const obj = this.objects[i];
868
- const body = this.bodies[i] as Collider;
869
-
870
- // if the collider is not attached to a rigidbody
871
- // it means that its kinematic so we need to update its position
872
- const col = (obj as ICollider);
873
- if (col?.isCollider === true && !col.attachedRigidbody) {
874
- const rigidbody = body.parent();
875
- if (rigidbody)
876
- this.syncPhysicsBody(obj.gameObject, rigidbody, true, true);
877
- continue;
878
- }
879
-
880
-
881
- // sync
882
- const pos = body.translation();
883
- const rot = body.rotation();
884
- // make sure to keep the collider offset
885
- const center = obj["center"] as Vector3;
886
- if (center && center.isVector3) {
887
- this._tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
888
- const offset = this._tempPosition.copy(center).applyQuaternion(this._tempQuaternion);
889
- const scale = getWorldScale(obj.gameObject);
890
- offset.multiply(scale);
891
- pos.x -= offset.x;
892
- pos.y -= offset.y;
893
- pos.z -= offset.z;
894
- }
895
- setWorldPositionXYZ(obj.gameObject, pos.x, pos.y, pos.z);
896
- setWorldQuaternionXYZW(obj.gameObject, rot.x, rot.y, rot.z, rot.w);
897
- }
898
- }
899
-
900
- private syncPhysicsBody(obj: Object3D, body: RigidBody, translation: boolean, rotation: boolean) {
901
-
902
- // const bodyType = body.bodyType();
903
- // const previous = physicsBody.translation();
904
- // const vel = physicsBody.linvel();
905
-
906
- const worldPosition = getWorldPosition(obj, this._tempPosition);
907
- const worldQuaternion = getWorldQuaternion(obj, this._tempQuaternion);
908
- const type = body.bodyType();
909
- switch (type) {
910
- case RigidBodyType.Fixed:
911
- case RigidBodyType.KinematicPositionBased:
912
- case RigidBodyType.KinematicVelocityBased:
913
- if (translation)
914
- body.setNextKinematicTranslation(worldPosition);
915
- if (rotation)
916
- body.setNextKinematicRotation(worldQuaternion);
917
- break;
918
- default:
919
- if (translation)
920
- body.setTranslation(worldPosition, false);
921
- if (rotation)
922
- body.setRotation(worldQuaternion, false);
923
- break;
924
-
925
- }
926
- body.wakeUp();
927
- // physicsBody.setBodyType(RAPIER.RigidBodyType.Fixed);
928
- // physicsBody.setLinvel(vel, false);
929
-
930
- // update velocity
931
- // const pos = physicsBody.translation();
932
- // pos.x -= previous.x;
933
- // pos.y -= previous.y;
934
- // pos.z -= previous.z;
935
- // // threhold
936
- // const t = 1;
937
- // const canUpdateVelocity = Math.abs(pos.x) < t && Math.abs(pos.y) < t && Math.abs(pos.z) < t;
938
- // if (canUpdateVelocity) {
939
- // const damping = 1 + this.context.time.deltaTime;
940
- // vel.x *= damping;
941
- // vel.y *= damping;
942
- // vel.z *= damping;
943
- // vel.x += pos.x;
944
- // vel.y += pos.y;
945
- // vel.z += pos.z;
946
- // console.log(vel);
947
- // physicsBody.setLinvel(vel, true);
948
- // }
949
- // else if(debugPhysics) console.warn("Movement exceeded threshold, not updating velocity", pos);
950
-
951
- // body.setBodyType(bodyType);
952
- }
953
-
954
- private static _matricesBuffer: Matrix4[] = [];
955
- private getRigidbodyRelativeMatrix(comp: Object3D, rigidbody: Object3D, mat: Matrix4, matrices?: Matrix4[]): Matrix4 {
956
- // collect all matrices to the rigidbody and then build the rigidbody relative matrix
957
- if (matrices === undefined) {
958
- matrices = Physics._matricesBuffer;
959
- matrices.length = 0;
960
- }
961
- if (comp === rigidbody) {
962
- const scale = getWorldScale(comp, this._tempPosition);
963
- mat.makeScale(scale.x, scale.y, scale.z);
964
- for (let i = matrices.length - 1; i >= 0; i--) {
965
- mat.multiply(matrices[i]);
966
- }
967
- return mat;
968
- }
969
- matrices.push(comp.matrix);
970
- if (comp.parent) {
971
- this.getRigidbodyRelativeMatrix(comp.parent, rigidbody, mat, matrices);
972
- }
973
- return mat;
974
- }
975
-
976
- private static centerConnectionPos = { x: 0, y: 0, z: 0 };
977
- private static centerConnectionRot = { x: 0, y: 0, z: 0, w: 1 };
978
-
979
-
980
-
981
- addFixedJoint(body1: IRigidbody, body2: IRigidbody) {
982
- if (!this.world) {
983
- console.error("Physics world not initialized");
984
- return;
985
- }
986
- const b1 = body1[$bodyKey] as RigidBody;
987
- const b2 = body2[$bodyKey] as RigidBody;
988
-
989
- this.calculateJointRelativeMatrices(body1.gameObject, body2.gameObject, this._tempMatrix);
990
- this._tempMatrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
991
-
992
- const params = JointData.fixed(
993
- Physics.centerConnectionPos, Physics.centerConnectionRot,
994
- this._tempPosition, this._tempQuaternion,
995
- );
996
- const joint = this.world.createImpulseJoint(params, b1, b2, true);
997
- if (debugPhysics)
998
- console.log("ADD FIXED JOINT", joint)
999
- }
1000
-
1001
-
1002
- /** The joint prevents any relative movement between two rigid-bodies, except for relative rotations along one axis. This is typically used to simulate wheels, fans, etc. They are characterized by one local anchor as well as one local axis on each rigid-body. */
1003
- addHingeJoint(body1: IRigidbody, body2: IRigidbody, anchor: { x: number, y: number, z: number }, axis: { x: number, y: number, z: number }) {
1004
- if (!this.world) {
1005
- console.error("Physics world not initialized");
1006
- return;
1007
- }
1008
- const b1 = body1[$bodyKey] as RigidBody;
1009
- const b2 = body2[$bodyKey] as RigidBody;
1010
-
1011
- this.calculateJointRelativeMatrices(body1.gameObject, body2.gameObject, this._tempMatrix);
1012
- this._tempMatrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
1013
-
1014
- let params = RAPIER.JointData.revolute(anchor, this._tempPosition, axis);
1015
- let joint = this.world.createImpulseJoint(params, b1, b2, true);
1016
- if (debugPhysics)
1017
- console.log("ADD HINGE JOINT", joint)
1018
- }
1019
-
1020
-
1021
- private calculateJointRelativeMatrices(body1: IGameObject, body2: IGameObject, mat: Matrix4) {
1022
- body1.updateWorldMatrix(true, false);
1023
- body2.updateWorldMatrix(true, false);
1024
- const world1 = body1.matrixWorld;
1025
- const world2 = body2.matrixWorld;
1026
- // set scale to 1
1027
- world1.elements[0] = 1;
1028
- world1.elements[5] = 1;
1029
- world1.elements[10] = 1;
1030
- world2.elements[0] = 1;
1031
- world2.elements[5] = 1;
1032
- world2.elements[10] = 1;
1033
- mat.copy(world2).premultiply(world1.invert()).invert();
1034
- }
1035
221
  }
1036
-
1037
-
1038
-
1039
- /** responsible of processing collision events for the component system */
1040
- class PhysicsCollisionHandler {
1041
-
1042
- readonly world: World;
1043
- readonly eventQueue: EventQueue;
1044
-
1045
- constructor(world: World, eventQueue: EventQueue) {
1046
- this.world = world;
1047
- this.eventQueue = eventQueue;
1048
- }
1049
-
1050
- private activeCollisions: Array<{ collider: ICollider, component: IComponent, collision: Collision }> = [];
1051
- private activeCollisionsStay: Array<{ collider: ICollider, component: IComponent, collision: Collision }> = [];
1052
- private activeTriggers: Array<{ collider: ICollider, component: IComponent, otherCollider: ICollider }> = [];
1053
-
1054
- handleCollisionEvents() {
1055
- if (!this.eventQueue) return;
1056
- if (!this.world) return;
1057
- this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
1058
- const col1 = this.world!.getCollider(handle1);
1059
- const col2 = this.world!.getCollider(handle2);
1060
- const colliderComponent1 = col1[$componentKey];
1061
- const colliderComponent2 = col2[$componentKey];
1062
- if (debugCollisions)
1063
- console.log("EVT", colliderComponent1.name, colliderComponent2.name, started, col1, col2);
1064
- if (colliderComponent1 && colliderComponent2) {
1065
- if (started) {
1066
- this.onCollisionStarted(colliderComponent1, col1, colliderComponent2, col2);
1067
- this.onCollisionStarted(colliderComponent2, col2, colliderComponent1, col1);
1068
- }
1069
- else {
1070
- this.onCollisionEnded(colliderComponent1, colliderComponent2);
1071
- this.onCollisionEnded(colliderComponent2, colliderComponent1);
1072
- }
1073
- }
1074
- });
1075
- }
1076
-
1077
- update() {
1078
- this.onHandleCollisionStay();
1079
- }
1080
-
1081
- private onCollisionStarted(self: ICollider, selfBody: Collider, other: ICollider, otherBody: Collider) {
1082
- let collision: Collision | null = null;
1083
-
1084
- // if one is a trigger we dont get collisions but want to raise the trigger events
1085
- if (self.isTrigger || other.isTrigger) {
1086
- foreachComponent(self.gameObject, (c: IComponent) => {
1087
- if (c.onTriggerEnter && !c.destroyed) {
1088
- c.onTriggerEnter(other);
1089
- }
1090
- this.activeTriggers.push({ collider: self, component: c, otherCollider: other });
1091
- });
1092
- }
1093
- else {
1094
- const object = self.gameObject;
1095
- // TODO: we dont respect the flip value here!
1096
- this.world.contactPair(selfBody, otherBody, (manifold, _flipped) => {
1097
- foreachComponent(object, (c: IComponent) => {
1098
- if(c.destroyed) return;
1099
- const hasDeclaredEventMethod = c.onCollisionEnter || c.onCollisionStay || c.onCollisionExit;
1100
- if (hasDeclaredEventMethod || debugCollisions) {
1101
- if (!collision) {
1102
- const contacts: Array<ContactPoint> = [];
1103
- const normal = manifold.normal();
1104
- for (let i = 0; i < manifold.numSolverContacts(); i++) {
1105
- // solver points are in world space
1106
- // https://rapier.rs/docs/user_guides/javascript/advanced_collision_detection_js#the-contact-graph
1107
- const pt = manifold.solverContactPoint(i);
1108
- const impulse = manifold.contactImpulse(i);
1109
- if (pt) {
1110
- const dist = manifold.contactDist(i);
1111
- const friction = manifold.solverContactFriction(i);
1112
- const contact = new ContactPoint(pt, dist, normal, impulse, friction);
1113
- contacts.push(contact);
1114
- if (debugCollisions) {
1115
- Gizmos.DrawDirection(pt, normal, 0xff0000, 3, true);
1116
- }
1117
- }
1118
- }
1119
- collision = new Collision(object, other, contacts);
1120
- }
1121
-
1122
- // we only need to keep track if any event exists
1123
- if (hasDeclaredEventMethod) {
1124
- const info = { collider: self, component: c, collision };
1125
-
1126
- this.activeCollisions.push(info);
1127
- if (c.onCollisionStay) {
1128
- this.activeCollisionsStay.push(info);
1129
- }
1130
-
1131
- c.onCollisionEnter?.call(c, collision);
1132
- }
1133
-
1134
- }
1135
- });
1136
- });
1137
- }
1138
- }
1139
-
1140
- private onHandleCollisionStay() {
1141
- for (const active of this.activeCollisionsStay) {
1142
- const c = active.component;
1143
- if(c.destroyed) continue;
1144
- if (c.activeAndEnabled && c.onCollisionStay) {
1145
- const arg = active.collision;
1146
- c.onCollisionStay(arg);
1147
- }
1148
- }
1149
- for (const active of this.activeTriggers) {
1150
- const c = active.component;
1151
- if(c.destroyed) continue;
1152
- if (c.activeAndEnabled && c.onTriggerStay) {
1153
- const arg = active.otherCollider;
1154
- c.onTriggerStay(arg);
1155
- }
1156
- }
1157
- }
1158
-
1159
- private onCollisionEnded(self: ICollider, other: ICollider) {
1160
- if(self.destroyed || other.destroyed) return;
1161
- for (let i = 0; i < this.activeCollisions.length; i++) {
1162
- const active = this.activeCollisions[i];
1163
- const collider = active.collider;
1164
- if(collider.destroyed) {
1165
- this.activeCollisions.splice(i, 1);
1166
- i--;
1167
- continue;
1168
- }
1169
- if (collider === self && active.collision.collider === other) {
1170
- const c = active.component;
1171
- this.activeCollisions.splice(i, 1);
1172
- i--;
1173
- if (c.activeAndEnabled && c.onCollisionExit) {
1174
- const collision = active.collision;
1175
- c.onCollisionExit(collision);
1176
- }
1177
- }
1178
- }
1179
- for (let i = 0; i < this.activeCollisionsStay.length; i++) {
1180
- const active = this.activeCollisionsStay[i];
1181
- const collider = active.collider;
1182
- if(collider.destroyed) {
1183
- this.activeCollisionsStay.splice(i, 1);
1184
- i--;
1185
- continue;
1186
- }
1187
- if (collider === self && active.collision.collider === other) {
1188
- const c = active.component;
1189
- this.activeCollisionsStay.splice(i, 1);
1190
- i--;
1191
- if (c.activeAndEnabled && c.onCollisionExit) {
1192
- const collision = active.collision;
1193
- c.onCollisionExit(collision);
1194
- }
1195
- }
1196
- }
1197
- for (let i = 0; i < this.activeTriggers.length; i++) {
1198
- const active = this.activeTriggers[i];
1199
- const collider = active.collider;
1200
- if(collider.destroyed) {
1201
- this.activeTriggers.splice(i, 1);
1202
- i--;
1203
- continue;
1204
- }
1205
- if (collider === self && active.otherCollider === other) {
1206
- const c = active.component;
1207
- this.activeTriggers.splice(i, 1);
1208
- i--;
1209
- if (c.activeAndEnabled && c.onTriggerExit) {
1210
- const collision = active.otherCollider;
1211
- c.onTriggerExit(collision);
1212
- }
1213
- }
1214
- }
1215
- }
1216
- }
src/engine/engine_physics.types.ts CHANGED
@@ -1,8 +1,6 @@
1
1
 
2
2
 
3
-
4
- export enum PhysicsMaterialCombine
5
- {
3
+ export enum PhysicsMaterialCombine {
6
4
  Average = 0,
7
5
  Multiply = 1,
8
6
  Minimum = 2,
src/engine/engine_types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { RenderTexture } from "./engine_texture";
2
- import { Camera, Color, Material, Object3D, Vector3, Quaternion, Ray, Scene, Renderer, WebGLRenderer } from "three";
2
+ import { Camera, Color, Material, Object3D, Vector3, Quaternion, Ray, Scene, Renderer, WebGLRenderer, Mesh } from "three";
3
3
  import { RGBAColor } from "../engine-components/js-extensions/RGBAColor";
4
4
  import { CollisionDetectionMode, PhysicsMaterial, RigidbodyConstraints } from "./engine_physics.types";
5
5
  import { CircularBuffer } from "./engine_utils";
@@ -20,7 +20,6 @@
20
20
  generateUUID(): string;
21
21
  }
22
22
 
23
-
24
23
  export declare type CoroutineData = {
25
24
  comp: IComponent,
26
25
  main: Generator,
@@ -31,9 +30,17 @@
31
30
  get time(): number;
32
31
  }
33
32
 
33
+ export interface IInput {
34
+ convertScreenspaceToRaycastSpace(vec: Vec2): void;
35
+ }
36
+
37
+ export interface IPhysics {
38
+ engine?: IPhysicsEngine;
39
+ }
40
+
34
41
  export interface IContext {
35
42
  alias?: string | null;
36
- hash?:string;
43
+ hash?: string;
37
44
 
38
45
  scene: Scene;
39
46
  renderer: WebGLRenderer;
@@ -42,6 +49,8 @@
42
49
  domElement: HTMLElement;
43
50
 
44
51
  time: ITime;
52
+ input: IInput;
53
+ physics: IPhysics;
45
54
 
46
55
  scripts: IComponent[];
47
56
  scripts_pausedChanged: IComponent[];
@@ -343,4 +352,57 @@
343
352
  // }
344
353
  // return this._point;
345
354
  // }
346
- }
355
+ }
356
+
357
+ export type RaycastResult = null | { point: Vector3, collider: ICollider, normal?: Vector3 };
358
+
359
+ export class SphereOverlapResult {
360
+ object: Object3D;
361
+ collider: ICollider;
362
+ constructor(object: Object3D, collider: ICollider) {
363
+ this.object = object;
364
+ this.collider = collider;
365
+ }
366
+ }
367
+
368
+
369
+ export interface IPhysicsEngine {
370
+ initialize(ctx: IContext): Promise<boolean>;
371
+ step(dt: number): void;
372
+ postStep();
373
+ get isUpdating(): boolean;
374
+ /** clear all possibly cached data (e.g. mesh data when creating scaled mesh colliders) */
375
+ clearCaches();
376
+
377
+ // raycasting
378
+ /** fast raycast without getting the normal vector */
379
+ raycast(origin: Vec2 | Vec3, direction: Vec3 | undefined, maxDistance: number, solid: boolean): RaycastResult;
380
+ /** raycast that also gets the normal vector. If you don't need it use raycast() */
381
+ raycastAndGetNormal(origin: Vec2 | Vec3, direction: Vec3 | undefined, maxDistance: number, solid: boolean) : RaycastResult;
382
+ sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult>;
383
+
384
+ // Collider methods
385
+ addSphereCollider(collider: ICollider, center: Vector3, radius: number);
386
+ addBoxCollider(collider: ICollider, center: Vector3, size: Vector3);
387
+ addCapsuleCollider(collider: ICollider, center: Vector3, radius: number, height: number);
388
+ addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale: Vector3);
389
+
390
+ // Rigidbody methods
391
+ wakeup(rb: IRigidbody);
392
+ updateProperties(rb: IRigidbody);
393
+ resetForces(rb: IRigidbody, wakeup: boolean);
394
+ resetTorques(rb: IRigidbody, wakeup: boolean);
395
+ addForce(rb: IRigidbody, vec: Vec3, wakeup: boolean);
396
+ applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean);
397
+ getLinearVelocity(rb: IRigidbody): Vec3 | null;
398
+ getAngularVelocity(rb: IRigidbody): Vec3 | null;
399
+ setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
400
+ setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
401
+
402
+ updateBody(comp: ICollider | IRigidbody, translation: boolean, rotation: boolean);
403
+ removeBody(body: IComponent);
404
+
405
+ // Joints
406
+ addFixedJoint(body1: IRigidbody, body2: IRigidbody)
407
+ addHingeJoint(body1: IRigidbody, body2: IRigidbody, anchor: Vec3, axis: Vec3)
408
+ }
src/engine-components/Joints.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  export class FixedJoint extends Joint {
33
33
 
34
34
  protected createJoint(self: Rigidbody, other: Rigidbody) {
35
- this.context.physics.addFixedJoint(self, other);
35
+ this.context.physics.engine?.addFixedJoint(self, other);
36
36
  }
37
37
  }
38
38
 
@@ -46,7 +46,7 @@
46
46
 
47
47
  protected createJoint(self: Rigidbody, other: Rigidbody) {
48
48
  if (this.axis && this.anchor)
49
- this.context.physics.addHingeJoint(self, other, this.anchor, this.axis);
49
+ this.context.physics.engine?.addHingeJoint(self, other, this.anchor, this.axis);
50
50
  }
51
51
 
52
52
  }
src/engine-components/ui/RectTransform.ts CHANGED
@@ -191,7 +191,8 @@
191
191
 
192
192
  const uiobject = this.shadowComponent;
193
193
  if (!uiobject) return;
194
- this._parentRectTransform = GameObject.getComponentInParent(this.gameObject.parent!, RectTransform) as RectTransform;
194
+ if (!this.gameObject.parent) return;
195
+ this._parentRectTransform = GameObject.getComponentInParent(this.gameObject.parent, RectTransform) as RectTransform;
195
196
 
196
197
  this._transformNeedsUpdate = false;
197
198
  this.lastMatrix.copy(this.gameObject.matrix);
@@ -228,7 +229,7 @@
228
229
  else {
229
230
  // We have to rotate the canvas when it's in worldspace
230
231
  const canvas = this.Root as any as ICanvas;
231
- if (!canvas.screenspace) uiobject.rotation.y = Math.PI;
232
+ if (canvas && !canvas.screenspace) uiobject.rotation.y = Math.PI;
232
233
  }
233
234
 
234
235
  // iterate other components on this object that might need to know about the transform change
src/engine-components/RigidBody.ts CHANGED
@@ -89,7 +89,7 @@
89
89
  this.position = {};
90
90
  // this.position = this.obj.position.clone();
91
91
  this._positionWatch.subscribeWrite((val, prop) => {
92
- if (this.context.physics.isUpdating || this.mute) return;
92
+ if (this.context.physics.engine?.isUpdating || this.mute) return;
93
93
  const prev = this.position![prop];
94
94
  if (Math.abs(prev - val) < .00001) return;
95
95
  this.position![prop] = val;
@@ -103,7 +103,7 @@
103
103
  this.quaternion = {};
104
104
  // this.quaternion = this.obj.quaternion.clone();
105
105
  this._rotationWatch.subscribeWrite((val, prop) => {
106
- if (this.context.physics.isUpdating || this.mute) return;
106
+ if (this.context.physics.engine?.isUpdating || this.mute) return;
107
107
  const prev = this.quaternion![prop];
108
108
  if (Math.abs(prev - val) < .00001) return;
109
109
  this.quaternion![prop] = val;
@@ -245,11 +245,11 @@
245
245
 
246
246
  onDisable() {
247
247
  this._watch?.stop();
248
- this.context.physics.removeBody(this);
248
+ this.context.physics.engine?.removeBody(this);
249
249
  }
250
250
 
251
251
  onDestroy(): void {
252
- this.context.physics.removeBody(this);
252
+ this.context.physics.engine?.removeBody(this);
253
253
  }
254
254
 
255
255
  onValidate() {
@@ -261,12 +261,12 @@
261
261
  while (true) {
262
262
  if (this._propertiesChanged) {
263
263
  this._propertiesChanged = false;
264
- this.context.physics.updateProperties(this);
264
+ this.context.physics.engine?.updateProperties(this);
265
265
  }
266
266
  if (this._watch?.isDirty) {
267
267
  this._watch.mute = true;
268
268
  this._watch.applyValues();
269
- this.context.physics.updateBody(this, this._watch.positionChanged, this._watch.rotationChanged);
269
+ this.context.physics.engine?.updateBody(this, this._watch.positionChanged, this._watch.rotationChanged);
270
270
  this._watch.reset();
271
271
  }
272
272
  else this._watch?.syncValues();
@@ -275,10 +275,6 @@
275
275
  }
276
276
  }
277
277
 
278
- private get body() {
279
- return this.context.physics.internal_getRigidbody(this);
280
- }
281
-
282
278
  public teleport(pt: { x: number, y: number, z: number }, localspace: boolean = true) {
283
279
  this._watch?.reset(true);
284
280
  if (localspace) this.gameObject.position.set(pt.x, pt.y, pt.z);
@@ -288,11 +284,11 @@
288
284
  }
289
285
 
290
286
  public resetForces() {
291
- this.body?.resetForces(true);
287
+ this.context.physics.engine?.resetForces(this, true);
292
288
  }
293
289
 
294
290
  public resetTorques() {
295
- this.body?.resetTorques(true);
291
+ this.context.physics.engine?.resetTorques(this, true);
296
292
  }
297
293
 
298
294
  public resetVelocities() {
@@ -306,24 +302,24 @@
306
302
  }
307
303
 
308
304
  public wakeUp() {
309
- this.body?.wakeUp();
305
+ this.context.physics.engine?.wakeup(this);
310
306
  }
311
307
 
312
308
  public applyForce(vec: Vector3, _rel?: THREE.Vector3) {
313
- this.body?.addForce(vec, true);
309
+ this.context.physics.engine?.addForce(this, vec, true);
314
310
  }
315
311
 
316
312
  public applyImpulse(vec: Vector3) {
317
- this.body?.applyImpulse(vec, true);
313
+ this.context.physics.engine?.applyImpulse(this, vec, true);
318
314
  }
319
315
 
320
316
  public setForce(x: number, y: number, z: number) {
321
- this.body?.resetForces(true);
322
- this.body?.addForce({ x, y, z }, true);
317
+ this.context.physics.engine?.resetForces(this, true);
318
+ this.context.physics.engine?.addForce(this, { x, y, z }, true);
323
319
  }
324
320
 
325
321
  public getVelocity(): Vector3 {
326
- const vel = this.body?.linvel();
322
+ const vel = this.context.physics.engine?.getLinearVelocity(this);
327
323
  if (!vel) return this._currentVelocity.set(0, 0, 0);
328
324
  this._currentVelocity.x = vel.x;
329
325
  this._currentVelocity.y = vel.y;
@@ -334,25 +330,25 @@
334
330
  public setVelocity(x: number | Vector3, y?: number, z?: number) {
335
331
  if (x instanceof Vector3) {
336
332
  const vec = x;
337
- this.body?.setLinvel(vec, true);
333
+ this.context.physics.engine?.setLinearVelocity(this,vec, true);
338
334
  return;
339
335
  }
340
336
  if (y === undefined || z === undefined) return;
341
- this.body?.setLinvel({ x: x, y: y, z: z }, true);
337
+ this.context.physics.engine?.setLinearVelocity(this, { x: x, y: y, z: z }, true);
342
338
  }
343
339
 
344
340
  public setAngularVelocity(x: number | Vector3, y?: number, z?: number) {
345
341
  if (x instanceof Vector3) {
346
342
  const vec = x;
347
- this.body?.setAngvel(vec, true);
343
+ this.context.physics.engine?.setAngularVelocity(this, vec, true);
348
344
  return;
349
345
  }
350
346
  if (y === undefined || z === undefined) return;
351
- this.body?.setAngvel({ x: x, y: y, z: z }, true);
347
+ this.context.physics.engine?.setAngularVelocity(this, { x: x, y: y, z: z }, true);
352
348
  }
353
349
 
354
350
  public getAngularVelocity(): Vector3 {
355
- const vel = this.body?.angvel();
351
+ const vel = this.context.physics.engine?.getAngularVelocity(this);
356
352
  if (!vel) return this._currentVelocity.set(0, 0, 0);
357
353
  this._currentVelocity.x = vel.x;
358
354
  this._currentVelocity.y = vel.y;
@@ -381,13 +377,10 @@
381
377
 
382
378
 
383
379
  private captureVelocity() {
384
- if (this.body) {
385
- const wp = getWorldPosition(this.gameObject);
386
- Rigidbody.tempPosition.copy(wp);
387
- const vel = wp.sub(this._lastPosition);
388
- this._lastPosition.copy(Rigidbody.tempPosition);
389
- this._smoothedVelocity.lerp(vel, this.context.time.deltaTime / .1);
390
- // this._smoothedVelocity.set(0, 1 / this.context.time.deltaTime, 0);
391
- }
380
+ const wp = getWorldPosition(this.gameObject);
381
+ Rigidbody.tempPosition.copy(wp);
382
+ const vel = wp.sub(this._lastPosition);
383
+ this._lastPosition.copy(Rigidbody.tempPosition);
384
+ this._smoothedVelocity.lerp(vel, this.context.time.deltaTime / .1);
392
385
  }
393
386
  }
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -17,6 +17,9 @@
17
17
  Camera,
18
18
  Color,
19
19
  MeshStandardMaterial,
20
+ LinearEncoding,
21
+ sRGBEncoding,
22
+ MeshPhysicalMaterial,
20
23
  } from 'three';
21
24
  import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
22
25
 
@@ -400,13 +403,7 @@
400
403
 
401
404
  async parse( scene, options: USDZExporterOptions = new USDZExporterOptions() ) {
402
405
 
403
- options = Object.assign( {
404
- ar: {
405
- anchoring: { type: 'plane' },
406
- planeAnchoring: { alignment: 'horizontal' }
407
- },
408
- extensions: []
409
- }, options );
406
+ options = Object.assign( new USDZExporterOptions(), options );
410
407
 
411
408
  this.sceneAnchoringOptions = options;
412
409
  // @ts-ignore
@@ -598,7 +595,7 @@
598
595
 
599
596
  writer.appendLine( `token preliminary:anchoring:type = "${context.exporter.sceneAnchoringOptions.ar.anchoring.type}"` );
600
597
  if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'plane')
601
- writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.ar.planeAnchoring.alignment}"` );
598
+ writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.planeAnchoring.alignment}"` );
602
599
  // bit hacky as we don't have a callback here yet. Relies on the fact that the image is named identical in the ImageTracking extension.
603
600
  if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'image')
604
601
  writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
@@ -679,22 +676,39 @@
679
676
 
680
677
  const geometry = new PlaneGeometry( 2, 2, 1, 1 );
681
678
  const material = new ShaderMaterial( {
682
- uniforms: { blitTexture: new Uniform( texture ) },
683
- vertexShader: `
684
- varying vec2 vUv;
685
- void main(){
686
- vUv = uv;
687
- gl_Position = vec4(position.xy * 1.0,0.,.999999);
688
- }`,
689
- fragmentShader: `
690
- uniform sampler2D blitTexture;
691
- varying vec2 vUv;
692
- void main(){
693
- gl_FragColor = vec4(vUv.xy, 0, 1);
694
- gl_FragColor = texture2D( blitTexture, vUv);
695
- }`
696
- } );
679
+ uniforms: {
680
+ blitTexture: new Uniform( texture ),
681
+ },
682
+ defines: {
683
+ IS_SRGB: texture.encoding == sRGBEncoding,
684
+ },
685
+ vertexShader: `
686
+ varying vec2 vUv;
687
+ void main(){
688
+ vUv = uv;
689
+ vUv.y = 1. - vUv.y;
690
+ gl_Position = vec4(position.xy * 1.0,0.,.999999);
691
+ }`,
692
+ fragmentShader: `
693
+ uniform sampler2D blitTexture;
694
+ varying vec2 vUv;
697
695
 
696
+ // took from threejs 05fc79cd52b79e8c3e8dec1e7dca72c5c39983a4
697
+ vec4 conv_LinearTosRGB( in vec4 value ) {
698
+ return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
699
+ }
700
+
701
+ void main(){
702
+ gl_FragColor = vec4(vUv.xy, 0, 1);
703
+
704
+ #ifdef IS_SRGB
705
+ gl_FragColor = conv_LinearTosRGB( texture2D( blitTexture, vUv) );
706
+ #else
707
+ gl_FragColor = texture2D( blitTexture, vUv);
708
+ #endif
709
+ }`
710
+ } );
711
+
698
712
  const mesh = new Mesh( geometry, material );
699
713
  mesh.frustumCulled = false;
700
714
  const cam = new PerspectiveCamera();
@@ -705,7 +719,9 @@
705
719
  renderer.clear();
706
720
  renderer.render( scene, cam );
707
721
 
708
- return new Texture( renderer.domElement );
722
+ const tex = new Texture( renderer.domElement );
723
+ tex.encoding = texture.encoding;
724
+ return tex;
709
725
 
710
726
  }
711
727
 
@@ -825,7 +841,10 @@
825
841
  }
826
842
 
827
843
  if ( geometry )
828
- writer.beginBlock( `def Xform "${name}" (prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>)` );
844
+ writer.beginBlock( `def Xform "${name}" (
845
+ prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>
846
+ prepend apiSchemas = ["MaterialBindingAPI"]
847
+ )` );
829
848
  else if ( camera )
830
849
  writer.beginBlock( `def Camera "${name}"` );
831
850
  else
@@ -929,11 +948,11 @@
929
948
  )
930
949
  point3f[] points = [${buildVector3Array( attributes.position, count )}]
931
950
  ${attributes.uv ?
932
- `float2[] primvars:st = [${buildVector2Array( attributes.uv, count )}] (
951
+ `texCoord2f[] primvars:st = [${buildVector2Array( attributes.uv, count )}] (
933
952
  interpolation = "vertex"
934
953
  )` : '' }
935
954
  ${attributes.uv2 ?
936
- `float2[] primvars:st2 = [${buildVector2Array( attributes.uv2, count )}] (
955
+ `texCoord2f[] primvars:st2 = [${buildVector2Array( attributes.uv2, count )}] (
937
956
  interpolation = "vertex"
938
957
  )` : '' }
939
958
  uniform token subdivisionScheme = "none"
@@ -1051,7 +1070,7 @@
1051
1070
 
1052
1071
  }
1053
1072
 
1054
- function buildMaterial( material, textures ) {
1073
+ function buildMaterial( material: MeshStandardMaterial, textures ) {
1055
1074
 
1056
1075
  // https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html
1057
1076
 
@@ -1090,6 +1109,13 @@
1090
1109
  const textureTransformInput = `</Materials/Material_${material.id}/${uvReader}.outputs:result>`;
1091
1110
  const textureTransformOutput = `</Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1092
1111
 
1112
+ const rawTextureExtra = `(
1113
+ colorSpace = "Raw"
1114
+ )`;
1115
+ const needsTextureScale = mapType !== 'normal' && (color && (color.r !== 1 || color.g !== 1 || color.b !== 1 || opacity !== 1)) || false;
1116
+ const needsNormalScaleAndBias = mapType === 'normal';
1117
+ const normalScaleValueString = (material.normalScale ? material.normalScale.x * 2 : 2).toFixed( PRECISION );
1118
+
1093
1119
  return `
1094
1120
  ${needsTextureTransform ? `def Shader "Transform2d_${mapType}" (
1095
1121
  sdrMetadata = {
@@ -1107,9 +1133,15 @@
1107
1133
  def Shader "Texture_${texture.id}_${mapType}"
1108
1134
  {
1109
1135
  uniform token info:id = "UsdUVTexture"
1110
- asset inputs:file = @textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}@
1136
+ asset inputs:file = @textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}@ ${mapType === 'normal' ? rawTextureExtra : ''}
1111
1137
  float2 inputs:st.connect = ${needsTextureTransform ? textureTransformOutput : textureTransformInput}
1138
+ ${needsTextureScale ? `
1112
1139
  float4 inputs:scale = (${color ? color.r + ', ' + color.g + ', ' + color.b : '1, 1, 1'}, ${opacity ? opacity : '1'})
1140
+ ` : `` }
1141
+ ${needsNormalScaleAndBias ? `
1142
+ float4 inputs:scale = (${normalScaleValueString}, ${normalScaleValueString}, ${normalScaleValueString}, 1)
1143
+ float4 inputs:bias = (-1, -1, -1, 0)
1144
+ ` : `` }
1113
1145
  token inputs:wrapS = "${wrapS}"
1114
1146
  token inputs:wrapT = "${wrapT}"
1115
1147
  float outputs:r
@@ -1221,7 +1253,7 @@
1221
1253
 
1222
1254
  }
1223
1255
 
1224
- if ( material.isMeshPhysicalMaterial ) {
1256
+ if ( material instanceof MeshPhysicalMaterial ) {
1225
1257
 
1226
1258
  inputs.push( `${pad}float inputs:clearcoat = ${material.clearcoat}` );
1227
1259
  inputs.push( `${pad}float inputs:clearcoatRoughness = ${material.clearcoatRoughness}` );
plugins/vite/defines.js ADDED
@@ -0,0 +1,30 @@
1
+ import { loadConfig } from "./config.js";
2
+
3
+ /** used to pass config variables into vite.config.define
4
+ * for example "useRapier"
5
+ */
6
+ export const needleDefines = (command, config, userSettings) => {
7
+
8
+ if (!userSettings) userSettings = {};
9
+
10
+ let useRapier = true;
11
+ if (config.useRapier === false || userSettings?.useRapier === false) useRapier = false;
12
+
13
+ return {
14
+ name: 'needle-defines',
15
+ enforce: 'pre',
16
+ config(config) {
17
+ if (useRapier && userSettings?.useRapier !== true) {
18
+ const meta = loadConfig();
19
+ if (meta?.useRapier === false) {
20
+ useRapier = false;
21
+ }
22
+ }
23
+ console.log("UseRapier?", useRapier);
24
+ if (!config.define) config.define = {};
25
+ if (config.define.NEEDLE_USE_RAPIER === undefined) {
26
+ config.define.NEEDLE_USE_RAPIER = useRapier;
27
+ }
28
+ }
29
+ }
30
+ }
plugins/vite/dependency-watcher.js ADDED
@@ -0,0 +1,173 @@
1
+ import { exec, execSync } from 'child_process';
2
+ import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const prefix = "[needle-dependency-watcher] ";
7
+ function log(...msg) {
8
+ console.log(prefix, ...msg)
9
+ }
10
+
11
+ export const needleDependencyWatcher = (command, config, userSettings) => {
12
+ if (command === "build") return;
13
+
14
+ if (userSettings?.noDependencyWatcher === true) return;
15
+
16
+ const dir = process.cwd();
17
+ const packageJsonPath = path.join(dir, "package.json");
18
+ const viteCacheDir = path.join(dir, "node_modules", ".vite");
19
+
20
+ return {
21
+ name: 'needle-dependency-watcher',
22
+ configureServer(server) {
23
+ watchPackageJson(server, dir, packageJsonPath, viteCacheDir);
24
+ manageClients(server);
25
+ }
26
+ }
27
+ }
28
+
29
+ const currentClients = new Set();
30
+
31
+ function manageClients(server) {
32
+ server.ws.on("connection", (socket) => {
33
+ currentClients.add(socket);
34
+ socket.on("close", () => {
35
+ currentClients.delete(socket);
36
+ });
37
+ });
38
+ }
39
+
40
+ function triggerReloadOnClients() {
41
+ log("Triggering reload on clients (todo)", currentClients.size)
42
+ // for (const client of currentClients) {
43
+ // client.send(JSON.stringify({ type: "full-reload" }));
44
+ // }
45
+ }
46
+
47
+
48
+ let packageJsonStat;
49
+ let lastEditTime;
50
+ let packageJsonSize;
51
+ let packageJson;
52
+ let requireInstall = false;
53
+
54
+ function watchPackageJson(server, projectDir, packageJsonPath, cachePath) {
55
+
56
+ if (!existsSync(packageJsonPath)) {
57
+ return;
58
+ }
59
+
60
+ log("Watching project", packageJsonPath)
61
+
62
+ lastRestartTime = 0;
63
+ packageJsonStat = statSync(packageJsonPath);
64
+ lastEditTime = packageJsonStat.mtime;
65
+ packageJsonSize = packageJsonStat.size;
66
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
67
+
68
+ setTimeout(() => {
69
+ requireInstall = testIfInstallIsRequired(projectDir, packageJson);
70
+ }, 1000);
71
+
72
+ setInterval(() => {
73
+ packageJsonStat = statSync(packageJsonPath);
74
+ let modified = false;
75
+ if (packageJsonStat.mtime > lastEditTime) {
76
+ modified = true;
77
+ }
78
+ if (packageJsonStat.size !== packageJsonSize) {
79
+ modified = true;
80
+ }
81
+ if (modified || requireInstall) {
82
+ if (modified)
83
+ log("package.json has changed")
84
+
85
+ let requireReload = false;
86
+ if (!requireInstall) {
87
+ requireInstall = testIfInstallIsRequired(projectDir, packageJson);
88
+ }
89
+
90
+ // test if dependencies changed
91
+ let newPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
92
+ for (const key in newPackageJson.dependencies) {
93
+ if (packageJson.dependencies[key] !== newPackageJson.dependencies[key] && newPackageJson.dependencies[key] !== undefined) {
94
+ log("Dependency added", key)
95
+ requireReload = true;
96
+ }
97
+ }
98
+
99
+
100
+ packageJsonSize = packageJsonStat.size;
101
+ lastEditTime = packageJsonStat.mtime;
102
+
103
+ if (requireReload || requireInstall) {
104
+ restart(server, projectDir, cachePath);
105
+ }
106
+ }
107
+ }, 1000);
108
+ }
109
+
110
+ function testIfInstallIsRequired(projectDir, packageJson) {
111
+
112
+ if (packageJson.dependencies) {
113
+ for (const key in packageJson.dependencies) {
114
+ // make sure the dependency is installed
115
+ const depPath = path.join(projectDir, "node_modules", key);
116
+ if (!existsSync(depPath)) {
117
+ log("Dependency not installed", key)
118
+ return true;
119
+ }
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ let isRunningRestart = false;
126
+ let restartId = 0;
127
+ let lastRestartTime = 0;
128
+
129
+ async function restart(server, projectDir, cachePath) {
130
+
131
+ if (isRunningRestart) return;
132
+ isRunningRestart = true;
133
+
134
+ try {
135
+ const id = ++restartId;
136
+
137
+ if (requireInstall) {
138
+ requireInstall = false;
139
+ log("Installing dependencies...")
140
+ execSync("npm install", { cwd: projectDir, stdio: "inherit" });
141
+ requireInstall = false;
142
+ }
143
+
144
+ if (id !== restartId) return;
145
+ if (Date.now() - lastRestartTime < 1000) return;
146
+ log("Restarting server...")
147
+ lastRestartTime = Date.now();
148
+ requireInstall = false;
149
+ if (existsSync(cachePath))
150
+ rmSync(cachePath, { recursive: true, force: true });
151
+ triggerReloadOnClients();
152
+
153
+ // touch vite config to trigger reload
154
+ // const viteConfigPath = path.join(projectDir, "vite.config.js");
155
+ // if (existsSync(viteConfigPath)) {
156
+ // const content = readFileSync(viteConfigPath, "utf8");
157
+ // writeFileSync(viteConfigPath, content, "utf8");
158
+ // isRunningRestart = false;
159
+ // return;
160
+ // }
161
+
162
+ // check if server is running
163
+ if (server.httpServer.listening)
164
+ server.restart();
165
+ isRunningRestart = false;
166
+ console.log("-----------------------------------------------")
167
+ }
168
+ catch (err) {
169
+ log("Error restarting server", err);
170
+ isRunningRestart = false;
171
+ }
172
+
173
+ }
src/engine/engine_physics_rapier.ts ADDED
@@ -0,0 +1,1127 @@
1
+ import { BufferAttribute, BufferGeometry, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three'
2
+ import { CircularBuffer, getParam } from "./engine_utils"
3
+ import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPositionXYZ, setWorldQuaternionXYZW } from "./engine_three_utils"
4
+ import {
5
+ IPhysicsEngine,
6
+ IComponent,
7
+ ICollider,
8
+ IRigidbody,
9
+ Collision,
10
+ ContactPoint,
11
+ Vec3,
12
+ IGameObject,
13
+ Vec2,
14
+ IContext,
15
+ } from './engine_types';
16
+ import { foreachComponent } from './engine_gameobject';
17
+
18
+ import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray } from '@dimforge/rapier3d-compat';
19
+ import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types';
20
+ import { Gizmos } from './engine_gizmos';
21
+ import { Mathf } from './engine_math';
22
+ import { SphereOverlapResult } from './engine_types';
23
+ import { ContextEvent, ContextRegistry } from './engine_context_registry';
24
+
25
+ const debugPhysics = getParam("debugphysics");
26
+ const debugColliderPlacement = getParam("debugphysicscolliders");
27
+ const debugCollisions = getParam("debugcollisions");
28
+ const showColliders = getParam("showcolliders");
29
+
30
+
31
+ /** on physics body and references the needle component */
32
+ const $componentKey = Symbol("needle component");
33
+ /** on needle component and references physics body */
34
+ const $bodyKey = Symbol("physics body");
35
+ const $colliderRigidbody = Symbol("rigidbody");
36
+
37
+
38
+ let RAPIER: undefined | any = undefined;
39
+ declare const NEEDLE_USE_RAPIER: boolean;
40
+
41
+
42
+ if (NEEDLE_USE_RAPIER) {
43
+ ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => {
44
+ if (debugPhysics)
45
+ console.log("Register rapier physics backend")
46
+ evt.context.physics.engine = new RapierPhysics();
47
+ // We want the physics engine to be initialized on start so when components start to enable and modify values they don't have delays
48
+ // TODO: should the promise be returned here to make the engine creation wait?
49
+ if (NEEDLE_USE_RAPIER) {
50
+ evt.context.physics.engine.initialize(evt.context);
51
+ }
52
+ });
53
+ }
54
+
55
+
56
+ declare type PhysicsBody = {
57
+ translation(): { x: number, y: number, z: number }
58
+ rotation(): { x: number, y: number, z: number, w: number }
59
+ }
60
+
61
+ export class RapierPhysics implements IPhysicsEngine {
62
+
63
+ removeBody(obj: IComponent) {
64
+ this.validate();
65
+ const body = obj[$bodyKey];
66
+ obj[$bodyKey] = null;
67
+ if (body && this.world) {
68
+ const index = this.objects.findIndex(o => o === obj);
69
+ if (index >= 0) {
70
+ const body = this.bodies[index];
71
+ this.bodies.splice(index, 1);
72
+ this.objects.splice(index, 1);
73
+
74
+ if (body instanceof Collider) {
75
+ const collider = body as Collider;
76
+ this.world?.removeCollider(collider, true);
77
+
78
+ // remove the rigidbody if it doesnt have colliders anymore
79
+ const rb = collider.parent();
80
+ if (rb && rb.numColliders() <= 0) {
81
+ this.world?.removeRigidBody(rb);
82
+ }
83
+ }
84
+ else if (body instanceof RigidBody) {
85
+ // TODO: running this code below causes a crash in rapier
86
+ // const rb = body as RigidBody;
87
+ // console.log("colliders", rb.numColliders())
88
+ // for (let i = 0; i < rb.numColliders(); i++) {
89
+ // const col = rb.collider(i);
90
+ // this.world?.removeCollider(col, true);
91
+ // }
92
+ // console.log("colliders", rb.numColliders(), rb)
93
+ // console.log(rb.handle, rb.userData);
94
+ // if (rb.userData === undefined)
95
+ // this.world?.removeRigidBody(rb);
96
+ }
97
+
98
+ // check if we need to remove the rigidbody too
99
+ // const col = obj as ICollider;
100
+ // if (col.isCollider && col.attachedRigidbody) {
101
+ // const rb = col.attachedRigidbody[$bodyKey] as RigidBody;
102
+ // if (rb && rb.numColliders() <= 0) {
103
+ // // this.world?.removeRigidBody(rb);
104
+ // }
105
+ // }
106
+ }
107
+ }
108
+ }
109
+
110
+ updateBody(comp: ICollider | IRigidbody, translation: boolean, rotation: boolean) {
111
+ this.validate();
112
+ if (!this.enabled) return;
113
+ if (comp.destroyed || !comp.gameObject) return;
114
+ if (!translation && !rotation) return;
115
+
116
+ if ((comp as ICollider).isCollider === true) {
117
+ // const collider = comp as ICollider;
118
+ console.warn("TODO: implement updating collider position");
119
+ }
120
+ else {
121
+ const rigidbody = comp as IRigidbody;
122
+ const body = rigidbody[$bodyKey];
123
+ if (body) {
124
+ this.syncPhysicsBody(rigidbody.gameObject, body, translation, rotation);
125
+ }
126
+ }
127
+ }
128
+
129
+ updateProperties(rigidbody: IRigidbody) {
130
+ this.validate();
131
+ const physicsBody = rigidbody[$bodyKey];
132
+ if (physicsBody) {
133
+ this.internalUpdateProperties(rigidbody, physicsBody);
134
+ }
135
+ }
136
+ addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
137
+ this.validate();
138
+ const body = this.internal_getRigidbody(rigidbody);
139
+ body?.addForce(force, wakeup)
140
+ }
141
+ addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
142
+ this.validate();
143
+ const body = this.internal_getRigidbody(rigidbody);
144
+ body?.applyImpulse(force, wakeup)
145
+ }
146
+ getLinearVelocity(rigidbody: IRigidbody): Vec3 | null {
147
+ this.validate();
148
+ const body = this.internal_getRigidbody(rigidbody);
149
+ if (body) {
150
+ const vel = body.linvel();
151
+ return vel;
152
+ }
153
+ return null;
154
+ }
155
+ getAngularVelocity(rb: IRigidbody): Vec3 | null {
156
+ this.validate();
157
+ const body = this.internal_getRigidbody(rb);
158
+ if (body) {
159
+ const vel = body.angvel();
160
+ return vel;
161
+ }
162
+ return null;
163
+ }
164
+ resetForces(rb: IRigidbody, wakeup: boolean) {
165
+ this.validate();
166
+ const body = this.internal_getRigidbody(rb);
167
+ body?.resetForces(wakeup);
168
+ }
169
+ resetTorques(rb: IRigidbody, wakeup: boolean) {
170
+ this.validate();
171
+ const body = this.internal_getRigidbody(rb);
172
+ body?.resetTorques(wakeup);
173
+ }
174
+ applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
175
+ this.validate();
176
+ const body = this.internal_getRigidbody(rb);
177
+ body?.applyImpulse(vec, wakeup);
178
+ }
179
+
180
+ wakeup(rb: IRigidbody) {
181
+ this.validate();
182
+ const body = this.internal_getRigidbody(rb);
183
+ body?.wakeUp();
184
+ }
185
+ setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
186
+ this.validate();
187
+ const body = this.internal_getRigidbody(rb);
188
+ body?.setAngvel(vec, wakeup);
189
+ }
190
+ setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
191
+ this.validate();
192
+ const body = this.internal_getRigidbody(rb);
193
+ body?.setLinvel(vec, wakeup);
194
+ }
195
+
196
+ private context?: IContext;
197
+ private _initializePromise?: Promise<boolean>;
198
+ private _isInitialized: boolean = false;
199
+
200
+ async initialize(context: IContext) {
201
+ this.context = context;
202
+ if (!this._initializePromise)
203
+ this._initializePromise = this.internalInitialization();
204
+ return this._initializePromise;
205
+ }
206
+
207
+ private async internalInitialization() {
208
+ // NEEDLE_PHYSICS_INIT_START
209
+ // use .env file with VITE_NEEDLE_USE_RAPIER=false to treeshape rapier
210
+ if (import.meta.env.VITE_NEEDLE_USE_RAPIER === "false") {
211
+ return false;
212
+ }
213
+ // Can be transformed during build time to disable rapier
214
+ if (!NEEDLE_USE_RAPIER) return false;
215
+ if (this._hasCreatedWorld) {
216
+ console.error("Invalid call to create physics world: world is already created");
217
+ return true;
218
+ }
219
+ this._hasCreatedWorld = true;
220
+ if (RAPIER === undefined) {
221
+ RAPIER = await import("@dimforge/rapier3d-compat");
222
+ await RAPIER.init()
223
+ }
224
+ if (debugPhysics) console.log("Physics engine initialized, creating world...");
225
+ this.world = new World(this._gravity);
226
+ this.enabled = true;
227
+ this._isInitialized = true;
228
+ if (debugPhysics) console.log("Physics world created");
229
+ return true;
230
+ // NEEDLE_PHYSICS_INIT_END
231
+ }
232
+
233
+ /** Check is the physics engine has been initialized and the call can be made */
234
+ private validate() {
235
+ if (!this._isInitialized) {
236
+ if (debugPhysics)
237
+ console.warn("Physics engine is not initialized");
238
+ }
239
+ }
240
+
241
+
242
+ private rapierRay = new Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 });
243
+ private raycastVectorsBuffer = new CircularBuffer(() => new Vector3(), 10);
244
+ /** Fast raycast against physics colliders
245
+ * @param origin ray origin in screen or worldspace
246
+ * @param direction ray direction in worldspace
247
+ * @param maxDistance max distance to raycast
248
+ * @param solid if true it will also hit the collider if origin is already inside it
249
+ */
250
+ public raycast(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true)
251
+ : null | { point: Vector3, collider: ICollider } {
252
+
253
+ const ray = this.getPhysicsRay(this.rapierRay, origin, direction);
254
+ if (!ray) return null;
255
+
256
+ const hit = this.world?.castRay(ray, maxDistance, solid, undefined, undefined, undefined, undefined, (c) => {
257
+ // ignore objects in the IgnoreRaycast=2 layer
258
+ return !c[$componentKey]?.gameObject.layers.isEnabled(2);
259
+ });
260
+ if (hit) {
261
+ const point = ray.pointAt(hit.toi);
262
+ const vec = this.raycastVectorsBuffer.get();
263
+ vec.set(point.x, point.y, point.z);
264
+ return { point: vec, collider: hit.collider[$componentKey] };
265
+ }
266
+
267
+ return null;
268
+ }
269
+
270
+ public raycastAndGetNormal(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true)
271
+ : null | { point: Vector3, normal: Vector3, collider: ICollider } {
272
+
273
+ const ray = this.getPhysicsRay(this.rapierRay, origin, direction);
274
+ if (!ray) return null;
275
+
276
+ const hit = this.world?.castRayAndGetNormal(ray, maxDistance, solid, undefined, undefined, undefined, undefined, (c) => {
277
+ // ignore objects in the IgnoreRaycast=2 layer
278
+ return !c[$componentKey]?.gameObject.layers.isEnabled(2);
279
+ });
280
+ if (hit) {
281
+ const point = ray.pointAt(hit.toi);
282
+ const normal = hit.normal;
283
+ const vec = this.raycastVectorsBuffer.get();
284
+ const nor = this.raycastVectorsBuffer.get();
285
+ vec.set(point.x, point.y, point.z);
286
+ nor.set(normal.x, normal.y, normal.z);
287
+ return { point: vec, normal: nor, collider: hit.collider[$componentKey] };
288
+ }
289
+ return null;
290
+ }
291
+
292
+ private getPhysicsRay(ray: Ray, origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined): Ray | null {
293
+ const cam = this.context?.mainCamera;
294
+ // if we get origin in 2d space we need to project it to 3d space
295
+ if (origin["z"] === undefined) {
296
+ if (!cam) {
297
+ console.error("Can not perform raycast from 2d point - no main camera found");
298
+ return null;
299
+ }
300
+ const vec3 = this.raycastVectorsBuffer.get();
301
+ vec3.x = origin.x;
302
+ vec3.y = origin.y;
303
+ vec3.z = 0;
304
+ // if the origin is in screen space we need to convert it to raycaster space
305
+ if (vec3.x > 1 || vec3.y > 1 || vec3.y < -1 || vec3.x < -1) {
306
+ this.context?.input.convertScreenspaceToRaycastSpace(vec3);
307
+ }
308
+ vec3.unproject(cam);
309
+ origin = vec3;
310
+ }
311
+
312
+ const o = origin as Vec3;
313
+
314
+ ray.origin.x = o.x;
315
+ ray.origin.y = o.y;
316
+ ray.origin.z = o.z;
317
+ const vec = this.raycastVectorsBuffer.get();
318
+ if (direction)
319
+ vec.set(direction.x, direction.y, direction.z);
320
+ else {
321
+ if (!cam) {
322
+ console.error("Can not perform raycast - no camera found");
323
+ return null;
324
+ }
325
+ vec.set(ray.origin.x, ray.origin.y, ray.origin.z);
326
+ const camPosition = getWorldPosition(cam);
327
+ vec.sub(camPosition);
328
+ }
329
+ // we need to normalize the ray because our input is a max travel length and the direction may be not normalized
330
+ vec.normalize();
331
+ ray.dir.x = vec.x;
332
+ ray.dir.y = vec.y;
333
+ ray.dir.z = vec.z;
334
+ // Gizmos.DrawRay(ray.origin, ray.dir, 0xff0000, Infinity);
335
+ return ray;
336
+ }
337
+
338
+
339
+ private rapierSphere: Ball | null = null;
340
+ private rapierColliderArray: Array<SphereOverlapResult> = [];
341
+ private readonly rapierIdentityRotation = { x: 0, y: 0, z: 0, w: 1 };
342
+ private readonly rapierForwardVector = { x: 0, y: 0, z: 1 };
343
+ /** Precice sphere overlap detection using rapier against colliders
344
+ * @param point center of the sphere in worldspace
345
+ * @param radius radius of the sphere
346
+ * @returns array of colliders that overlap with the sphere. Note: they currently only contain the collider and the gameobject
347
+ */
348
+ public sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult> {
349
+ this.rapierColliderArray.length = 0;
350
+ if (!this.world) return this.rapierColliderArray;
351
+ if (!this.rapierSphere)
352
+ this.rapierSphere = new Ball(radius);
353
+ this.rapierSphere.radius = radius;
354
+
355
+ this.world.intersectionsWithShape(point, this.rapierIdentityRotation, this.rapierSphere, col => {
356
+ const collider = col[$componentKey] as ICollider
357
+ // if (collider.gameObject.layers.isEnabled(2)) return true;
358
+ const intersection = new SphereOverlapResult(collider.gameObject, collider);
359
+ this.rapierColliderArray.push(intersection);
360
+ return true; // Return `false` instead if we want to stop searching for other colliders that contain this point.
361
+ }, QueryFilterFlags.EXCLUDE_SENSORS, undefined, undefined, undefined,
362
+ col => {
363
+ const collider = col[$componentKey] as ICollider
364
+ return collider.gameObject.layers.isEnabled(2) == false
365
+ }
366
+ );
367
+ return this.rapierColliderArray;
368
+
369
+
370
+ // TODO: this only returns one hit
371
+ // let filterGroups = 0xffffffff;
372
+ // filterGroups &= ~(1 << 2);
373
+ // const hit: ShapeColliderTOI | null = this.world.castShape(point,
374
+ // this.rapierIdentityRotation,
375
+ // this.rapierForwardVector,
376
+ // this.rapierSphere,
377
+ // 0,
378
+ // QueryFilterFlags.EXCLUDE_SENSORS,
379
+ // // filterGroups,
380
+ // );
381
+ // // console.log(hit);
382
+ // if (hit) {
383
+ // const collider = hit.collider[$componentKey] as ICollider
384
+ // const intersection = new SphereOverlapResult(collider.gameObject);
385
+ // this.rapierColliderArray.push(intersection);
386
+ // // const localpt = hit.witness2;
387
+ // // // const normal = hit.normal2;
388
+ // // const hitPoint = new Vector3(localpt.x, localpt.y, localpt.z);
389
+ // // // collider.gameObject.localToWorld(hitPoint);
390
+ // // // const normalPt = new Vector3(normal.x, normal.y, normal.z);
391
+ // // // const mat = new Matrix4().setPosition(point).scale(new Vector3(radius, radius, radius));
392
+ // // // hitPoint.applyMatrix4(mat);
393
+ // // console.log(hit.witness2)
394
+ // // // hitPoint.add(point);
395
+ // // const dist = hitPoint.distanceTo(point);
396
+ // }
397
+
398
+ // return this.rapierColliderArray;
399
+ }
400
+
401
+
402
+
403
+
404
+ // physics simulation
405
+
406
+ enabled: boolean = false;
407
+
408
+ private _tempPosition: Vector3 = new Vector3();
409
+ private _tempQuaternion: Quaternion = new Quaternion();
410
+ private _tempScale: Vector3 = new Vector3();
411
+ private _tempMatrix: Matrix4 = new Matrix4();
412
+
413
+ private static _didLoadPhysicsEngine: boolean = false;
414
+
415
+ private _isUpdatingPhysicsWorld: boolean = false;
416
+ get isUpdating(): boolean { return this._isUpdatingPhysicsWorld; }
417
+
418
+
419
+ private world?: World;
420
+ private _hasCreatedWorld: boolean = false;
421
+ private eventQueue?: EventQueue;
422
+ private collisionHandler?: PhysicsCollisionHandler;
423
+
424
+
425
+ private objects: IComponent[] = [];
426
+ private bodies: PhysicsBody[] = [];
427
+
428
+ private _meshCache: Map<string, Float32Array> = new Map<string, Float32Array>();
429
+
430
+ private _gravity = { x: 0.0, y: -9.81, z: 0.0 };
431
+
432
+ get gravity() {
433
+ return this.world?.gravity ?? this._gravity;
434
+ }
435
+
436
+ set gravity(value: Vec3) {
437
+ if (this.world) {
438
+ this.world.gravity = value;
439
+ }
440
+ else {
441
+ this._gravity = value;
442
+ }
443
+ }
444
+
445
+ clearCaches() {
446
+ this._meshCache.clear();
447
+ }
448
+
449
+ async addBoxCollider(collider: ICollider, center: Vector3, size: Vector3) {
450
+ if (!this._isInitialized)
451
+ await this.initialize(collider.context);
452
+ if (!collider.activeAndEnabled) return;
453
+
454
+ if (!this.enabled) {
455
+ if (debugPhysics) console.warn("Physics are disabled");
456
+ return;
457
+ }
458
+ const obj = collider.gameObject;
459
+ const scale = getWorldScale(obj, this._tempPosition).multiply(size);
460
+ scale.multiplyScalar(0.5);
461
+
462
+ // prevent negative scale
463
+ if (scale.x < 0)
464
+ scale.x = Math.abs(scale.x);
465
+ if (scale.y < 0)
466
+ scale.y = Math.abs(scale.y);
467
+ if (scale.z < 0)
468
+ scale.z = Math.abs(scale.z);
469
+
470
+ // prevent zero scale - seems normals are flipped otherwise
471
+ if (scale.x == 0) scale.x = 0.0000001;
472
+ if (scale.y == 0) scale.y = 0.0000001;
473
+ if (scale.z == 0) scale.z = 0.0000001;
474
+
475
+ const desc = ColliderDesc.cuboid(scale.x, scale.y, scale.z);
476
+ // const objectLayerMask = collider.gameObject.layers.mask;
477
+ // const mask = objectLayerMask & ~2;
478
+ // TODO: https://rapier.rs/docs/user_guides/javascript/colliders/#collision-groups-and-solver-groups
479
+ // desc.setCollisionGroups(objectLayerMask);
480
+ this.createCollider(collider, desc, center);
481
+ }
482
+
483
+ async addSphereCollider(collider: ICollider, center: Vector3, radius: number) {
484
+ if (!this._isInitialized)
485
+ await this.initialize(collider.context);
486
+ if (!collider.activeAndEnabled) return;
487
+ if (!this.enabled) {
488
+ if (debugPhysics) console.warn("Physics are disabled");
489
+ return;
490
+ }
491
+ const obj = collider.gameObject;
492
+ const scale = getWorldScale(obj, this._tempPosition).multiplyScalar(radius);
493
+ // Prevent negative scales
494
+ scale.x = Math.abs(scale.x);
495
+ const desc = ColliderDesc.ball(scale.x);
496
+ this.createCollider(collider, desc, center);
497
+ }
498
+
499
+ async addCapsuleCollider(collider: ICollider, center: Vector3, height: number, radius: number) {
500
+ if (!this._isInitialized)
501
+ await this.initialize(collider.context);
502
+ if (!collider.activeAndEnabled) return;
503
+ if (!this.enabled) {
504
+ if (debugPhysics) console.warn("Physics are disabled");
505
+ return;
506
+ }
507
+ const obj = collider.gameObject;
508
+ const scale = getWorldScale(obj, this._tempPosition);
509
+ // Prevent negative scales
510
+ scale.x = Math.abs(scale.x);
511
+ scale.y = Math.abs(scale.y);
512
+ const desc = ColliderDesc.capsule(height * .5 * scale.y - radius, radius * scale.x);
513
+ this.createCollider(collider, desc, center);
514
+ }
515
+
516
+ async addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale: Vector3) {
517
+ if (!this._isInitialized)
518
+ await this.initialize(collider.context);
519
+ if (!collider.activeAndEnabled) return;
520
+ if (!this.enabled) {
521
+ if (debugPhysics) console.warn("Physics are disabled");
522
+ return;
523
+ }
524
+ const geo = mesh.geometry;
525
+ if (!geo) {
526
+ if (debugPhysics) console.warn("Missing mesh geometry", mesh.name);
527
+ return;
528
+ }
529
+
530
+ let positions = geo.getAttribute("position").array as Float32Array;
531
+ const indices = geo.index?.array as Uint32Array;
532
+
533
+ // console.log(geo.center())
534
+
535
+ // scaling seems not supported yet https://github.com/dimforge/rapier/issues/243
536
+ if (Math.abs(scale.x - 1) > 0.0001 || Math.abs(scale.y - 1) > 0.0001 || Math.abs(scale.z - 1) > 0.0001) {
537
+ const key = geo.uuid + "_" + scale.x + "_" + scale.y + "_" + scale.z + "_" + convex;
538
+ if (this._meshCache.has(key)) {
539
+ positions = this._meshCache.get(key)!;
540
+ }
541
+ else {
542
+ console.warn("Your model is using scaled mesh colliders which is not optimal for performance", mesh.name, Object.assign({}, scale), mesh);
543
+ // showBalloonWarning("Your model is using scaled mesh colliders which is not optimal for performance: " + mesh.name + ", consider using unscaled objects");
544
+ const scaledPositions = new Float32Array(positions.length);
545
+ for (let i = 0; i < positions.length; i += 3) {
546
+ scaledPositions[i] = positions[i] * scale.x;
547
+ scaledPositions[i + 1] = positions[i + 1] * scale.y;
548
+ scaledPositions[i + 2] = positions[i + 2] * scale.z;
549
+ }
550
+ positions = scaledPositions;
551
+ this._meshCache.set(key, scaledPositions);
552
+ }
553
+ }
554
+ const desc = convex ? ColliderDesc.convexMesh(positions) : ColliderDesc.trimesh(positions, indices);
555
+ if (desc) {
556
+ const col = this.createCollider(collider, desc);
557
+ col.setMassProperties(1, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 });
558
+ // rb?.setTranslation({ x: 0, y: 2, z: 0 });
559
+ // col.setTranslationWrtParent(new Vector3(0,2,0));
560
+
561
+ }
562
+ }
563
+
564
+ private createCollider(collider: ICollider, desc: ColliderDesc, center?: Vector3) {
565
+ if (!this.world) throw new Error("Physics world not initialized");
566
+ const matrix = this._tempMatrix;
567
+ const {
568
+ rigidBody,
569
+ useExplicitMassProperties
570
+ } = this.getRigidbody(collider, this._tempMatrix);
571
+
572
+ matrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
573
+ getWorldScale(collider.gameObject, this._tempScale);
574
+ if (center) {
575
+ center.multiply(this._tempScale);
576
+ this._tempPosition.x -= center.x;
577
+ this._tempPosition.y += center.y;
578
+ this._tempPosition.z += center.z;
579
+ }
580
+ desc.setTranslation(this._tempPosition.x, this._tempPosition.y, this._tempPosition.z);
581
+ desc.setRotation(this._tempQuaternion);
582
+ desc.setSensor(collider.isTrigger);
583
+
584
+ // TODO: we might want to update this if the material changes
585
+ const physicsMaterial = collider.sharedMaterial;
586
+ if (physicsMaterial) {
587
+ desc.setRestitution(physicsMaterial.bounciness);
588
+ switch (physicsMaterial.bounceCombine) {
589
+ case PhysicsMaterialCombine.Average:
590
+ desc.setRestitutionCombineRule(CoefficientCombineRule.Average);
591
+ break;
592
+ case PhysicsMaterialCombine.Maximum:
593
+ desc.setRestitutionCombineRule(CoefficientCombineRule.Max);
594
+ break;
595
+ case PhysicsMaterialCombine.Minimum:
596
+ desc.setRestitutionCombineRule(CoefficientCombineRule.Min);
597
+ break;
598
+ case PhysicsMaterialCombine.Multiply:
599
+ desc.setRestitutionCombineRule(CoefficientCombineRule.Multiply);
600
+ break;
601
+ }
602
+ desc.setFriction(physicsMaterial.dynamicFriction);
603
+ switch (physicsMaterial.frictionCombine) {
604
+ case PhysicsMaterialCombine.Average:
605
+ desc.setFrictionCombineRule(CoefficientCombineRule.Average);
606
+ break;
607
+ case PhysicsMaterialCombine.Maximum:
608
+ desc.setFrictionCombineRule(CoefficientCombineRule.Max);
609
+ break;
610
+ case PhysicsMaterialCombine.Minimum:
611
+ desc.setFrictionCombineRule(CoefficientCombineRule.Min);
612
+ break;
613
+ case PhysicsMaterialCombine.Multiply:
614
+ desc.setFrictionCombineRule(CoefficientCombineRule.Multiply);
615
+ break;
616
+ }
617
+ }
618
+
619
+ // if we want to use explicit mass properties, we need to set the collider density to 0
620
+ // otherwise rapier will compute the mass properties based on the collider shape and density
621
+ // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties
622
+ if (useExplicitMassProperties) {
623
+ // desc.setDensity(0);
624
+ }
625
+
626
+ const col = this.world.createCollider(desc, rigidBody);
627
+ col[$componentKey] = collider;
628
+ collider[$bodyKey] = col;
629
+ col.setActiveEvents(ActiveEvents.COLLISION_EVENTS);
630
+ // We want to receive collisitons between two triggers too
631
+ col.setActiveCollisionTypes(ActiveCollisionTypes.ALL);
632
+
633
+ // const objectLayerMask = collider.gameObject.layers.mask;
634
+ // const mask = objectLayerMask & ~2;
635
+ // col.setCollisionGroups(objectLayerMask);
636
+ this.objects.push(collider);
637
+ this.bodies.push(col);
638
+ return col;
639
+ }
640
+
641
+ private getRigidbody(collider: ICollider, _matrix: Matrix4): { rigidBody: RigidBody, useExplicitMassProperties: boolean } {
642
+
643
+ if (!this.world) throw new Error("Physics world not initialized");
644
+ let rigidBody: RigidBody | null = null;
645
+ let useExplicitMassProperties = false;
646
+
647
+ if (collider.attachedRigidbody) {
648
+
649
+ const rb = collider.attachedRigidbody;
650
+ rigidBody = rb[$bodyKey];
651
+ useExplicitMassProperties = true;
652
+ if (!rigidBody) {
653
+ const kinematic = rb.isKinematic && !debugColliderPlacement;
654
+ if (debugPhysics)
655
+ console.log("Create rigidbody", kinematic);
656
+ const rigidBodyDesc = kinematic ? RAPIER.RigidBodyDesc.kinematicPositionBased() : RAPIER.RigidBodyDesc.dynamic();
657
+ const pos = getWorldPosition(collider.attachedRigidbody.gameObject);
658
+ rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z);
659
+ rigidBodyDesc.setRotation(getWorldQuaternion(collider.attachedRigidbody.gameObject));
660
+ rigidBody = this.world.createRigidBody(rigidBodyDesc);
661
+ this.bodies.push(rigidBody);
662
+ this.objects.push(rb);
663
+ }
664
+ rigidBody[$componentKey] = rb;
665
+ rb[$bodyKey] = rigidBody;
666
+ this.internalUpdateProperties(rb, rigidBody);
667
+ this.getRigidbodyRelativeMatrix(collider.gameObject, rb.gameObject, _matrix);
668
+
669
+ }
670
+ else {
671
+
672
+ const rigidBodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased();
673
+ const pos = getWorldPosition(collider.gameObject);
674
+ rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z);
675
+ rigidBodyDesc.setRotation(getWorldQuaternion(collider.gameObject));
676
+ rigidBody = this.world.createRigidBody(rigidBodyDesc);
677
+ _matrix.identity();
678
+ rigidBody[$componentKey] = null;
679
+
680
+ }
681
+
682
+ collider[$colliderRigidbody] = rigidBody;
683
+
684
+ return { rigidBody: rigidBody, useExplicitMassProperties: useExplicitMassProperties };
685
+ }
686
+
687
+ private internal_getRigidbody(rb: IRigidbody): RigidBody | null {
688
+ return rb[$bodyKey] as RigidBody;
689
+ }
690
+
691
+ private internalUpdateProperties(rb: IRigidbody, rigidbody: RigidBody) {
692
+ // continuous collision detection
693
+ // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#continuous-collision-detection
694
+ rigidbody.enableCcd(rb.collisionDetectionMode !== CollisionDetectionMode.Discrete);
695
+ rigidbody.setLinearDamping(rb.drag);
696
+ rigidbody.setAngularDamping(rb.angularDrag);
697
+ rigidbody.setGravityScale(rb.useGravity ? rb.gravityScale : 0, true);
698
+
699
+ // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties
700
+ // rigidbody.setAdditionalMass(rb.mass, true);
701
+ // for (let i = 0; i < rigidbody.numColliders(); i++) {
702
+ // const collider = rigidbody.collider(i);
703
+ // if (collider) {
704
+ // collider.setMass(rb.mass);
705
+ // // const density = rb.mass / collider.shape.computeMassProperties().mass;
706
+ // }
707
+ // }
708
+
709
+ // lock rotations
710
+ rigidbody.setEnabledRotations(!rb.lockRotationX, !rb.lockRotationY, !rb.lockRotationZ, true);
711
+ rigidbody.setEnabledTranslations(!rb.lockPositionX, !rb.lockPositionY, !rb.lockPositionZ, true);
712
+
713
+ if (rb.isKinematic) {
714
+ rigidbody.setBodyType(RAPIER.RigidBodyType.KinematicPositionBased);
715
+ }
716
+ else {
717
+ rigidbody.setBodyType(RAPIER.RigidBodyType.Dynamic);
718
+ }
719
+ }
720
+
721
+ // private _lastStepTime: number | undefined = 0;
722
+ private lines?: LineSegments;
723
+
724
+ public step(dt?: number) {
725
+ if (!this.world) return;
726
+ if (!this.enabled) return;
727
+ this._isUpdatingPhysicsWorld = true;
728
+ if (!this.eventQueue) {
729
+ this.eventQueue = new EventQueue(false);
730
+ }
731
+ if (dt) {
732
+ // if we make to sudden changes to the timestep the physics can get unstable
733
+ // https://rapier.rs/docs/user_guides/javascript/integration_parameters/#dt
734
+ this.world.timestep = Mathf.lerp(this.world.timestep, dt, 0.8);
735
+ }
736
+ this.world.step(this.eventQueue);
737
+ this._isUpdatingPhysicsWorld = false;
738
+ this.updateDebugRendering(this.world);
739
+ }
740
+
741
+ private updateDebugRendering(world: World) {
742
+ if (debugPhysics || debugColliderPlacement || showColliders) {
743
+ if (!this.lines) {
744
+ const material = new LineBasicMaterial({
745
+ color: 0x227700,
746
+ // vertexColors: THREE.VertexColors
747
+ });
748
+ const geometry = new BufferGeometry();
749
+ this.lines = new LineSegments(geometry, material);
750
+ this.context?.scene.add(this.lines);
751
+ }
752
+ const buffers = world.debugRender();
753
+ this.lines.geometry.setAttribute('position', new BufferAttribute(buffers.vertices, 3));
754
+ this.lines.geometry.setAttribute('color', new BufferAttribute(buffers.colors, 4));
755
+ }
756
+ }
757
+
758
+ public postStep() {
759
+ if (!this.world) return;
760
+ if (!this.enabled) return;
761
+ this._isUpdatingPhysicsWorld = true;
762
+ this.syncObjects();
763
+ this._isUpdatingPhysicsWorld = false;
764
+
765
+ if (this.eventQueue && !this.collisionHandler) {
766
+ this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue);
767
+ }
768
+ if (this.collisionHandler) {
769
+ this.collisionHandler.handleCollisionEvents();
770
+ this.collisionHandler.update();
771
+ }
772
+ }
773
+
774
+ /** sync rendered objects with physics world (except for colliders without rigidbody) */
775
+ private syncObjects() {
776
+ if (debugColliderPlacement) return;
777
+ for (let i = 0; i < this.bodies.length; i++) {
778
+ const obj = this.objects[i];
779
+ const body = this.bodies[i] as Collider;
780
+
781
+ // if the collider is not attached to a rigidbody
782
+ // it means that its kinematic so we need to update its position
783
+ const col = (obj as ICollider);
784
+ if (col?.isCollider === true && !col.attachedRigidbody) {
785
+ const rigidbody = body.parent();
786
+ if (rigidbody)
787
+ this.syncPhysicsBody(obj.gameObject, rigidbody, true, true);
788
+ continue;
789
+ }
790
+
791
+
792
+ // sync
793
+ const pos = body.translation();
794
+ const rot = body.rotation();
795
+ // make sure to keep the collider offset
796
+ const center = obj["center"] as Vector3;
797
+ if (center && center.isVector3) {
798
+ this._tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
799
+ const offset = this._tempPosition.copy(center).applyQuaternion(this._tempQuaternion);
800
+ const scale = getWorldScale(obj.gameObject);
801
+ offset.multiply(scale);
802
+ pos.x -= offset.x;
803
+ pos.y -= offset.y;
804
+ pos.z -= offset.z;
805
+ }
806
+ setWorldPositionXYZ(obj.gameObject, pos.x, pos.y, pos.z);
807
+ setWorldQuaternionXYZW(obj.gameObject, rot.x, rot.y, rot.z, rot.w);
808
+ }
809
+ }
810
+
811
+ private syncPhysicsBody(obj: Object3D, body: RigidBody, translation: boolean, rotation: boolean) {
812
+
813
+ // const bodyType = body.bodyType();
814
+ // const previous = physicsBody.translation();
815
+ // const vel = physicsBody.linvel();
816
+
817
+ const worldPosition = getWorldPosition(obj, this._tempPosition);
818
+ const worldQuaternion = getWorldQuaternion(obj, this._tempQuaternion);
819
+ const type = body.bodyType();
820
+ switch (type) {
821
+ case RigidBodyType.Fixed:
822
+ case RigidBodyType.KinematicPositionBased:
823
+ case RigidBodyType.KinematicVelocityBased:
824
+ if (translation)
825
+ body.setNextKinematicTranslation(worldPosition);
826
+ if (rotation)
827
+ body.setNextKinematicRotation(worldQuaternion);
828
+ break;
829
+ default:
830
+ if (translation)
831
+ body.setTranslation(worldPosition, false);
832
+ if (rotation)
833
+ body.setRotation(worldQuaternion, false);
834
+ break;
835
+
836
+ }
837
+ body.wakeUp();
838
+ // physicsBody.setBodyType(RAPIER.RigidBodyType.Fixed);
839
+ // physicsBody.setLinvel(vel, false);
840
+
841
+ // update velocity
842
+ // const pos = physicsBody.translation();
843
+ // pos.x -= previous.x;
844
+ // pos.y -= previous.y;
845
+ // pos.z -= previous.z;
846
+ // // threhold
847
+ // const t = 1;
848
+ // const canUpdateVelocity = Math.abs(pos.x) < t && Math.abs(pos.y) < t && Math.abs(pos.z) < t;
849
+ // if (canUpdateVelocity) {
850
+ // const damping = 1 + this.context.time.deltaTime;
851
+ // vel.x *= damping;
852
+ // vel.y *= damping;
853
+ // vel.z *= damping;
854
+ // vel.x += pos.x;
855
+ // vel.y += pos.y;
856
+ // vel.z += pos.z;
857
+ // console.log(vel);
858
+ // physicsBody.setLinvel(vel, true);
859
+ // }
860
+ // else if(debugPhysics) console.warn("Movement exceeded threshold, not updating velocity", pos);
861
+
862
+ // body.setBodyType(bodyType);
863
+ }
864
+
865
+ private static _matricesBuffer: Matrix4[] = [];
866
+ private getRigidbodyRelativeMatrix(comp: Object3D, rigidbody: Object3D, mat: Matrix4, matrices?: Matrix4[]): Matrix4 {
867
+ // collect all matrices to the rigidbody and then build the rigidbody relative matrix
868
+ if (matrices === undefined) {
869
+ matrices = RapierPhysics._matricesBuffer;
870
+ matrices.length = 0;
871
+ }
872
+ if (comp === rigidbody) {
873
+ const scale = getWorldScale(comp, this._tempPosition);
874
+ mat.makeScale(scale.x, scale.y, scale.z);
875
+ for (let i = matrices.length - 1; i >= 0; i--) {
876
+ mat.multiply(matrices[i]);
877
+ }
878
+ return mat;
879
+ }
880
+ matrices.push(comp.matrix);
881
+ if (comp.parent) {
882
+ this.getRigidbodyRelativeMatrix(comp.parent, rigidbody, mat, matrices);
883
+ }
884
+ return mat;
885
+ }
886
+
887
+ private static centerConnectionPos = { x: 0, y: 0, z: 0 };
888
+ private static centerConnectionRot = { x: 0, y: 0, z: 0, w: 1 };
889
+
890
+
891
+
892
+ addFixedJoint(body1: IRigidbody, body2: IRigidbody) {
893
+ if (!this.world) {
894
+ console.error("Physics world not initialized");
895
+ return;
896
+ }
897
+ const b1 = body1[$bodyKey] as RigidBody;
898
+ const b2 = body2[$bodyKey] as RigidBody;
899
+
900
+ this.calculateJointRelativeMatrices(body1.gameObject, body2.gameObject, this._tempMatrix);
901
+ this._tempMatrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
902
+
903
+ const params = JointData.fixed(
904
+ RapierPhysics.centerConnectionPos, RapierPhysics.centerConnectionRot,
905
+ this._tempPosition, this._tempQuaternion,
906
+ );
907
+ const joint = this.world.createImpulseJoint(params, b1, b2, true);
908
+ if (debugPhysics)
909
+ console.log("ADD FIXED JOINT", joint)
910
+ }
911
+
912
+
913
+ /** The joint prevents any relative movement between two rigid-bodies, except for relative rotations along one axis. This is typically used to simulate wheels, fans, etc. They are characterized by one local anchor as well as one local axis on each rigid-body. */
914
+ addHingeJoint(body1: IRigidbody, body2: IRigidbody, anchor: { x: number, y: number, z: number }, axis: { x: number, y: number, z: number }) {
915
+ if (!this.world) {
916
+ console.error("Physics world not initialized");
917
+ return;
918
+ }
919
+ const b1 = body1[$bodyKey] as RigidBody;
920
+ const b2 = body2[$bodyKey] as RigidBody;
921
+
922
+ this.calculateJointRelativeMatrices(body1.gameObject, body2.gameObject, this._tempMatrix);
923
+ this._tempMatrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
924
+
925
+ let params = RAPIER.JointData.revolute(anchor, this._tempPosition, axis);
926
+ let joint = this.world.createImpulseJoint(params, b1, b2, true);
927
+ if (debugPhysics)
928
+ console.log("ADD HINGE JOINT", joint)
929
+ }
930
+
931
+
932
+ private calculateJointRelativeMatrices(body1: IGameObject, body2: IGameObject, mat: Matrix4) {
933
+ body1.updateWorldMatrix(true, false);
934
+ body2.updateWorldMatrix(true, false);
935
+ const world1 = body1.matrixWorld;
936
+ const world2 = body2.matrixWorld;
937
+ // set scale to 1
938
+ world1.elements[0] = 1;
939
+ world1.elements[5] = 1;
940
+ world1.elements[10] = 1;
941
+ world2.elements[0] = 1;
942
+ world2.elements[5] = 1;
943
+ world2.elements[10] = 1;
944
+ mat.copy(world2).premultiply(world1.invert()).invert();
945
+ }
946
+ }
947
+
948
+
949
+
950
+ /** responsible of processing collision events for the component system */
951
+ class PhysicsCollisionHandler {
952
+
953
+ readonly world: World;
954
+ readonly eventQueue: EventQueue;
955
+
956
+ constructor(world: World, eventQueue: EventQueue) {
957
+ this.world = world;
958
+ this.eventQueue = eventQueue;
959
+ }
960
+
961
+ private activeCollisions: Array<{ collider: ICollider, component: IComponent, collision: Collision }> = [];
962
+ private activeCollisionsStay: Array<{ collider: ICollider, component: IComponent, collision: Collision }> = [];
963
+ private activeTriggers: Array<{ collider: ICollider, component: IComponent, otherCollider: ICollider }> = [];
964
+
965
+ handleCollisionEvents() {
966
+ if (!this.eventQueue) return;
967
+ if (!this.world) return;
968
+ this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
969
+ const col1 = this.world!.getCollider(handle1);
970
+ const col2 = this.world!.getCollider(handle2);
971
+ const colliderComponent1 = col1[$componentKey];
972
+ const colliderComponent2 = col2[$componentKey];
973
+ if (debugCollisions)
974
+ console.log("EVT", colliderComponent1.name, colliderComponent2.name, started, col1, col2);
975
+ if (colliderComponent1 && colliderComponent2) {
976
+ if (started) {
977
+ this.onCollisionStarted(colliderComponent1, col1, colliderComponent2, col2);
978
+ this.onCollisionStarted(colliderComponent2, col2, colliderComponent1, col1);
979
+ }
980
+ else {
981
+ this.onCollisionEnded(colliderComponent1, colliderComponent2);
982
+ this.onCollisionEnded(colliderComponent2, colliderComponent1);
983
+ }
984
+ }
985
+ });
986
+ }
987
+
988
+ update() {
989
+ this.onHandleCollisionStay();
990
+ }
991
+
992
+ private onCollisionStarted(self: ICollider, selfBody: Collider, other: ICollider, otherBody: Collider) {
993
+ let collision: Collision | null = null;
994
+
995
+ // if one is a trigger we dont get collisions but want to raise the trigger events
996
+ if (self.isTrigger || other.isTrigger) {
997
+ foreachComponent(self.gameObject, (c: IComponent) => {
998
+ if (c.onTriggerEnter && !c.destroyed) {
999
+ c.onTriggerEnter(other);
1000
+ }
1001
+ this.activeTriggers.push({ collider: self, component: c, otherCollider: other });
1002
+ });
1003
+ }
1004
+ else {
1005
+ const object = self.gameObject;
1006
+ // TODO: we dont respect the flip value here!
1007
+ this.world.contactPair(selfBody, otherBody, (manifold, _flipped) => {
1008
+ foreachComponent(object, (c: IComponent) => {
1009
+ if (c.destroyed) return;
1010
+ const hasDeclaredEventMethod = c.onCollisionEnter || c.onCollisionStay || c.onCollisionExit;
1011
+ if (hasDeclaredEventMethod || debugCollisions) {
1012
+ if (!collision) {
1013
+ const contacts: Array<ContactPoint> = [];
1014
+ const normal = manifold.normal();
1015
+ for (let i = 0; i < manifold.numSolverContacts(); i++) {
1016
+ // solver points are in world space
1017
+ // https://rapier.rs/docs/user_guides/javascript/advanced_collision_detection_js#the-contact-graph
1018
+ const pt = manifold.solverContactPoint(i);
1019
+ const impulse = manifold.contactImpulse(i);
1020
+ if (pt) {
1021
+ const dist = manifold.contactDist(i);
1022
+ const friction = manifold.solverContactFriction(i);
1023
+ const contact = new ContactPoint(pt, dist, normal, impulse, friction);
1024
+ contacts.push(contact);
1025
+ if (debugCollisions) {
1026
+ Gizmos.DrawDirection(pt, normal, 0xff0000, 3, true);
1027
+ }
1028
+ }
1029
+ }
1030
+ collision = new Collision(object, other, contacts);
1031
+ }
1032
+
1033
+ // we only need to keep track if any event exists
1034
+ if (hasDeclaredEventMethod) {
1035
+ const info = { collider: self, component: c, collision };
1036
+
1037
+ this.activeCollisions.push(info);
1038
+ if (c.onCollisionStay) {
1039
+ this.activeCollisionsStay.push(info);
1040
+ }
1041
+
1042
+ c.onCollisionEnter?.call(c, collision);
1043
+ }
1044
+
1045
+ }
1046
+ });
1047
+ });
1048
+ }
1049
+ }
1050
+
1051
+ private onHandleCollisionStay() {
1052
+ for (const active of this.activeCollisionsStay) {
1053
+ const c = active.component;
1054
+ if (c.destroyed) continue;
1055
+ if (c.activeAndEnabled && c.onCollisionStay) {
1056
+ const arg = active.collision;
1057
+ c.onCollisionStay(arg);
1058
+ }
1059
+ }
1060
+ for (const active of this.activeTriggers) {
1061
+ const c = active.component;
1062
+ if (c.destroyed) continue;
1063
+ if (c.activeAndEnabled && c.onTriggerStay) {
1064
+ const arg = active.otherCollider;
1065
+ c.onTriggerStay(arg);
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ private onCollisionEnded(self: ICollider, other: ICollider) {
1071
+ if (self.destroyed || other.destroyed) return;
1072
+ for (let i = 0; i < this.activeCollisions.length; i++) {
1073
+ const active = this.activeCollisions[i];
1074
+ const collider = active.collider;
1075
+ if (collider.destroyed) {
1076
+ this.activeCollisions.splice(i, 1);
1077
+ i--;
1078
+ continue;
1079
+ }
1080
+ if (collider === self && active.collision.collider === other) {
1081
+ const c = active.component;
1082
+ this.activeCollisions.splice(i, 1);
1083
+ i--;
1084
+ if (c.activeAndEnabled && c.onCollisionExit) {
1085
+ const collision = active.collision;
1086
+ c.onCollisionExit(collision);
1087
+ }
1088
+ }
1089
+ }
1090
+ for (let i = 0; i < this.activeCollisionsStay.length; i++) {
1091
+ const active = this.activeCollisionsStay[i];
1092
+ const collider = active.collider;
1093
+ if (collider.destroyed) {
1094
+ this.activeCollisionsStay.splice(i, 1);
1095
+ i--;
1096
+ continue;
1097
+ }
1098
+ if (collider === self && active.collision.collider === other) {
1099
+ const c = active.component;
1100
+ this.activeCollisionsStay.splice(i, 1);
1101
+ i--;
1102
+ if (c.activeAndEnabled && c.onCollisionExit) {
1103
+ const collision = active.collision;
1104
+ c.onCollisionExit(collision);
1105
+ }
1106
+ }
1107
+ }
1108
+ for (let i = 0; i < this.activeTriggers.length; i++) {
1109
+ const active = this.activeTriggers[i];
1110
+ const collider = active.collider;
1111
+ if (collider.destroyed) {
1112
+ this.activeTriggers.splice(i, 1);
1113
+ i--;
1114
+ continue;
1115
+ }
1116
+ if (collider === self && active.otherCollider === other) {
1117
+ const c = active.component;
1118
+ this.activeTriggers.splice(i, 1);
1119
+ i--;
1120
+ if (c.activeAndEnabled && c.onTriggerExit) {
1121
+ const collision = active.otherCollider;
1122
+ c.onTriggerExit(collision);
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ }