Needle Engine

Changes between version 3.46.0-beta.3 and 3.46.0-beta.4
Files changed (2) hide show
  1. src/engine-components/DragControls.ts +70 -49
  2. src/engine-components/SceneSwitcher.ts +78 -11
src/engine-components/DragControls.ts CHANGED
@@ -56,11 +56,11 @@
56
56
  /** Snap dragged objects to a XYZ grid – 0 means: no snapping. */
57
57
  @serializable()
58
58
  public snapGridResolution: number = 0.0;
59
-
59
+
60
60
  /** Keep the original rotation of the dragged object. */
61
61
  @serializable()
62
62
  public keepRotation: boolean = true;
63
-
63
+
64
64
  /** How and where the object is dragged along while dragging in XR. */
65
65
  @serializable()
66
66
  public xrDragMode: DragMode = DragMode.Attached;
@@ -82,7 +82,7 @@
82
82
 
83
83
  public static get HasAnySelected(): boolean { return this._active > 0; }
84
84
  private static _active: number = 0;
85
-
85
+
86
86
  /** The object to be dragged – we pass this to handlers when they are created */
87
87
  private targetObject: GameObject | null = null;
88
88
  private orbit: OrbitControls | null = null;
@@ -167,10 +167,10 @@
167
167
  }
168
168
 
169
169
  DragControls._active += 1;
170
-
170
+
171
171
  const newDragHandler = new DragPointerHandler(this, this.targetObject || this.gameObject);
172
172
  this._dragHandlers.set(args.event.space, newDragHandler);
173
-
173
+
174
174
  // We need to turn off OrbitControls immediately, otherwise they still get data for a short moment
175
175
  // and they don't properly handle being disabled while already processing data (smoothing happens when enabling again)
176
176
  if (this.orbit) this.orbit.enabled = false;
@@ -197,8 +197,8 @@
197
197
 
198
198
  onPointerUp(args: PointerEventData) {
199
199
 
200
- if(debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
201
-
200
+ if (debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
201
+
202
202
  if (!this.allowEdit(this.gameObject)) return;
203
203
  if (args.button !== 0) return;
204
204
  this._potentialDragStartEvt = null;
@@ -214,10 +214,10 @@
214
214
  if (handler) {
215
215
  if (DragControls._active > 0)
216
216
  DragControls._active -= 1;
217
-
217
+
218
218
  if (handler.onDragEnd) handler.onDragEnd(args);
219
219
  this._dragHandlers.delete(args.event.space);
220
-
220
+
221
221
  if (this._dragHandlers.size === 0) {
222
222
  this.onLastDragEnd(args);
223
223
  }
@@ -254,7 +254,7 @@
254
254
 
255
255
  for (const handler of this._dragHandlers.values())
256
256
  if (handler.onDragUpdate) handler.onDragUpdate(this._dragHandlers.size);
257
-
257
+
258
258
  if (this._dragHelper && this._dragHelper.hasSelected)
259
259
  this.onAnyDragUpdate();
260
260
  }
@@ -312,7 +312,7 @@
312
312
  }
313
313
 
314
314
  const object = this.targetObject || this.gameObject;
315
-
315
+
316
316
  InstancingUtil.markDirty(object);
317
317
  }
318
318
 
@@ -347,7 +347,7 @@
347
347
  getTotalMovement?(): Vector3;
348
348
  /** Target object can change mid-flight (e.g. in Duplicatable), handlers should react properly to that */
349
349
  setTargetObject(obj: Object3D | null): void;
350
-
350
+
351
351
  /** Prewarms the drag – can already move internal points around here but should not move the object itself */
352
352
  collectMovementInfo?(): void;
353
353
  onDragStart?(args: PointerEventData): void;
@@ -382,7 +382,7 @@
382
382
 
383
383
  this._followObject = new Object3D() as GameObject;
384
384
  this._manipulatorObject = new Object3D() as GameObject;
385
-
385
+
386
386
  this.context.scene.add(this._manipulatorObject);
387
387
 
388
388
  const rig = NeedleXRSession.active?.rig?.gameObject;
@@ -401,7 +401,7 @@
401
401
  rig.worldToLocal(this._tempVec2);
402
402
  }
403
403
  this._initialDistance = this._tempVec1.distanceTo(this._tempVec2);
404
-
404
+
405
405
  if (this._initialDistance < 0.02) {
406
406
  if (debug) {
407
407
  console.log("Finding alternative drag attachment points since initial distance is too low: " + this._initialDistance.toFixed(2));
@@ -464,7 +464,7 @@
464
464
  console.error("onDragEnd called on MultiTouchDragHandler without valid handlers. This is likely a bug.");
465
465
  return;
466
466
  }
467
-
467
+
468
468
  // we want to initialize the drag points for these handlers again.
469
469
  // one of them will be removed, but we don't know here which one
470
470
  this.handlerA.recenter();
@@ -493,7 +493,7 @@
493
493
  console.error("alignManipulator called on MultiTouchDragHandler without valid handlers. This is likely a bug.", this);
494
494
  return;
495
495
  }
496
-
496
+
497
497
  if (!this.handlerA.followObject || !this.handlerB.followObject) {
498
498
  console.error("alignManipulator called on MultiTouchDragHandler without valid follow objects. This is likely a bug.", this.handlerA, this.handlerB);
499
499
  return;
@@ -520,7 +520,7 @@
520
520
  this._manipulatorObject.updateMatrixWorld(true);
521
521
 
522
522
  if (debug) {
523
- Gizmos.DrawLabel(this._tempVec3.clone().add(new Vector3(0,0.2,0)), "A:B " + dist.toFixed(2), 0.03);
523
+ Gizmos.DrawLabel(this._tempVec3.clone().add(new Vector3(0, 0.2, 0)), "A:B " + dist.toFixed(2), 0.03);
524
524
  Gizmos.DrawLine(this._tempVec1, this._tempVec2, 0x00ff00, 0, false);
525
525
 
526
526
  // const wp = this._manipulatorObject.worldPosition;
@@ -585,7 +585,7 @@
585
585
  const rot = draggedObject.worldQuaternion;
586
586
  rot.slerp(targetObject.worldQuaternion, t);
587
587
  draggedObject.worldQuaternion = rot;
588
-
588
+
589
589
  const scl = draggedObject.worldScale;
590
590
  scl.lerp(targetObject.worldScale, t);
591
591
  draggedObject.worldScale = scl;
@@ -614,7 +614,7 @@
614
614
  private _lastRig: IGameObject | undefined = undefined;
615
615
 
616
616
  /** This object is placed at the pivot of the dragged object, and parented to the control space. */
617
- private _followObject: GameObject;
617
+ private _followObject: GameObject;
618
618
  private _totalMovement: Vector3 = new Vector3();
619
619
  /** Motion along the pointer ray. On screens this doesn't change. In XR it can be used to determine how much
620
620
  * effort someone is putting into moving an object closer or further away. */
@@ -663,7 +663,7 @@
663
663
 
664
664
  this.gameObject.add(this._followObject);
665
665
  this._followObject.matrixAutoUpdate = false;
666
-
666
+
667
667
  this._followObject.position.set(0, 0, 0);
668
668
  this._followObject.quaternion.set(0, 0, 0, 1);
669
669
  this._followObject.scale.set(1, 1, 1);
@@ -690,20 +690,19 @@
690
690
  this._totalMovementAlongRayDirection = 0;
691
691
  this._lastDragPosRigSpace = undefined;
692
692
 
693
- if (debug)
694
- {
693
+ if (debug) {
695
694
  Gizmos.DrawLine(hitPointWP, p.worldPosition, 0x00ff00, 0.5, false);
696
- Gizmos.DrawLabel(p.worldPosition.add(new Vector3(0,0.1,0)), this._grabStartDistance.toFixed(2), 0.03, 0.5);
695
+ Gizmos.DrawLabel(p.worldPosition.add(new Vector3(0, 0.1, 0)), this._grabStartDistance.toFixed(2), 0.03, 0.5);
697
696
  }
698
697
  }
699
698
 
700
699
  onDragStart(args: PointerEventData) {
701
-
700
+
702
701
  args.event.space.add(this._followObject);
703
702
 
704
703
  // prepare for drag, we will start dragging after an object has been dragged for a few centimeters
705
704
  this._lastDragPosRigSpace = undefined;
706
-
705
+
707
706
  if (args.point && args.normal) {
708
707
  this._hitPointInLocalSpace.copy(args.point);
709
708
  this.gameObject.worldToLocal(this._hitPointInLocalSpace);
@@ -741,7 +740,7 @@
741
740
 
742
741
  switch (dragMode) {
743
742
  case DragMode.XZPlane:
744
- const up = new Vector3(0,1,0);
743
+ const up = new Vector3(0, 1, 0);
745
744
  if (this.gameObject.parent) {
746
745
  // TODO in this case _dragPlane should be in parent space, not world space,
747
746
  // otherwise dragging the parent and this object at the same time doesn't keep the plane constrained
@@ -757,16 +756,11 @@
757
756
  case DragMode.Attached:
758
757
  this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
759
758
  break;
759
+ case DragMode.DynamicViewAngle: // At start (when nothing is hit yet) the drag plane should be aligned to the view
760
+ this.setPlaneViewAligned(hitWP, true);
761
+ break;
760
762
  case DragMode.SnapToSurfaces: // At start (when nothing is hit yet) the drag plane should be aligned to the view
761
- case DragMode.DynamicViewAngle:
762
- const v0 = new Vector3(0, 1, 0);
763
- const v1 = rayDirection;
764
- const angle = v0.angleTo(v1);
765
- const angleThreshold = 0.5;
766
- if (angle > Math.PI / 2 + angleThreshold || angle < Math.PI / 2 - angleThreshold)
767
- this._dragPlane.setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), hitWP);
768
- else
769
- this._dragPlane.setFromNormalAndCoplanarPoint(rayDirection, hitWP);
763
+ this.setPlaneViewAligned(hitWP, false);
770
764
  break;
771
765
  case DragMode.None:
772
766
  break;
@@ -797,7 +791,7 @@
797
791
  bbox.getCenter(bboxCenter);
798
792
  const bboxSize = new Vector3();
799
793
  bbox.getSize(bboxSize);
800
-
794
+
801
795
  // attachment points for dragging
802
796
  this._bottomCenter.copy(bboxCenter.clone().add(new Vector3(0, -bboxSize.y / 2, 0)));
803
797
  this._backCenter.copy(bboxCenter.clone().add(new Vector3(0, 0, bboxSize.z / 2)));
@@ -917,10 +911,9 @@
917
911
  currentDist = Math.max(0.0, factor);
918
912
  currentDist = currentDist * currentDist * currentDist;
919
913
  }
920
- else if (this._grabStartDistance <= 0.5)
921
- {
914
+ else if (this._grabStartDistance <= 0.5) {
922
915
  // TODO there's still a frame delay between dragged objects and the hand models
923
- lerpFactor = 3.0;
916
+ lerpFactor = 3.0;
924
917
  }
925
918
 
926
919
  // reset _followObject to its original position and rotation
@@ -935,6 +928,8 @@
935
928
  this._followObject.position.multiplyScalar(currentDist);
936
929
  this._followObject.updateMatrix();
937
930
 
931
+ const didHaveSurfaceHitPointLastFrame = this._hasLastSurfaceHitPoint;
932
+ this._hasLastSurfaceHitPoint = false;
938
933
  const ray = new Ray(dragSourceWP, rayDirection);
939
934
  let didHit = false;
940
935
 
@@ -964,6 +959,9 @@
964
959
 
965
960
  if (hit.face) {
966
961
  didHit = true;
962
+ this._hasLastSurfaceHitPoint = true;
963
+ this._lastSurfaceHitPoint.copy(hit.point);
964
+
967
965
  const dragTimeThreshold = 0.15;
968
966
  const dragTimeSatisfied = this._draggedOverObjectDuration >= dragTimeThreshold;
969
967
  const dragDistance = 0.001;
@@ -972,7 +970,7 @@
972
970
  // or if the surface normal changed
973
971
  if ((dragTimeSatisfied || dragDistanceSatisfied) &&
974
972
  (this._draggedOverObjectLastSetUp !== this._draggedOverObject
975
- || this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999
973
+ || this._draggedOverObjectLastNormal.dot(hit.face.normal) < 0.999999
976
974
  // if we're dragging on a flat surface with different levels (like the sandbox floor)
977
975
  || this.context.time.frame % 60 === 0
978
976
  )
@@ -988,7 +986,7 @@
988
986
  center.sub(size.multiplyScalar(0.5).multiply(hit.face.normal));
989
987
  this._hitPointInLocalSpace.copy(center);
990
988
  this._hitNormalInLocalSpace.copy(hit.face.normal);
991
-
989
+
992
990
  // ensure plane is far enough up that we don't drag into the surface
993
991
  // Which offset we use here depends on the face normal direction we hit
994
992
  // If we hit the bottom, we want to use the top, and vice versa
@@ -1014,6 +1012,9 @@
1014
1012
  }
1015
1013
  }
1016
1014
  }
1015
+ else if (didHaveSurfaceHitPointLastFrame) {
1016
+ this.setPlaneViewAligned(this.gameObject.worldPosition, false)
1017
+ }
1017
1018
  }
1018
1019
 
1019
1020
  // if(dragMode === DragMode.SnapToSurfaces){
@@ -1034,12 +1035,12 @@
1034
1035
 
1035
1036
  const newWP = this._hitPointInLocalSpace.clone();
1036
1037
  this._followObject.localToWorld(newWP);
1037
-
1038
+
1038
1039
  if (debug) {
1039
1040
  Gizmos.DrawLine(newWP, this._tempVec, 0x00ffff, 0, false);
1040
1041
  }
1041
-
1042
- this._followObject.worldPosition = this._tempVec.multiplyScalar(2).sub(newWP);
1042
+
1043
+ this._followObject.worldPosition = this._tempVec.multiplyScalar(2).sub(newWP);
1043
1044
  this._followObject.updateMatrix();
1044
1045
 
1045
1046
  /*
@@ -1069,11 +1070,11 @@
1069
1070
  this._followObject.worldQuaternion = this._followObjectStartWorldQuaternion;
1070
1071
  this._followObject.updateMatrix();
1071
1072
  }
1072
-
1073
+
1073
1074
  // TODO refactor to a common place
1074
1075
  // TODO should use unscaled time here // some test for lerp speed depending on distance
1075
1076
  const t = Mathf.clamp01(this.context.time.deltaTime * lerpStrength * lerpFactor);// / (currentDist - 1 + 0.01));
1076
-
1077
+
1077
1078
  const wp = draggedObject.worldPosition;
1078
1079
  wp.lerp(this._followObject.worldPosition, t);
1079
1080
  draggedObject.worldPosition = wp;
@@ -1083,8 +1084,7 @@
1083
1084
  draggedObject.worldQuaternion = rot;
1084
1085
 
1085
1086
 
1086
- if (debug)
1087
- {
1087
+ if (debug) {
1088
1088
  const hitPointWP = this._hitPointInLocalSpace.clone();
1089
1089
  draggedObject.localToWorld(hitPointWP);
1090
1090
  // draw grab attachment point and normal. They are in grabbed object space
@@ -1094,7 +1094,7 @@
1094
1094
  Gizmos.DrawRay(hitPointWP, hitNormalWP, 0xff0000);
1095
1095
 
1096
1096
  // debug info
1097
- Gizmos.DrawLabel(wp.add(new Vector3(0, 0.25, 0)),
1097
+ Gizmos.DrawLabel(wp.add(new Vector3(0, 0.25, 0)),
1098
1098
  `Distance: ${this._totalMovement.length().toFixed(2)}\n
1099
1099
  Along Ray: ${this._totalMovementAlongRayDirection.toFixed(2)}\n
1100
1100
  Session: ${!!NeedleXRSession.active}\n
@@ -1124,6 +1124,27 @@
1124
1124
  this._followObject.destroy();
1125
1125
  this._lastDragPosRigSpace = undefined;
1126
1126
  }
1127
+
1128
+
1129
+ private _hasLastSurfaceHitPoint: boolean = false;
1130
+ private readonly _lastSurfaceHitPoint: Vector3 = new Vector3();
1131
+
1132
+ private setPlaneViewAligned(worldPoint: Vector3, useUpAngle: boolean) {
1133
+ if (!this._followObject.parent) {
1134
+ return false;
1135
+ }
1136
+ const viewDirection = (this._followObject.parent as IGameObject).worldForward;;
1137
+ const v0 = getTempVector(0, 1, 0);
1138
+ const v1 = viewDirection;
1139
+ const angle = v0.angleTo(v1);
1140
+ const angleThreshold = 0.5;
1141
+ if (useUpAngle && (angle > Math.PI / 2 + angleThreshold || angle < Math.PI / 2 - angleThreshold))
1142
+ this._dragPlane.setFromNormalAndCoplanarPoint(v0, worldPoint);
1143
+ else
1144
+ this._dragPlane.setFromNormalAndCoplanarPoint(viewDirection, worldPoint);
1145
+ return true;
1146
+
1147
+ }
1127
1148
  }
1128
1149
 
1129
1150
  /** Currently does _only_ provide visuals support for DragControls operations.
src/engine-components/SceneSwitcher.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Object3D } from "three";
1
+ import { EquirectangularReflectionMapping, Object3D, Scene, Texture } from "three";
2
2
 
3
3
  import { Addressables, AssetReference } from "../engine/engine_addressables.js";
4
4
  import { registerObservableAttribute } from "../engine/engine_element_extras.js";
@@ -7,8 +7,10 @@
7
7
  import { serializable } from "../engine/engine_serialization.js";
8
8
  import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
9
9
  import { Behaviour, GameObject } from "./Component.js";
10
+ import { destroy } from "../engine/engine_gameobject.js";
10
11
 
11
12
  const debug = getParam("debugsceneswitcher");
13
+ const experimental_clearSceneOnLoad = getParam("sceneswitcher:clearscene");
12
14
 
13
15
  const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
14
16
  registerObservableAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
@@ -103,58 +105,94 @@
103
105
  */
104
106
  export class SceneSwitcher extends Behaviour {
105
107
 
106
- /** When enabled the first scene will be loaded when the SceneSwitcher becomes active */
108
+ /** When enabled the first scene will be loaded when the SceneSwitcher becomes active
109
+ * @default true
110
+ */
107
111
  @serializable()
108
112
  autoLoadFirstScene: boolean = true;
109
113
 
110
114
  /**
111
115
  * The scenes that can be loaded by the SceneSwitcher.
116
+ * @default []
112
117
  */
113
118
  @serializable(AssetReference)
114
119
  scenes: AssetReference[] = [];
115
120
 
121
+ /**
122
+ * The scene that is displayed while a scene is loading.
123
+ * @default undefined
124
+ */
116
125
  @serializable(AssetReference)
117
126
  loadingScene?: AssetReference;
118
127
 
119
- /** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable */
128
+ /** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable
129
+ * @default "scene"
130
+ */
120
131
  @serializable()
121
132
  queryParameterName: string = "scene";
122
133
 
123
134
  /**
124
135
  * when enabled the scene name will be used as the query parameter (otherwise the scene index will be used)
125
136
  * Needs `queryParameterName` set
126
- * */
137
+ * @default true
138
+ */
127
139
  @serializable()
128
140
  useSceneName: boolean = true;
129
141
 
142
+ /**
143
+ * When enabled the current scene index will be clamped to the scenes array bounds.
144
+ * For example when the last scene is loaded and `clamp` is true then trying to load the `next()` scene will not change the scene.
145
+ * When `clamp` is false and the last scene is loaded then the first scene will be loaded instead.
146
+ * @default true
147
+ */
130
148
  @serializable()
131
149
  clamp: boolean = true;
132
150
 
133
- /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set */
151
+ /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set
152
+ * @default true
153
+ */
134
154
  @serializable()
135
155
  useHistory: boolean = true;
136
156
 
137
- /** when enabled you can switch between scenes using keyboard left, right, A and D or number keys */
157
+ /** when enabled you can switch between scenes using keyboard left, right, A and D or number keys
158
+ * @default true
159
+ */
138
160
  @serializable()
139
161
  useKeyboard: boolean = true;
140
162
 
141
- /** when enabled you can switch between scenes using swipe (mobile only) */
163
+ /** when enabled you can switch between scenes using swipe (mobile only)
164
+ * @default true
165
+ */
142
166
  @serializable()
143
167
  useSwipe: boolean = true;
144
168
 
145
- /** when enabled will automatically apply the environment scene lights */
169
+ /** when enabled will automatically apply the environment scene lights
170
+ * @default true
171
+ */
146
172
  @serializable()
147
173
  useSceneLighting: boolean = true;
148
174
 
149
- /** how many scenes after the currently active scene should be preloaded */
175
+ /** When enabled will automatically apply the skybox from the loaded scene
176
+ * @default true
177
+ */
150
178
  @serializable()
179
+ useSceneBackground: boolean = true;
180
+
181
+ /** how many scenes after the currently active scene should be preloaded
182
+ * @default 1
183
+ */
184
+ @serializable()
151
185
  preloadNext: number = 1;
152
186
 
153
- /** how many scenes before the currently active scene should be preloaded */
187
+ /** how many scenes before the currently active scene should be preloaded
188
+ * @default 1
189
+ */
154
190
  @serializable()
155
191
  preloadPrevious: number = 1;
156
192
 
157
- /** how many scenes can be loaded in parallel */
193
+ /** how many scenes can be loaded in parallel
194
+ * @default 2
195
+ */
158
196
  @serializable()
159
197
  preloadConcurrent: number = 2;
160
198
 
@@ -516,9 +554,38 @@
516
554
  if (this._currentIndex === index) {
517
555
  if (debug) console.log("ADD", scene.uri);
518
556
  this._currentScene = scene;
557
+
558
+
559
+ // Experimental: replace the whole content of the scene
560
+ if (experimental_clearSceneOnLoad) {
561
+ const camera = this.context.mainCameraComponent?.gameObject || this.context.mainCamera;
562
+ camera?.removeFromParent();
563
+ const self = this.gameObject.removeFromParent();
564
+ destroy(this.context.scene, true, true)
565
+ this.context.scene = new Scene();
566
+ this.context.scene.add(self);
567
+ if (camera) {
568
+ this.context.scene.add(camera);
569
+ }
570
+ }
571
+
519
572
  GameObject.add(scene.asset, this.gameObject);
573
+
520
574
  if (this.useSceneLighting)
521
575
  this.context.sceneLighting.enable(scene);
576
+ // Set the background texture from the loaded scene
577
+ if (this.useSceneBackground) {
578
+ const skybox = this.context.lightmaps.tryGetSkybox(scene.url) as Texture;
579
+ if (skybox) {
580
+ skybox.mapping = EquirectangularReflectionMapping;
581
+ this.context.scene.background = skybox;
582
+ }
583
+ else if (debug) {
584
+ console.warn("SceneSwitcher: Can't find skybox for scene " + scene.url);
585
+ }
586
+ }
587
+
588
+
522
589
  if (this.useHistory && index >= 0) {
523
590
  // take the index as the query parameter value
524
591
  let queryParameterValue = index.toString();