@@ -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)
|
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;
|
@@ -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;
|
@@ -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.
|
987
|
+
console.debug("Controller already exists for input source", index);
|
988
988
|
return;
|
989
989
|
}
|
990
|
-
if
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1072
|
-
|
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)
|
@@ -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(
|
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
|
-
|
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);
|
@@ -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
|
77
|
-
res?.
|
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
|
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
|
275
|
+
const componentsExtension = registerComponentExtension(loader);
|
269
276
|
|
270
|
-
|
271
|
-
|
277
|
+
let filename = "";
|
278
|
+
|
272
279
|
const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
|
273
280
|
if (customHand) {
|
274
|
-
|
275
|
-
|
276
|
-
|
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,
|
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);
|