Needle Engine

Changes between version 3.20.4 and 3.21.0-alpha
Files changed (9) hide show
  1. src/engine-components/Collider.ts +27 -4
  2. src/engine/engine_context.ts +39 -17
  3. src/engine/engine_physics_rapier.ts +66 -21
  4. src/engine/engine_types.ts +11 -3
  5. src/engine/engine_util_decorator.ts +21 -1
  6. src/engine/engine_utils.ts +41 -0
  7. src/engine/extensions/NEEDLE_lightmaps.ts +3 -1
  8. src/engine-components/RendererLightmap.ts +5 -8
  9. src/engine-components/webxr/WebARSessionRoot.ts +262 -28
src/engine-components/Collider.ts CHANGED
@@ -3,9 +3,11 @@
3
3
  import { serializable } from "../engine/engine_serialization_decorator.js";
4
4
  import { Event, Group, Mesh, Object3D, Vector3 } from "three"
5
5
  // import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
6
- import { ICollider } from "../engine/engine_types.js";
6
+ import { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
7
7
  import { getWorldScale } from "../engine/engine_three_utils.js";
8
8
  import { PhysicsMaterial } from "../engine/engine_physics.types.js";
9
+ import { validate } from "../engine/engine_util_decorator.js";
10
+ import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
9
11
 
10
12
 
11
13
  export class Collider extends Behaviour implements ICollider {
@@ -48,26 +50,43 @@
48
50
  return this.context.physics.engine?.getBody(this);
49
51
  }
50
52
 
53
+ updateProperties = () => {
54
+ this.context.physics.engine?.updateProperties(this);
55
+ }
51
56
  }
52
57
 
53
58
 
54
- export class SphereCollider extends Collider {
59
+ export class SphereCollider extends Collider implements ISphereCollider {
55
60
 
61
+ @validate()
56
62
  @serializable()
57
63
  radius: number = .5;
64
+
58
65
  @serializable(Vector3)
59
66
  center: Vector3 = new Vector3(0, 0, 0);
60
67
 
61
68
  onEnable() {
62
69
  super.onEnable();
63
- this.context.physics.engine?.addSphereCollider(this, this.center, this.radius);
70
+ this.context.physics.engine?.addSphereCollider(this, this.center);
71
+ watchWrite(this.gameObject.scale, this.updateProperties);
64
72
  }
73
+
74
+ onDisable(): void {
75
+ super.onDisable();
76
+ unwatchWrite(this.gameObject.scale, this.updateProperties);
77
+ }
78
+
79
+ onValidate(): void {
80
+ this.updateProperties();
81
+ }
65
82
  }
66
83
 
67
- export class BoxCollider extends Collider {
84
+ export class BoxCollider extends Collider implements IBoxCollider {
68
85
 
86
+ @validate()
69
87
  @serializable(Vector3)
70
88
  size: Vector3 = new Vector3(1, 1, 1);
89
+
71
90
  @serializable(Vector3)
72
91
  center: Vector3 = new Vector3(0, 0, 0);
73
92
 
@@ -75,6 +94,10 @@
75
94
  super.onEnable();
76
95
  this.context.physics.engine?.addBoxCollider(this, this.center, this.size);
77
96
  }
97
+
98
+ onValidate(): void {
99
+ this.updateProperties();
100
+ }
78
101
  }
79
102
 
80
103
 
src/engine/engine_context.ts CHANGED
@@ -156,7 +156,10 @@
156
156
 
157
157
  name: string;
158
158
  alias: string | undefined | null;
159
- /** When the renderer or camera are managed by an external process (e.g. when running in r3f context) */
159
+ /** When the renderer or camera are managed by an external process (e.g. when running in r3f context).
160
+ * When this is false you are responsible to call update(timestamp, xframe.
161
+ * It is also currently assumed that rendering is handled performed by an external process
162
+ * */
160
163
  isManagedExternally: boolean = false;
161
164
  /** set to true to pause the update loop. You can receive an event for it in your components.
162
165
  * Note that script updates will not be called when paused */
@@ -903,15 +906,19 @@
903
906
  console.warn("Can not start render loop while creating context");
904
907
  return false;
905
908
  }
906
- this.renderer.setAnimationLoop((timestamp, frame: XRFrame | null) => this.update(timestamp, frame));
909
+ this.renderer.setAnimationLoop((timestamp, frame: XRFrame | null) => {
910
+ if (this.isManagedExternally) return;
911
+ this.update(timestamp, frame)
912
+ });
907
913
  return true;
908
914
  }
909
915
 
916
+ /** Performs a full update step including script callbacks, rendering (unless isManagedExternally is set to false) and post render callbacks */
910
917
  public update(timestamp: DOMHighResTimeStamp, frame?: XRFrame | null) {
911
918
  if (frame === undefined) frame = null;
912
919
  if (isDevEnvironment() || debug || looputils.hasNewScripts()) {
913
920
  try {
914
- this.internalRender(timestamp, frame);
921
+ this.internalStep(timestamp, frame);
915
922
  }
916
923
  catch (err) {
917
924
  if ((isDevEnvironment() || debug) && err instanceof Error)
@@ -923,7 +930,7 @@
923
930
  }
924
931
  }
925
932
  else {
926
- this.internalRender(timestamp, frame);
933
+ this.internalStep(timestamp, frame);
927
934
  }
928
935
  }
929
936
 
@@ -931,7 +938,15 @@
931
938
  private _accumulatedTime = 0;
932
939
  private _dispatchReadyAfterFrame = false;
933
940
 
934
- private internalRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
941
+ // TODO: we need to skip after render callbacks if the render loop is managed externally. When changing this we also need to to update the r3f sample
942
+ private internalStep(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
943
+ if (this.internalOnBeforeRender(timestamp, frame) === false) return;
944
+ this.internalOnRender();
945
+ this.internalOnAfterRender();
946
+ }
947
+
948
+ private internalOnBeforeRender(timestamp: DOMHighResTimeStamp, frame: XRFrame | null) {
949
+
935
950
  this._xrFrame = frame;
936
951
 
937
952
  this._currentFrameEvent = FrameEvent.Undefined;
@@ -944,7 +959,7 @@
944
959
  if (typeof targetFrameRate === "object") targetFrameRate = targetFrameRate.value!;
945
960
  // if(debug) console.log(this._accumulatedTime, (1 / (targetFrameRate)))
946
961
  if (this._accumulatedTime < (1 / (targetFrameRate + 1))) {
947
- return;
962
+ return false;
948
963
  }
949
964
  this._accumulatedTime = 0;
950
965
  }
@@ -952,7 +967,7 @@
952
967
  this._stats?.begin();
953
968
 
954
969
  Context.Current = this;
955
- if (this.onHandlePaused()) return;
970
+ if (this.onHandlePaused()) return false;
956
971
 
957
972
  Context.Current = this;
958
973
  this.time.update();
@@ -987,7 +1002,7 @@
987
1002
  }
988
1003
  }
989
1004
  this.executeCoroutines(FrameEvent.EarlyUpdate);
990
- if (this.onHandlePaused()) return;
1005
+ if (this.onHandlePaused()) return false;
991
1006
 
992
1007
  this._currentFrameEvent = FrameEvent.Update;
993
1008
 
@@ -1000,7 +1015,7 @@
1000
1015
  }
1001
1016
  }
1002
1017
  this.executeCoroutines(FrameEvent.Update);
1003
- if (this.onHandlePaused()) return;
1018
+ if (this.onHandlePaused()) return false;
1004
1019
 
1005
1020
  this._currentFrameEvent = FrameEvent.LateUpdate;
1006
1021
 
@@ -1015,7 +1030,7 @@
1015
1030
 
1016
1031
  // this.mainLight = null;
1017
1032
  this.executeCoroutines(FrameEvent.LateUpdate);
1018
- if (this.onHandlePaused()) return;
1033
+ if (this.onHandlePaused()) return false;
1019
1034
 
1020
1035
  if (this.physics.engine) {
1021
1036
  const physicsSteps = 1;
@@ -1030,7 +1045,7 @@
1030
1045
  this.physics.engine.postStep();
1031
1046
  }
1032
1047
 
1033
- if (this.onHandlePaused()) return;
1048
+ if (this.onHandlePaused()) return false;
1034
1049
 
1035
1050
  if (this.isVisibleToUser || this.runInBackground) {
1036
1051
 
@@ -1058,15 +1073,22 @@
1058
1073
  }
1059
1074
  }
1060
1075
 
1076
+ }
1061
1077
 
1062
- if (!this.isManagedExternally) {
1063
- looputils.runPrewarm(this);
1064
- this._currentFrameEvent = FrameEvent.Undefined;
1065
- this.renderNow();
1066
- this._currentFrameEvent = FrameEvent.OnAfterRender;
1067
- }
1078
+ return true;
1079
+ }
1068
1080
 
1081
+ private internalOnRender() {
1082
+ if (!this.isManagedExternally) {
1083
+ looputils.runPrewarm(this);
1084
+ this._currentFrameEvent = FrameEvent.Undefined;
1085
+ this.renderNow();
1086
+ this._currentFrameEvent = FrameEvent.OnAfterRender;
1087
+ }
1088
+ }
1069
1089
 
1090
+ private internalOnAfterRender() {
1091
+ if (this.isVisibleToUser || this.runInBackground) {
1070
1092
  for (let i = 0; i < this.scripts_onAfterRender.length; i++) {
1071
1093
  const script = this.scripts_onAfterRender[i];
1072
1094
  if (!script.activeAndEnabled) continue;
src/engine/engine_physics_rapier.ts CHANGED
@@ -13,10 +13,12 @@
13
13
  IGameObject,
14
14
  Vec2,
15
15
  IContext,
16
+ ISphereCollider,
17
+ IBoxCollider,
16
18
  } from './engine_types.js';
17
19
  import { foreachComponent } from './engine_gameobject.js';
18
20
 
19
- import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray } from '@dimforge/rapier3d-compat';
21
+ import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray, ShapeType, Cuboid } from '@dimforge/rapier3d-compat';
20
22
  import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
21
23
  import { Gizmos } from './engine_gizmos.js';
22
24
  import { Mathf } from './engine_math.js';
@@ -40,8 +42,8 @@
40
42
  let RAPIER: undefined | any = undefined;
41
43
  declare const NEEDLE_USE_RAPIER: boolean;
42
44
  globalThis["NEEDLE_USE_RAPIER"] = globalThis["NEEDLE_USE_RAPIER"] !== undefined ? globalThis["NEEDLE_USE_RAPIER"] : true;
43
- if(debugPhysics)
44
- console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"] )
45
+ if (debugPhysics)
46
+ console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"])
45
47
 
46
48
  if (NEEDLE_USE_RAPIER) {
47
49
  ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => {
@@ -79,6 +81,7 @@
79
81
  this.bodies.splice(index, 1);
80
82
  this.objects.splice(index, 1);
81
83
 
84
+
82
85
  // Remove the collider from the physics world
83
86
  if (rapierBody instanceof Collider) {
84
87
  const rapierCollider = rapierBody as Collider;
@@ -88,7 +91,13 @@
88
91
  const rapierRigidbody: RigidBody | null = rapierCollider.parent();
89
92
  if (rapierRigidbody && rapierRigidbody.numColliders() <= 0) {
90
93
  const rigidbody = rapierRigidbody[$componentKey] as IRigidbody;
91
- this.removeBody(rigidbody);
94
+ if (rigidbody) {
95
+ // If the collider was attached to a rigidbody and this rigidbody now has no colliders anymore we should ignore it - because the Rigidbody component will delete itself
96
+ }
97
+ else {
98
+ // But if there is no explicit rigidbody needle component then the colliders did create it implictly and thus we need to remove it here:
99
+ this.world?.removeRigidBody(rapierRigidbody);
100
+ }
92
101
  }
93
102
  }
94
103
  // Remove the rigidbody from the physics world
@@ -133,12 +142,23 @@
133
142
  }
134
143
  }
135
144
 
136
- updateProperties(rigidbody: IRigidbody) {
145
+ updateProperties(obj: IRigidbody | ICollider) {
137
146
  this.validate();
138
- const physicsBody = this.internal_getRigidbody(rigidbody);
139
- if (physicsBody) {
140
- this.internalUpdateProperties(rigidbody, physicsBody);
147
+
148
+ if ((obj as ICollider).isCollider) {
149
+ const col = obj as ICollider;
150
+ const body = col[$bodyKey];
151
+ if (body) {
152
+ this.internalUpdateColliderProperties(col, body);
153
+ }
141
154
  }
155
+ else {
156
+ const rb = obj as IRigidbody;
157
+ const physicsBody = this.internal_getRigidbody(rb);
158
+ if (physicsBody) {
159
+ this.internalUpdateRigidbodyProperties(rb, physicsBody);
160
+ }
161
+ }
142
162
  }
143
163
  addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
144
164
  this.validate();
@@ -212,7 +232,7 @@
212
232
  }
213
233
 
214
234
  private async internalInitialization() {
215
- if(debugPhysics) console.log("Initialize rapier physics engine");
235
+ if (debugPhysics) console.log("Initialize rapier physics engine");
216
236
  // NEEDLE_PHYSICS_INIT_START
217
237
  // use .env file with VITE_NEEDLE_USE_RAPIER=false to treeshake rapier
218
238
  // @ts-ignore
@@ -229,7 +249,7 @@
229
249
  this._hasCreatedWorld = true;
230
250
  if (RAPIER === undefined) {
231
251
  if (debugPhysics) console.log("Import Rapier");
232
- const _rapier = await import("@dimforge/rapier3d-compat");
252
+ const _rapier = await import("@dimforge/rapier3d-compat");
233
253
  if (debugPhysics) console.log("Init Rapier");
234
254
  await _rapier.init();
235
255
  // only assign after all loads are done to avoid a race condition
@@ -512,7 +532,7 @@
512
532
  this.createCollider(collider, desc, center);
513
533
  }
514
534
 
515
- async addSphereCollider(collider: ICollider, center: Vector3, radius: number) {
535
+ async addSphereCollider(collider: ICollider, center: Vector3) {
516
536
  if (!this._isInitialized)
517
537
  await this.initialize(collider.context);
518
538
  if (!collider.activeAndEnabled) return;
@@ -520,12 +540,9 @@
520
540
  if (debugPhysics) console.warn("Physics are disabled");
521
541
  return;
522
542
  }
523
- const obj = collider.gameObject;
524
- const scale = getWorldScale(obj, this._tempPosition).multiplyScalar(radius);
525
- // Prevent negative scales
526
- scale.x = Math.abs(scale.x);
527
- const desc = ColliderDesc.ball(scale.x);
543
+ const desc = ColliderDesc.ball(.5);
528
544
  this.createCollider(collider, desc, center);
545
+ this.updateProperties(collider);
529
546
  }
530
547
 
531
548
  async addCapsuleCollider(collider: ICollider, center: Vector3, height: number, radius: number) {
@@ -572,7 +589,7 @@
572
589
  if (Math.abs(scale.x - 1) > 0.0001 || Math.abs(scale.y - 1) > 0.0001 || Math.abs(scale.z - 1) > 0.0001) {
573
590
  const key = geo.uuid + "_" + scale.x + "_" + scale.y + "_" + scale.z + "_" + convex;
574
591
  if (this._meshCache.has(key)) {
575
- if(debugPhysics) console.warn("Use cached mesh collider")
592
+ if (debugPhysics) console.warn("Use cached mesh collider")
576
593
  positions = this._meshCache.get(key)!;
577
594
  }
578
595
  else {
@@ -606,8 +623,8 @@
606
623
  }
607
624
 
608
625
  /** Get the Needle Engine component for a rapier object */
609
- getComponent(rapierObject:object) : IComponent | null {
610
- if(!rapierObject) return null;
626
+ getComponent(rapierObject: object): IComponent | null {
627
+ if (!rapierObject) return null;
611
628
  const component = rapierObject[$componentKey];
612
629
  return component;
613
630
  }
@@ -714,7 +731,7 @@
714
731
  }
715
732
  rigidBody[$componentKey] = rb;
716
733
  rb[$bodyKey] = rigidBody;
717
- this.internalUpdateProperties(rb, rigidBody);
734
+ this.internalUpdateRigidbodyProperties(rb, rigidBody);
718
735
  this.getRigidbodyRelativeMatrix(collider.gameObject, rb.gameObject, _matrix);
719
736
 
720
737
  }
@@ -739,7 +756,35 @@
739
756
  return rb[$bodyKey] as RigidBody;
740
757
  }
741
758
 
742
- private internalUpdateProperties(rb: IRigidbody, rigidbody: RigidBody) {
759
+ private internalUpdateColliderProperties(col: ICollider, collider: Collider) {
760
+ const shape = collider.shape;
761
+ switch (shape.type) {
762
+ // Sphere Collider
763
+ case ShapeType.Ball:
764
+ {
765
+ const ball = shape as Ball;
766
+ const sc = col as ISphereCollider;
767
+ const changed = ball.radius !== sc.radius;
768
+ const obj = col.gameObject;
769
+ const scale = getWorldScale(obj, this._tempPosition).multiplyScalar(sc.radius);
770
+ // Prevent negative scales
771
+ ball.radius = Math.abs(scale.x);
772
+ if (changed)
773
+ collider.setShape(ball);
774
+ break;
775
+ }
776
+ case ShapeType.Cuboid:
777
+ const cuboid = shape as Cuboid;
778
+ const sc = col as IBoxCollider;
779
+ cuboid.halfExtents.x = sc.size.x * 0.5;
780
+ cuboid.halfExtents.y = sc.size.y * 0.5;
781
+ cuboid.halfExtents.z = sc.size.z * 0.5;
782
+ collider.setShape(cuboid);
783
+ break;
784
+ }
785
+ }
786
+
787
+ private internalUpdateRigidbodyProperties(rb: IRigidbody, rigidbody: RigidBody) {
743
788
  // continuous collision detection
744
789
  // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#continuous-collision-detection
745
790
  rigidbody.enableCcd(rb.collisionDetectionMode !== CollisionDetectionMode.Discrete);
src/engine/engine_types.ts CHANGED
@@ -244,6 +244,14 @@
244
244
  sharedMaterial?: PhysicsMaterial;
245
245
  }
246
246
 
247
+ export declare interface ISphereCollider extends ICollider {
248
+ radius: number;
249
+ }
250
+
251
+ export declare interface IBoxCollider extends ICollider {
252
+ size: Vec3;
253
+ }
254
+
247
255
  export declare interface IRigidbody extends IComponent {
248
256
  constraints: RigidbodyConstraints;
249
257
  isKinematic: boolean;
@@ -255,7 +263,7 @@
255
263
  useGravity: boolean;
256
264
  gravityScale: number;
257
265
  dominanceGroup: number;
258
-
266
+
259
267
  collisionDetectionMode: CollisionDetectionMode;
260
268
 
261
269
  lockPositionX: boolean;
@@ -422,14 +430,14 @@
422
430
  sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult>;
423
431
 
424
432
  // Collider methods
425
- addSphereCollider(collider: ICollider, center: Vector3, radius: number);
433
+ addSphereCollider(collider: ICollider, center: Vector3);
426
434
  addBoxCollider(collider: ICollider, center: Vector3, size: Vector3);
427
435
  addCapsuleCollider(collider: ICollider, center: Vector3, radius: number, height: number);
428
436
  addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale: Vector3);
429
437
 
430
438
  // Rigidbody methods
431
439
  wakeup(rb: IRigidbody);
432
- updateProperties(rb: IRigidbody);
440
+ updateProperties(rb: IRigidbody | ICollider);
433
441
  resetForces(rb: IRigidbody, wakeup: boolean);
434
442
  resetTorques(rb: IRigidbody, wakeup: boolean);
435
443
  addForce(rb: IRigidbody, vec: Vec3, wakeup: boolean);
src/engine/engine_util_decorator.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { $isAssigningProperties } from "./engine_serialization_core.js";
2
- import { LogType, showBalloonMessage } from "./debug/index.js";
2
+ import { LogType, isDevEnvironment, showBalloonMessage } from "./debug/index.js";
3
3
  import { Constructor, IComponent } from "./engine_types.js";
4
+ import { Quaternion, Vector2, Vector3, Vector4 } from "three";
5
+ import { Watch, unwatchWrite, watchWrite } from "./engine_utils.js";
4
6
 
5
7
 
6
8
  declare type setter = (v: any) => void;
@@ -41,12 +43,30 @@
41
43
  const awake = target.__internalAwake;
42
44
  target.__internalAwake = function () {
43
45
 
46
+ if (!this.onValidate) {
47
+ if(isDevEnvironment()) console.warn("Usage of @validate decorate detected but there is no onValidate method in your class: \"" + target.constructor?.name + "\"")
48
+ return;
49
+ }
50
+
44
51
  // only build wrapper once per type
45
52
  if (this[$prop] === undefined) {
46
53
 
47
54
  // make sure the field is initialized in a hidden property
48
55
  this[$prop] = this[propertyKey];
49
56
 
57
+ // For complex types we need to watch the write operation (the underlying values)
58
+ // Since the object itself doesnt change (normally)
59
+ // This is relevant if we want to use @validate() on e.g. a Vector3 which is animated from an animationclip
60
+ const _val = this[propertyKey];
61
+ if (_val instanceof Vector2 ||
62
+ _val instanceof Vector3 ||
63
+ _val instanceof Vector4 ||
64
+ _val instanceof Quaternion) {
65
+ const vec = this[propertyKey];
66
+ const cb = () => { this.onValidate(propertyKey); }
67
+ watchWrite(vec, cb)
68
+ }
69
+
50
70
  Object.defineProperty(this, propertyKey, {
51
71
  set: function (v) {
52
72
  if (this[$isAssigningProperties] === true) {
src/engine/engine_utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // use for typesafe interface method calls
2
+ import { Quaternion, Vector, Vector2, Vector3, Vector4 } from "three";
2
3
  import { SourceIdentifier } from "./engine_types.js";
3
4
 
4
5
  // https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
@@ -276,6 +277,7 @@
276
277
 
277
278
  export interface IWatch {
278
279
  subscribeWrite(callback: WriteCallback);
280
+ unsubscribeWrite(callback: WriteCallback);
279
281
  apply();
280
282
  revoke();
281
283
  dispose();
@@ -287,6 +289,11 @@
287
289
  subscribeWrite(callback: WriteCallback) {
288
290
  this.writeCallbacks.push(callback);
289
291
  }
292
+ unsubscribeWrite(callback: WriteCallback) {
293
+ const i = this.writeCallbacks.indexOf(callback);
294
+ if (i === -1) return;
295
+ this.writeCallbacks.splice(i, 1);
296
+ }
290
297
  private writeCallbacks: (WriteCallback)[] = [];
291
298
 
292
299
  constructor(object: object, prop: string) {
@@ -372,6 +379,11 @@
372
379
  w.subscribeWrite(callback);
373
380
  }
374
381
  }
382
+ unsubscribeWrite(callback: WriteCallback) {
383
+ for (const w of this._watches) {
384
+ w.unsubscribeWrite(callback);
385
+ }
386
+ }
375
387
 
376
388
  apply() {
377
389
  for (const w of this._watches) {
@@ -393,7 +405,36 @@
393
405
  }
394
406
  }
395
407
 
408
+ const watchesKey = Symbol("needle:watches");
409
+ /** Subscribe to an object being written to
410
+ * Currently supporting Vector3
411
+ */
412
+ export function watchWrite(vec: Vector, cb: Function) {
413
+ if (!vec[watchesKey]) {
414
+ if (vec instanceof Vector2) {
415
+ vec[watchesKey] = new Watch(vec, ["x", "y"]);
416
+ }
417
+ else if (vec instanceof Vector3) {
418
+ vec[watchesKey] = new Watch(vec, ["x", "y", "z"]);
419
+ }
420
+ else if (vec instanceof Vector4 || vec instanceof Quaternion) {
421
+ vec[watchesKey] = new Watch(vec, ["x", "y", "z", "w"]);
422
+ }
423
+ else {
424
+ return false;
425
+ }
426
+ }
427
+ vec[watchesKey].subscribeWrite(cb);
428
+ return true;
429
+ }
430
+ export function unwatchWrite(vec: Vector, cb: Function) {
431
+ if (!vec) return;
432
+ const watch = vec[watchesKey];
433
+ if (!watch) return;
434
+ watch.unsubscribeWrite(cb);
435
+ };
396
436
 
437
+
397
438
  export function isMobileDevice() {
398
439
  return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
399
440
  }
src/engine/extensions/NEEDLE_lightmaps.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  // should we split it into multiple extensions?
14
14
 
15
15
  export const EXTENSION_NAME = "NEEDLE_lightmaps";
16
- const debug = getParam("debuglightmapsextension");
16
+ const debug = getParam("debuglightmapsextension") || getParam("debuglightmaps")
17
17
 
18
18
  export enum LightmapType {
19
19
  Lightmap = 0,
@@ -65,6 +65,8 @@
65
65
  const dependencies: Array<Promise<any>> = [];
66
66
  for (const entry of arr) {
67
67
  if (entry.pointer) {
68
+ if (debug)
69
+ console.log(entry);
68
70
  let res: Promise<any> | null = null;
69
71
  // Check if the pointer is a json pointer:
70
72
  if (entry.pointer.startsWith("/textures/")) {
src/engine-components/RendererLightmap.ts CHANGED
@@ -42,7 +42,10 @@
42
42
  this.lightmapScaleOffset = lightmapScaleOffset;
43
43
  this.lightmapTexture = lightmapTexture;
44
44
 
45
- if (debug) this.setLightmapDebugMaterial();
45
+ if (debug) {
46
+ console.log("Lightmap:", this.gameObject.name, lightmapIndex, "\nScaleOffset:", lightmapScaleOffset, "\nTexture:", lightmapTexture)
47
+ this.setLightmapDebugMaterial();
48
+ }
46
49
  this.applyLightmap();
47
50
  }
48
51
 
@@ -134,14 +137,8 @@
134
137
  vec2 lUv = vUv1.xy * lightmapScaleOffset.xy + vec2(lightmapScaleOffset.z, (1. - (lightmapScaleOffset.y + lightmapScaleOffset.w)));
135
138
 
136
139
  vec4 lightMapTexel = texture2D( lightMap, lUv);
137
- // The range of RGBM lightmaps goes from 0 to 34.49 (5^2.2) in linear space, and from 0 to 5 in gamma space.
138
- // lightMapTexel.rgb *= lightMapTexel.a * 8.; // no idea where that "8" comes from... heuristically derived
139
- // lightMapTexel.a = 1.;
140
- // lightMapTexel = conv_sRGBToLinear(lightMapTexel);
141
- // lightMapTexel.rgb = vec3(1.);
142
-
143
- // gl_FragColor = vec4(vUv1.xy, 0, 1);
144
140
  gl_FragColor = lightMapTexel;
141
+ gl_FragColor.a = 1.;
145
142
  }
146
143
  `,
147
144
  defines: { USE_LIGHTMAP: '' }
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { Behaviour, GameObject } from "../Component.js";
2
- import { Matrix4, Object3D } from "three";
2
+ import { Matrix4, Object3D, Plane, Quaternion, Ray, Raycaster, Vector2, Vector3 } from "three";
3
3
  import { WebAR, WebXR } from "./WebXR.js";
4
4
  import { InstancingUtil } from "../../engine/engine_instancing.js";
5
5
  import { serializable } from "../../engine/engine_serialization_decorator.js";
6
+ import { Context } from "../../engine/engine_context.js";
6
7
 
7
8
  // https://github.com/takahirox/takahirox.github.io/blob/master/js.mmdeditor/examples/js/controls/DeviceOrientationControls.js
8
9
 
10
+ const tempMatrix = new Matrix4();
11
+
9
12
  export class WebARSessionRoot extends Behaviour {
10
13
 
11
14
  webAR: WebAR | null = null;
@@ -17,7 +20,11 @@
17
20
  @serializable()
18
21
  invertForward: boolean = false;
19
22
 
23
+ /** Preview feature: enable touch transform */
20
24
  @serializable()
25
+ arTouchTransform: boolean = false;
26
+
27
+ @serializable()
21
28
  get arScale(): number {
22
29
  return this._arScale;
23
30
  }
@@ -30,6 +37,7 @@
30
37
  private readonly _initalMatrix = new Matrix4();
31
38
  private readonly _selectStartFn = this.onSelectStart.bind(this);
32
39
  private readonly _selectEndFn = this.onSelectEnd.bind(this);
40
+ private userInput?: WebXRSessionRootUserInput;
33
41
 
34
42
  start() {
35
43
  const xr = GameObject.findObjectOfType(WebXR);
@@ -49,6 +57,7 @@
49
57
  private _anchor: XRAnchor | null = null;
50
58
 
51
59
  onBegin(session: XRSession) {
60
+
52
61
  this._placementPose = null;
53
62
  this.gameObject.visible = false;
54
63
  this.gameObject.matrixAutoUpdate = false;
@@ -77,9 +86,7 @@
77
86
 
78
87
  onUpdate(rig: Object3D | null, _session: XRSession, hit: XRHitTestResult | null, pose: XRPose | null | undefined): boolean {
79
88
 
80
-
81
89
  if (pose && !this._placementPose) {
82
-
83
90
  if (!this._gotFirstHitTestResult) {
84
91
  this._gotFirstHitTestResult = true;
85
92
  this.dispatchEvent(new CustomEvent('canPlaceSession', { detail: { pose: pose } }));
@@ -94,19 +101,11 @@
94
101
  this.placeAt(rig, poseMatrix);
95
102
  if (hit && pose)
96
103
  this.onCreatePlacementAnchor(hit, pose);
104
+
97
105
  return true;
98
106
  }
99
107
  }
100
108
  return false;
101
-
102
- // if (this._placementPose) {
103
- // this.gameObject.matrixAutoUpdate = false;
104
- // const matrix = pose?.transform.matrix;
105
- // if (matrix) {
106
- // this.gameObject.matrix.fromArray(matrix);
107
- // }
108
- // this.gameObject.visible = true;
109
- // }
110
109
  }
111
110
 
112
111
  private onCreatePlacementAnchor(hit: XRHitTestResult, pose: XRPose) {
@@ -118,45 +117,55 @@
118
117
  }
119
118
 
120
119
  private _anchorMatrix: Matrix4 = new Matrix4();
120
+
121
121
  onBeforeRender(frame: XRFrame | null): void {
122
- if (frame && this._anchor && this._rig) {
123
- const referenceSpace = this.context.renderer.xr.getReferenceSpace();
124
- if (referenceSpace) {
125
- const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
126
- if (pose) {
127
- const poseMatrix = this._anchorMatrix.fromArray(pose.transform.matrix).invert();
128
- this.placeAt(this._rig, poseMatrix);
122
+ if (frame && this._rig) {
123
+ if (this._anchor) {
124
+ const referenceSpace = this.context.renderer.xr.getReferenceSpace();
125
+ if (referenceSpace) {
126
+ const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
127
+ if (pose) {
128
+ const poseMatrix = this._anchorMatrix.fromArray(pose.transform.matrix).invert();
129
+ this.placeAt(this._rig, poseMatrix);
130
+ return;
131
+ }
129
132
  }
130
133
  }
134
+ else if (this._placementPose) {
135
+ this.placeAt(this._rig, this._placementPose!);
136
+ }
131
137
  }
132
138
  }
133
139
 
134
140
  private _invertedSessionRootMatrix: Matrix4 = new Matrix4();
141
+ private _invertForwardMatrix: Matrix4 = new Matrix4().makeRotationY(Math.PI);
142
+
135
143
  placeAt(rig: Object3D | null, mat: Matrix4) {
136
144
  if (!this._placementPose) this._placementPose = new Matrix4();
137
145
  this._placementPose.copy(mat);
146
+
138
147
  // apply session root offset
139
148
  const invertedSessionRoot = this._invertedSessionRootMatrix.copy(this.gameObject.matrixWorld).invert();
140
149
  this._placementPose.premultiply(invertedSessionRoot);
141
150
  if (rig) {
142
-
143
151
  if (this.invertForward) {
144
- const rot = new Matrix4().makeRotationY(Math.PI);
145
- this._placementPose.premultiply(rot);
152
+ this._placementPose.premultiply(this._invertForwardMatrix);
146
153
  }
154
+
155
+ if (!this.userInput) this.userInput = new WebXRSessionRootUserInput(this.context);
156
+ this.userInput.enable();
157
+
147
158
  this._rig = rig;
148
-
149
159
  this.setScale(this.arScale);
150
160
  }
151
161
  else this._rig = null;
152
- // this.gameObject.matrix.copy(this._placementPose);
153
- // if (rig) {
154
- // this.gameObject.matrix.premultiply(rig.matrixWorld)
155
- // }
156
162
  this.gameObject.visible = true;
157
163
  }
158
164
 
159
165
  onEnd(rig: Object3D | null, _session: XRSession) {
166
+ this.userInput?.disable();
167
+ this.userInput?.reset();
168
+
160
169
  this._placementPose = null;
161
170
  this.gameObject.visible = false;
162
171
  this.gameObject.matrixAutoUpdate = false;
@@ -200,10 +209,235 @@
200
209
  }
201
210
  // we apply the transform to the rig because we want to move the user's position for easy networking
202
211
  rig.matrixAutoUpdate = false;
212
+ if (this.arTouchTransform && this.userInput) {
213
+ this.userInput.applyMatrixTo(this._placementPose);
214
+ // rig.matrix.premultiply(this.userInput.offset);
215
+ }
203
216
  rig.matrix.multiplyMatrices(tempMatrix.makeScale(scale, scale, scale), this._placementPose);
204
217
  rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
205
218
  rig.updateMatrixWorld();
206
219
  }
207
220
  }
208
221
 
209
- const tempMatrix = new Matrix4();
222
+
223
+
224
+
225
+ class WebXRSessionRootUserInput {
226
+ private static up = new Vector3(0, 1, 0);
227
+ private static zero = new Vector3(0, 0, 0);
228
+ private static one = new Vector3(1, 1, 1);
229
+
230
+ oneFingerDrag: boolean = true;
231
+ twoFingerRotate: boolean = true;
232
+ twoFingerScale: boolean = true;
233
+
234
+ readonly context: Context;
235
+ readonly offset: Matrix4;
236
+ readonly plane: Plane;
237
+
238
+ private _scale: number = 1;
239
+
240
+ // readonly translate: Vector3 = new Vector3();
241
+ // readonly rotation: Quaternion = new Quaternion();
242
+ // readonly scale: Vector3 = new Vector3(1, 1, 1);
243
+
244
+ constructor(context: Context) {
245
+ this.context = context;
246
+ this.offset = new Matrix4()
247
+ this.plane = new Plane();
248
+ this.plane.setFromNormalAndCoplanarPoint(WebXRSessionRootUserInput.up, WebXRSessionRootUserInput.zero);
249
+ }
250
+
251
+ private _enabled: boolean = false;
252
+ enable() {
253
+ if (this._enabled) return;
254
+ this._enabled = true;
255
+ window.addEventListener('touchstart', this.touchStart, { passive: false });
256
+ window.addEventListener('touchmove', this.touchMove, { passive: false });
257
+ window.addEventListener('touchend', this.touchEnd, { passive: false });
258
+ }
259
+ disable() {
260
+ if (!this._enabled) return;
261
+ this._enabled = false;
262
+ window.removeEventListener('touchstart', this.touchStart);
263
+ window.removeEventListener('touchmove', this.touchMove);
264
+ window.removeEventListener('touchend', this.touchEnd);
265
+ }
266
+ reset() {
267
+ this._scale = 1;
268
+ this.offset.identity();
269
+ }
270
+ applyMatrixTo(matrix: Matrix4) {
271
+ matrix.premultiply(this.offset);
272
+ // if (this._needsUpdate)
273
+ // this.updateMatrix();
274
+ // matrix.premultiply(this._rotationMatrix);
275
+ // matrix.premultiply(this.offset).premultiply(this._rotationMatrix)
276
+ }
277
+
278
+ // private _needsUpdate: boolean = true;
279
+ // private _rotationMatrix: Matrix4 = new Matrix4();
280
+ // private updateMatrix() {
281
+ // this._needsUpdate = false;
282
+ // this._rotationMatrix.makeRotationFromQuaternion(this.rotation);
283
+ // this.offset.compose(this.translate, new Quaternion(), this.scale);
284
+ // // const rot = this._tempMatrix.makeRotationY(this.angle);
285
+ // // this.translate.applyMatrix4(rot);
286
+ // // this.offset.elements[12] = this.translate.x;
287
+ // // this.offset.elements[13] = this.translate.y;
288
+ // // this.offset.elements[14] = this.translate.z;
289
+ // // this.offset.premultiply(rot);
290
+ // // const s = this.scale;
291
+ // // this.offset.premultiply(this._tempMatrix.makeScale(s, s, s));
292
+ // }
293
+
294
+ private prev: Map<number, { x: number, z: number, screenx: number, screeny: number }> = new Map();
295
+ private _didMultitouch: boolean = false;
296
+
297
+ private touchStart = (evt: TouchEvent) => {
298
+ for (let i = 0; i < evt.changedTouches.length; i++) {
299
+ const touch = evt.changedTouches[i];
300
+ if (!this.prev.has(touch.identifier))
301
+ this.prev.set(touch.identifier, {
302
+ x: 0,
303
+ z: 0,
304
+ screenx: 0,
305
+ screeny: 0,
306
+ });
307
+ const prev = this.prev.get(touch.identifier);
308
+ if (prev) {
309
+ const pos = this.getPositionOnPlane(touch.clientX, touch.clientY);
310
+ prev.x = pos.x;
311
+ prev.z = pos.z;
312
+ prev.screenx = touch.clientX;
313
+ prev.screeny = touch.clientY;
314
+ }
315
+ }
316
+ }
317
+ private touchEnd = (evt: TouchEvent) => {
318
+ if (evt.touches.length <= 0) {
319
+ this._didMultitouch = false;
320
+ }
321
+ }
322
+ private touchMove = (evt: TouchEvent) => {
323
+ if (evt.defaultPrevented) return;
324
+
325
+ if (evt.touches.length === 1) {
326
+ // if we had multiple touches before due to e.g. pinching / rotating
327
+ // and stopping one of the touches, we don't want to move the scene suddenly
328
+ // this will be resettet when all touches stop
329
+ if (this._didMultitouch) {
330
+ return;
331
+ }
332
+ const touch = evt.touches[0];
333
+ const prev = this.prev.get(touch.identifier);
334
+ if (!prev) return;
335
+ const pos = this.getPositionOnPlane(touch.clientX, touch.clientY);
336
+ const dx = pos.x - prev.x;
337
+ const dy = pos.z - prev.z;
338
+ if (dx === 0 && dy === 0) return;
339
+ if (this.oneFingerDrag)
340
+ this.addMovement(dx, dy);
341
+ prev.x = pos.x;
342
+ prev.z = pos.z;
343
+ prev.screenx = touch.clientX;
344
+ prev.screeny = touch.clientY;
345
+ return;
346
+ }
347
+ else if (evt.touches.length === 2) {
348
+ this._didMultitouch = true;
349
+ const touch1 = evt.touches[0];
350
+ const touch2 = evt.touches[1];
351
+ const prev1 = this.prev.get(touch1.identifier);
352
+ const prev2 = this.prev.get(touch2.identifier);
353
+ if (!prev1 || !prev2) return;
354
+
355
+ if (this.twoFingerRotate) {
356
+ const angle1 = Math.atan2(touch1.clientY - touch2.clientY, touch1.clientX - touch2.clientX);
357
+ const lastAngle = Math.atan2(prev1.screeny - prev2.screeny, prev1.screenx - prev2.screenx);
358
+ const angleDiff = angle1 - lastAngle;
359
+ if (Math.abs(angleDiff) > 0.001) {
360
+ this.addRotation(angleDiff);
361
+ }
362
+ }
363
+
364
+ if (this.twoFingerScale) {
365
+ const distx = touch1.clientX - touch2.clientX;
366
+ const disty = touch1.clientY - touch2.clientY;
367
+ const dist = Math.sqrt(distx * distx + disty * disty);
368
+ const lastDistx = prev1.screenx - prev2.screenx;
369
+ const lastDisty = prev1.screeny - prev2.screeny;
370
+ const lastDist = Math.sqrt(lastDistx * lastDistx + lastDisty * lastDisty);
371
+ const distDiff = dist - lastDist;
372
+ if (Math.abs(distDiff) > 2) {
373
+ this.addScale(distDiff)
374
+ }
375
+ }
376
+
377
+ prev1.screenx = touch1.clientX;
378
+ prev1.screeny = touch1.clientY;
379
+ prev2.screenx = touch2.clientX;
380
+ prev2.screeny = touch2.clientY;
381
+ }
382
+ }
383
+
384
+ private readonly _raycaster: Raycaster = new Raycaster();
385
+ private readonly _intersection: Vector3 = new Vector3();
386
+ private readonly _screenPos: Vector3 = new Vector3();
387
+
388
+ private getPositionOnPlane(tx: number, ty: number): Vector3 {
389
+ const camera = this.context.mainCamera!;
390
+ this._screenPos.x = (tx / window.innerWidth) * 2 - 1;
391
+ this._screenPos.y = -(ty / window.innerHeight) * 2 + 1;
392
+ this._screenPos.z = 1;
393
+
394
+ this._screenPos.unproject(camera);
395
+ this._raycaster.set(camera.position, this._screenPos.sub(camera.position));
396
+ this._raycaster.ray.intersectPlane(this.plane, this._intersection);
397
+ return this._intersection;
398
+ }
399
+
400
+ private addMovement(dx: number, dz: number) {
401
+ // this.translate.x -= dx;
402
+ // this.translate.z -= dz;
403
+ // this._needsUpdate = true;
404
+ // return
405
+ // some arbitrary factor
406
+ dx *= .75;
407
+ dz *= .75;
408
+ // increase diff if the scene is scaled small
409
+ dx /= this._scale;
410
+ dz /= this._scale;
411
+ // apply it
412
+ this.offset.elements[12] -= dx;
413
+ this.offset.elements[14] -= dz;
414
+ };
415
+
416
+ private readonly _tempMatrix: Matrix4 = new Matrix4();
417
+
418
+ private addScale(diff: number) {
419
+ diff /= window.innerWidth
420
+
421
+ // this.scale.x *= 1 + diff;
422
+ // this.scale.y *= 1 + diff;
423
+ // this.scale.z *= 1 + diff;
424
+ // this._needsUpdate = true;
425
+ // return;
426
+
427
+
428
+ // we use this factor to modify the translation factor (in apply movement)
429
+ this._scale *= 1 + diff;
430
+ // apply the scale
431
+ this._tempMatrix.makeScale(1 - diff, 1 - diff, 1 - diff);
432
+ this.offset.premultiply(this._tempMatrix);
433
+ }
434
+
435
+
436
+ private addRotation(rot: number) {
437
+ // this.rotation.multiply(new Quaternion().setFromAxisAngle(WebXRSessionRootUserInput.up, rot));
438
+ // this._needsUpdate = true;
439
+ // return;
440
+ this._tempMatrix.makeRotationY(rot);
441
+ this.offset.premultiply(this._tempMatrix);
442
+ }
443
+ }