Needle Engine

Changes between version 4.0.1-alpha and 4.0.2-alpha
Files changed (5) hide show
  1. src/engine/engine_gltf_builtin_components.ts +7 -4
  2. src/engine/extensions/NEEDLE_components.ts +4 -0
  3. src/engine/xr/NeedleXRSession.ts +15 -8
  4. src/engine-components/OrbitControls.ts +11 -7
  5. src/engine-components/webxr/controllers/XRControllerModel.ts +54 -36
src/engine/engine_gltf_builtin_components.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { LogType, showBalloonMessage } from "./debug/index.js";
6
6
  import { addNewComponent } from "./engine_components.js";
7
- import { builtinComponentKeyName,editorGuidKeyName } from "./engine_constants.js";
7
+ import { builtinComponentKeyName, editorGuidKeyName } from "./engine_constants.js";
8
8
  import { debugExtension } from "./engine_default_parameters.js";
9
9
  import { InstantiateIdProvider } from "./engine_networking_instantiate.js"
10
10
  import { isLocalNetwork } from "./engine_networking_utils.js";
@@ -42,7 +42,10 @@
42
42
  const $context_deserialize_queue = Symbol("deserialize-queue");
43
43
 
44
44
  export async function createBuiltinComponents(context: Context, gltfId: SourceIdentifier, gltf, seed: number | null | UIDProvider = null, extension?: NEEDLE_components) {
45
- if (!gltf) return;
45
+ if (!gltf) {
46
+ console.debug("Can not create component instances: gltf is null");
47
+ return;
48
+ }
46
49
  const lateResolve: Array<(gltf: Object3D) => {}> = [];
47
50
 
48
51
  let idProvider: UIDProvider | null = seed as UIDProvider;
@@ -141,7 +144,7 @@
141
144
  }
142
145
  }
143
146
  const objectIdProvider = idProviderKey && idProviderCache.get(idProviderKey) || idProvider;
144
-
147
+
145
148
  obj.guid = objectIdProvider.generateUUID();
146
149
  if (prev && prev !== "invalid")
147
150
  guidsMap[prev] = obj.guid;
@@ -160,7 +163,7 @@
160
163
  idProviderCache.set(idProviderKey, new InstantiateIdProvider(idProviderKey));
161
164
  }
162
165
  }
163
- else if(debug) console.warn("Can not create IdProvider: component " + comp[originalComponentNameKey] + " has no guid", comp.guid);
166
+ else if (debug) console.warn("Can not create IdProvider: component " + comp[originalComponentNameKey] + " has no guid", comp.guid);
164
167
  const componentIdProvider = idProviderCache.get(idProviderKey) || idProvider
165
168
 
166
169
  const prev = comp.guid;
src/engine/extensions/NEEDLE_components.ts CHANGED
@@ -39,6 +39,8 @@
39
39
  // import
40
40
  parser?: GLTFParser;
41
41
  nodeToObjectMap: NodeToObjectMap = {};
42
+ /** The loaded gltf */
43
+ gltf: GLTF | null = null;
42
44
 
43
45
  // export
44
46
  exportContext!: { [nodeIndex: number]: ExportData };
@@ -163,6 +165,8 @@
163
165
 
164
166
  // called by GLTFLoader
165
167
  async afterRoot(result: GLTF): Promise<void> {
168
+ this.gltf = result;
169
+
166
170
  const parser = result.parser;
167
171
  const ext = parser?.extensions;
168
172
  if (!ext) return;
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -984,10 +984,13 @@
984
984
  // check if an xr controller for this input source already exists
985
985
  // in case we have both an event from inputsourceschange and from the construtor initial input sources
986
986
  if (this.controllers.find(c => c.inputSource === newInputSource)) {
987
- console.warn("Controller already exists for input source", index);
987
+ console.debug("Controller already exists for input source", index);
988
988
  return;
989
989
  }
990
- if (debug) console.log("Adding controller", index);
990
+ else if(this._newControllers.find(c => c.inputSource === newInputSource)) {
991
+ console.debug("Controller already registered for input source", index);
992
+ return;
993
+ }
991
994
  // TODO: check if this is a transient input source AND we can figure out which existing controller it likely belongs to
992
995
  // TODO: do not draw raycasts for controllers that don't have primary input actions / until we know that they have primary input actions
993
996
  const newController = new NeedleXRController(this, newInputSource, index);
@@ -996,7 +999,7 @@
996
999
 
997
1000
  /** Disconnects the controller, invokes events and notifies previou controller (if any) */
998
1001
  private disconnectInputSource(inputSource: XRInputSource) {
999
- const handleController = (oldController: NeedleXRController, _array: Array<NeedleXRController>, i: number) => {
1002
+ const handleRemove = (oldController: NeedleXRController, _array: Array<NeedleXRController>, i: number) => {
1000
1003
  if (oldController.inputSource === inputSource) {
1001
1004
  if (debug) console.log("Disconnecting controller", oldController.index);
1002
1005
  this.controllers.splice(i, 1);
@@ -1014,11 +1017,11 @@
1014
1017
  }
1015
1018
  for (let i = this.controllers.length - 1; i >= 0; i--) {
1016
1019
  const oldController = this.controllers[i];
1017
- handleController(oldController, this.controllers, i);
1020
+ handleRemove(oldController, this.controllers, i);
1018
1021
  }
1019
1022
  for (let i = this._newControllers.length - 1; i >= 0; i--) {
1020
1023
  const oldController = this._newControllers[i];
1021
- handleController(oldController, this._newControllers, i);
1024
+ handleRemove(oldController, this._newControllers, i);
1022
1025
  }
1023
1026
  }
1024
1027
 
@@ -1038,7 +1041,7 @@
1038
1041
  if (this._ended) return;
1039
1042
  this._ended = true;
1040
1043
 
1041
- if (debug) console.log("XR Session ended");
1044
+ console.debug("XR Session ended");
1042
1045
 
1043
1046
  deleteSessionInfo();
1044
1047
 
@@ -1068,9 +1071,13 @@
1068
1071
  }
1069
1072
 
1070
1073
  // make sure we disconnect all controllers
1071
- for (let i = 0; i < this.controllers.length; i++) {
1072
- this.disconnectInputSource(this.controllers[i].inputSource);
1074
+ // we copy the array because the disconnectInputSource method modifies the controllers array
1075
+ const copy = [...this.controllers];
1076
+ for (let i = 0; i < copy.length; i++) {
1077
+ this.disconnectInputSource(copy[i].inputSource);
1073
1078
  }
1079
+ this._newControllers.length = 0;
1080
+ this.controllers.length = 0;
1074
1081
 
1075
1082
  // we want to call leave XR for *all* scripts that are still registered
1076
1083
  // even if they might already be destroyed e.g. by the WebXR component (it destroys the default controller scripts)
src/engine-components/OrbitControls.ts CHANGED
@@ -307,10 +307,13 @@
307
307
  }
308
308
  this._controls.addEventListener("start", this.onControlsChangeStarted);
309
309
 
310
- if (!this._startedListeningToKeyEvents) {
310
+ if (!this._startedListeningToKeyEvents && this.enableKeys) {
311
311
  this._startedListeningToKeyEvents = true;
312
- this._controls.listenToKeyEvents(window.document.body);
312
+ this._controls.listenToKeyEvents(this.context.domElement);
313
313
  }
314
+ else {
315
+ this._controls.stopListenToKeyEvents();
316
+ }
314
317
  }
315
318
  this._syncedTransform = GameObject.getComponent(this.gameObject, SyncedTransform) ?? undefined;
316
319
  this.context.pre_render_callbacks.push(this.__onPreRender);
@@ -330,7 +333,8 @@
330
333
  this._controls.enabled = false;
331
334
  this._controls.autoRotate = false;
332
335
  this._controls.removeEventListener("start", this.onControlsChangeStarted);
333
- // this._controls.reset();
336
+ this._controls.stopListenToKeyEvents();
337
+ this._startedListeningToKeyEvents = false;
334
338
  }
335
339
  this._activePointerEvents.length = 0;
336
340
  this.context.input.removeEventListener("pointerdown", this._onPointerDown);
@@ -383,7 +387,7 @@
383
387
  };
384
388
 
385
389
  private _onPointerUpLate = (evt: NEPointerEvent) => {
386
- if(this.doubleClickToFocus && evt.isDoubleClick && !evt.used){
390
+ if (this.doubleClickToFocus && evt.isDoubleClick && !evt.used) {
387
391
  this.setTargetFromRaycast();
388
392
  }
389
393
  };
@@ -677,7 +681,7 @@
677
681
 
678
682
  // if a user calls setLookTargetPosition we don't want to perform autoTarget in onBeforeRender (and override whatever the user set here)
679
683
  this._didSetTarget++;
680
-
684
+
681
685
  if (debug) {
682
686
  console.warn("OrbitControls: setLookTargetPosition", position, immediateOrDuration);
683
687
  Gizmos.DrawWireSphere(this._lookTargetEndPosition, .2, 0xff0000, 2);
@@ -851,7 +855,7 @@
851
855
  const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance) + size.z / 2;
852
856
 
853
857
  if (debugCameraFit) {
854
- console.log("Fit camera to objects", {fitHeightDistance, fitWidthDistance, distance, verticalFov, horizontalFov});
858
+ console.log("Fit camera to objects", { fitHeightDistance, fitWidthDistance, distance, verticalFov, horizontalFov });
855
859
  }
856
860
 
857
861
  this.maxZoom = distance * 10;
@@ -903,7 +907,7 @@
903
907
  cameraLocalPosition = camera.parent.worldToLocal(cameraLocalPosition);
904
908
  }
905
909
  this.setCameraTargetPosition(cameraLocalPosition, immediate);
906
-
910
+
907
911
  if (debugCameraFit) {
908
912
  const helper = new Box3Helper(box);
909
913
  this.context.scene.add(helper);
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -7,15 +7,17 @@
7
7
  import { AssetReference } from "../../../engine/engine_addressables.js";
8
8
  import { setDontDestroy } from "../../../engine/engine_gameobject.js";
9
9
  import { Gizmos } from "../../../engine/engine_gizmos.js";
10
+ import { getLoader } from "../../../engine/engine_gltf.js";
11
+ import { createBuiltinComponents } from "../../../engine/engine_gltf_builtin_components.js";
10
12
  import { addDracoAndKTX2Loaders } from "../../../engine/engine_loaders.js";
11
13
  import { serializable } from "../../../engine/engine_serialization_decorator.js";
12
- import type { IGameObject } from "../../../engine/engine_types.js";
14
+ import type { IGameObject, SourceIdentifier } from "../../../engine/engine_types.js";
13
15
  import { getParam } from "../../../engine/engine_utils.js";
14
16
  import { NeedleXRController, type NeedleXRControllerEventArgs, type NeedleXREventArgs, NeedleXRSession } from "../../../engine/engine_xr.js";
15
- import { registerExtensions } from "../../../engine/extensions/extensions.js";
17
+ import { registerComponentExtension, registerExtensions } from "../../../engine/extensions/extensions.js";
16
18
  import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
17
19
  import { flipForwardMatrix } from "../../../engine/xr/internal.js";
18
- import { Behaviour, GameObject } from "../../Component.js"
20
+ import { Behaviour, Component, GameObject } from "../../Component.js"
19
21
 
20
22
  const debug = getParam("debugwebxr");
21
23
 
@@ -70,11 +72,11 @@
70
72
  if (this.createControllerModel || this.createHandModel) {
71
73
  if (controller.hand) {
72
74
  if (this.createHandModel) {
73
- const res = await this.loadHandModel(controller);
75
+ const res = await this.loadHandModel(this, controller);
74
76
  // check if the model doesnt exist, the hand disconnected or it's suddenly a controller
75
77
  if (!res || !controller.connected || !controller.isHand) {
76
- res?.handObject?.removeFromParent();
77
- res?.handmesh?.controller?.removeFromParent();
78
+ if (res?.handObject) setDontDestroy(res.handObject, false);
79
+ res?.handObject?.destroy();
78
80
  return;
79
81
  }
80
82
  this._models.push({ controller: controller, model: res.handObject, handmesh: res.handmesh });
@@ -113,6 +115,7 @@
113
115
  }
114
116
  }
115
117
  onXRControllerRemoved(args: NeedleXRControllerEventArgs): void {
118
+ console.debug("XR Controller Removed", args.controller.side, args.controller.index);
116
119
  // we need to find the index by the controller because if controller 0 is removed first then args.controller.index 1 will be at index 0
117
120
  const indexInArray = this._models.findIndex(m => m.controller === args.controller);
118
121
  const entry = this._models[indexInArray];
@@ -120,15 +123,29 @@
120
123
 
121
124
  this._models.splice(indexInArray, 1);
122
125
 
123
- if (entry.handmesh) {
124
- entry.handmesh.handModel?.removeFromParent();
125
- entry.handmesh = undefined;
126
- }
127
126
  if (entry.model) {
128
- entry.model.removeFromParent();
127
+ setDontDestroy(entry.model, false);
128
+ entry.model.destroy();
129
129
  entry.model = undefined;
130
130
  }
131
131
  }
132
+ onLeaveXR(_args: NeedleXREventArgs): void {
133
+ for (const entry of this._models) {
134
+ if (!entry) continue;
135
+
136
+ if (entry.model) {
137
+ setDontDestroy(entry.model, false);
138
+ entry.model.destroy();
139
+ entry.model = undefined;
140
+ }
141
+
142
+ // Unassign the model from the controller when this script becomes inactive
143
+ if (entry.controller.model === entry.model) {
144
+ entry.controller.model = null;
145
+ }
146
+ }
147
+ this._models.length = 0;
148
+ }
132
149
  onBeforeRender() {
133
150
  if (!NeedleXRSession.active) return;
134
151
 
@@ -147,17 +164,6 @@
147
164
  }
148
165
  }
149
166
  }
150
- onLeaveXR(_args: NeedleXREventArgs): void {
151
- for (const entry of this._models) {
152
- if (!entry) continue;
153
- entry.model?.removeFromParent();
154
- // Unassign the model from the controller when this script becomes inactive
155
- if (entry.controller.model === entry.model) {
156
- entry.controller.model = null;
157
- }
158
- }
159
- this._models.length = 0;
160
- }
161
167
 
162
168
  private updateRendering(xr: NeedleXRSession) {
163
169
 
@@ -171,6 +177,7 @@
171
177
  continue;
172
178
  }
173
179
 
180
+
174
181
  // do we have a controller model?
175
182
  if (entry.model && !entry.handmesh) {
176
183
  entry.model.matrixAutoUpdate = false;
@@ -253,7 +260,7 @@
253
260
  return model as IGameObject;
254
261
  }
255
262
 
256
- protected async loadHandModel(controller: NeedleXRController): Promise<{ handObject: IGameObject, handmesh: XRHandMeshModel } | null> {
263
+ protected async loadHandModel(comp: Component, controller: NeedleXRController): Promise<{ handObject: IGameObject, handmesh: XRHandMeshModel } | null> {
257
264
 
258
265
  const context = this.context;
259
266
  const hand = context.renderer.xr.getHand(controller.index);
@@ -265,28 +272,39 @@
265
272
  const loader = new GLTFLoader();
266
273
  addDracoAndKTX2Loaders(loader, context);
267
274
  await registerExtensions(loader, context, this.sourceId ?? "");
268
- loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
275
+ const componentsExtension = registerComponentExtension(loader);
269
276
 
270
- // TODO: we should handle the loading here ourselves to not have this requirement of a specific model name
271
- const expectedHandModelName = controller.side === "left" ? "left." : "right.";
277
+ let filename = "";
278
+
272
279
  const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
273
280
  if (customHand) {
274
- if (!customHand.url.includes(expectedHandModelName)) {
275
- console.warn("XRControllerModel: custom hand model must be named " + expectedHandModelName);
276
- showBalloonWarning("Custom Hand: unexpected name, please see the console for details");
277
- }
278
- else {
279
- const basePath = customHand.url.substring(0, customHand.url.indexOf(expectedHandModelName));
280
- loader.setPath(basePath);
281
- if (debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
282
- }
281
+ const urlWithoutExtension = customHand.url.split('.').slice(0, -1).join('.');
282
+ filename = urlWithoutExtension;
283
+ loader.setPath("");
283
284
  }
285
+ else {
286
+ // DEFAULT hands
287
+ // XRHandmeshModel is using "<handedness>.glb" for loading the file
288
+ filename = controller.inputSource.handedness === "left" ? "left" : "right";
289
+ loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
290
+ }
284
291
 
285
292
 
286
293
  const handObject = new Object3D();
287
294
  setDontDestroy(handObject);
288
295
  // @ts-ignore
289
- const handmesh = new XRHandMeshModel(handObject, hand, loader.path, controller.inputSource.handedness, loader, (object: Object3D) => {
296
+ const handmesh = new XRHandMeshModel(handObject, hand, loader.path, filename, loader, (object: Object3D) => {
297
+
298
+ const gltf = componentsExtension.gltf;
299
+ // The XRHandMeshController removes the hand from the gltf before calling this callback
300
+ // we need this in the GLTF scene however for creating the builtin components
301
+ if (gltf?.scene.children?.length === 0) {
302
+ gltf.scene.children[0] = object;
303
+ }
304
+
305
+ // console.log(controller.side, componentsExtension.gltf, object, componentsExtension.gltf.scene?.children)
306
+ getLoader().createBuiltinComponents(comp.context, comp.sourceId || filename, componentsExtension.gltf, null, componentsExtension);
307
+
290
308
  // The hand mesh should not receive raycasts
291
309
  object.traverse(child => {
292
310
  child.layers.set(2);