@@ -42,7 +42,9 @@
|
|
42
42
|
marker.avatar = this.gameObject;
|
43
43
|
marker.connectionId = playerstate.owner;
|
44
44
|
}
|
45
|
-
else if(this.context.connection.isConnected) console.error("No player state found for avatar", this);
|
45
|
+
else if (this.context.connection.isConnected) console.error("No player state found for avatar", this);
|
46
|
+
// don't destroy the avatar when entering XR and not connected to a networking backend
|
47
|
+
else if (playerstate && !this.context.connection.isConnected) playerstate.dontDestroy = true;
|
46
48
|
}
|
47
49
|
|
48
50
|
onLeaveXR(_args: NeedleXREventArgs): void {
|
@@ -182,6 +184,9 @@
|
|
182
184
|
this.head = new AssetReference("", this.sourceId, head);
|
183
185
|
if (debug) console.log("Create head", head);
|
184
186
|
}
|
187
|
+
else if (this.head instanceof Object3D) {
|
188
|
+
this.head = new AssetReference("", this.sourceId, this.head);
|
189
|
+
}
|
185
190
|
|
186
191
|
if (!this.rightHand) {
|
187
192
|
const rightHand = new Object3D();
|
@@ -190,6 +195,9 @@
|
|
190
195
|
this.rightHand = new AssetReference("", this.sourceId, rightHand);
|
191
196
|
if (debug) console.log("Create right hand", rightHand);
|
192
197
|
}
|
198
|
+
else if (this.rightHand instanceof Object3D) {
|
199
|
+
this.rightHand = new AssetReference("", this.sourceId, this.rightHand);
|
200
|
+
}
|
193
201
|
|
194
202
|
if (!this.leftHand) {
|
195
203
|
const leftHand = new Object3D();
|
@@ -198,6 +206,9 @@
|
|
198
206
|
this.leftHand = new AssetReference("", this.sourceId, leftHand);
|
199
207
|
if (debug) console.log("Create left hand", leftHand);
|
200
208
|
}
|
209
|
+
else if (this.leftHand instanceof Object3D) {
|
210
|
+
this.leftHand = new AssetReference("", this.sourceId, this.leftHand);
|
211
|
+
}
|
201
212
|
|
202
213
|
await this.loadAvatarObjects(this.head, this.leftHand, this.rightHand);
|
203
214
|
|
@@ -346,10 +346,15 @@
|
|
346
346
|
totalProgress01: this._loadingProgress01
|
347
347
|
};
|
348
348
|
const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
|
349
|
+
const displayNames = new Array<string>();
|
349
350
|
const args: ContextCreateArgs = {
|
350
351
|
files: filesToLoad,
|
351
352
|
onLoadingProgress: evt => {
|
352
|
-
|
353
|
+
const index = evt.index;
|
354
|
+
if (!displayNames[index] && evt.name) {
|
355
|
+
displayNames[index] = getDisplayName(evt.name);
|
356
|
+
}
|
357
|
+
evt.name = displayNames[index];
|
353
358
|
if (useDefaultLoading) this._loadingView?.onLoadingUpdate(evt);
|
354
359
|
progressEventDetail.name = evt.name;
|
355
360
|
progressEventDetail.progress = evt.progress;
|
@@ -500,7 +505,6 @@
|
|
500
505
|
onEnterAR(session: XRSession) {
|
501
506
|
this.onSetupAR();
|
502
507
|
const overlayContainer = this.getAROverlayContainer();
|
503
|
-
console.log("onEnterAR", session, overlayContainer);
|
504
508
|
this._overlay_ar.onBegin(this._context!, overlayContainer, session);
|
505
509
|
this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: this._context, htmlContainer: this._overlay_ar?.ARContainer } }));
|
506
510
|
}
|
@@ -615,12 +619,36 @@
|
|
615
619
|
return hash;
|
616
620
|
}
|
617
621
|
|
618
|
-
function
|
622
|
+
function getDisplayName(str: string) {
|
619
623
|
const parts = str.split("/");
|
620
624
|
let name = parts[parts.length - 1];
|
621
625
|
// Remove params
|
622
626
|
const index = name.indexOf("?")
|
623
627
|
if (index > 0)
|
624
628
|
name = name.substring(0, index);
|
625
|
-
|
629
|
+
const extension = name.split(".").pop();
|
630
|
+
if (extension === "glb" || extension === "gltf")
|
631
|
+
name = name.substring(0, name.length - 4);
|
632
|
+
name = decodeURIComponent(name);
|
633
|
+
if (name.length > 3) {
|
634
|
+
let displayName = "";
|
635
|
+
for (let i = 0; i < name.length; i++) {
|
636
|
+
let c = name[i];
|
637
|
+
if (c === ' ' && displayName.length <= 0) continue;
|
638
|
+
const isFirstCharacter = displayName.length === 0;
|
639
|
+
if (isFirstCharacter == false && c === c.toUpperCase()) {
|
640
|
+
displayName += " " + c;
|
641
|
+
}
|
642
|
+
else {
|
643
|
+
if (isFirstCharacter) {
|
644
|
+
c = c.toUpperCase();
|
645
|
+
}
|
646
|
+
displayName += c;
|
647
|
+
}
|
648
|
+
}
|
649
|
+
if (debug) console.log("displayName", name, displayName);
|
650
|
+
return displayName;
|
651
|
+
}
|
652
|
+
if (debug) console.log("displayName", name);
|
653
|
+
return name;
|
626
654
|
}
|
@@ -46,8 +46,8 @@
|
|
46
46
|
this.frame += 1;
|
47
47
|
this.time += this.deltaTime;
|
48
48
|
|
49
|
-
if (this._fpsSamples.length <
|
50
|
-
else this._fpsSamples[(this._fpsSampleIndex++) %
|
49
|
+
if (this._fpsSamples.length < 60) this._fpsSamples.push(this.deltaTime);
|
50
|
+
else this._fpsSamples[(this._fpsSampleIndex++) % 60] = this.deltaTime;
|
51
51
|
let sum = 0;
|
52
52
|
for (let i = 0; i < this._fpsSamples.length; i++)
|
53
53
|
sum += this._fpsSamples[i];
|
@@ -95,6 +95,8 @@
|
|
95
95
|
* You should usually use the `getButton` and `getStick` methods instead to get access to named buttons and sticks
|
96
96
|
*/
|
97
97
|
get gamepad() { return this.inputSource.gamepad; }
|
98
|
+
/** @returns true if this is a hand (otherwise this is a controller) */
|
99
|
+
get isHand() { return this.inputSource.hand != undefined; }
|
98
100
|
/**
|
99
101
|
* If this is a hand then this is the hand info (XRHand)
|
100
102
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRHand
|
@@ -138,6 +140,18 @@
|
|
138
140
|
return this.xr.getHitTest(this);
|
139
141
|
}
|
140
142
|
|
143
|
+
/** This is cleared at the beginning of each frame */
|
144
|
+
private readonly _handJointPoses: Map<XRJointSpace, XRJointPose> = new Map();
|
145
|
+
/** Get the hand joint pose from the current XRFrame. Results are cached for a frame to avoid calling getJointPose multiple times */
|
146
|
+
getHandJointPose(joint: XRJointSpace) {
|
147
|
+
if (!this.hand || !this.xr.frame?.getJointPose || !this.xr.referenceSpace) return null;
|
148
|
+
let pose = this._handJointPoses?.get(joint);
|
149
|
+
if (pose) return pose;
|
150
|
+
pose = this.xr.frame.getJointPose(joint, this.xr.referenceSpace);
|
151
|
+
if (pose) this._handJointPoses.set(joint, pose);
|
152
|
+
return pose;
|
153
|
+
}
|
154
|
+
|
141
155
|
private readonly _gripPosition = new Vector3();
|
142
156
|
private readonly _gripQuaternion = new Quaternion();
|
143
157
|
private readonly _linearVelocity: Vector3 = new Vector3();
|
@@ -274,6 +288,9 @@
|
|
274
288
|
}
|
275
289
|
|
276
290
|
private onUpdateFrame(frame: XRFrame) {
|
291
|
+
// make sure this is cleared every frame
|
292
|
+
this._handJointPoses.clear();
|
293
|
+
|
277
294
|
if (!this.xr.referenceSpace) {
|
278
295
|
this._isTracking = false;
|
279
296
|
return;
|
@@ -312,15 +329,13 @@
|
|
312
329
|
// TODO check why types are not correct here
|
313
330
|
// @ts-ignore
|
314
331
|
const wrist = hand.get("wrist");
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
this._object.quaternion.set(q.x, q.y, q.z, q.w).multiply(flipForwardQuaternion);
|
323
|
-
}
|
332
|
+
const writePose = wrist && this.getHandJointPose(wrist);
|
333
|
+
if (writePose) {
|
334
|
+
gotWrist = true;
|
335
|
+
const p = writePose.transform.position;
|
336
|
+
const q = writePose.transform.orientation;
|
337
|
+
this._object.position.set(p.x, p.y, p.z);
|
338
|
+
this._object.quaternion.set(q.x, q.y, q.z, q.w).multiply(flipForwardQuaternion);
|
324
339
|
}
|
325
340
|
if (!gotWrist) {
|
326
341
|
this._object.position.copy(this._rayPosition);
|
@@ -329,16 +344,14 @@
|
|
329
344
|
|
330
345
|
//@ts-ignore
|
331
346
|
const middle = hand.get("middle-finger-metacarpal");
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
this._gripQuaternion.set(q.x, q.y, q.z, q.w);
|
341
|
-
}
|
347
|
+
const middlePose = middle && this.getHandJointPose(middle);
|
348
|
+
if (middlePose) {
|
349
|
+
const p = middlePose.transform.position;
|
350
|
+
const q = middlePose.transform.orientation;
|
351
|
+
// for some reason the grip rotation is different from the wrist rotation
|
352
|
+
// but we want to use the wrist rotation for the grip
|
353
|
+
this._gripPosition.set(p.x, p.y, p.z);
|
354
|
+
this._gripQuaternion.set(q.x, q.y, q.z, q.w);
|
342
355
|
}
|
343
356
|
}
|
344
357
|
else {
|
@@ -687,11 +700,33 @@
|
|
687
700
|
}
|
688
701
|
|
689
702
|
|
703
|
+
private _didMoveLastFrame = false;
|
704
|
+
private readonly _lastPointerMovePosition = new Vector3();
|
705
|
+
private readonly _lastPointerMoveQuaternion = new Quaternion();
|
706
|
+
|
690
707
|
private onUpdateMove() {
|
691
|
-
let
|
692
|
-
|
693
|
-
|
694
|
-
|
708
|
+
let didMove = false;
|
709
|
+
const dist = this._lastPointerMovePosition.distanceTo(this.gripWorldPosition);
|
710
|
+
if (dist > .02) didMove = true;
|
711
|
+
if (!didMove) {
|
712
|
+
const angle = this._lastPointerMoveQuaternion.angleTo(this.gripWorldQuaternion);
|
713
|
+
if (angle > .02) didMove = true;
|
714
|
+
}
|
715
|
+
|
716
|
+
if (didMove) {
|
717
|
+
this._didMoveLastFrame = true;
|
718
|
+
this._lastPointerMovePosition.copy(this.gripWorldPosition);
|
719
|
+
this._lastPointerMoveQuaternion.copy(this.gripWorldQuaternion);
|
720
|
+
if (debug) Gizmos.DrawLabel(this.rayWorldPosition.add(this.object.worldForward.multiplyScalar(.1)), "move", .01);
|
721
|
+
|
722
|
+
let button = this.xr.context.input.getFirstPressedButtonForPointer(this.index);
|
723
|
+
if (button === undefined) button = 0;
|
724
|
+
const pressure = this.gamepad?.buttons[button]?.value;
|
725
|
+
this.emitPointerEvent("pointermove", button, "none", false, null, pressure);
|
726
|
+
}
|
727
|
+
else {
|
728
|
+
this._didMoveLastFrame = false;
|
729
|
+
}
|
695
730
|
}
|
696
731
|
|
697
732
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Camera,
|
1
|
+
import { Camera, Object3D, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
|
4
4
|
import { Context, FrameEvent } from "../engine_context.js";
|
@@ -6,7 +6,6 @@
|
|
6
6
|
import { isDestroyed } from "../engine_gameobject.js";
|
7
7
|
import { Gizmos } from "../engine_gizmos.js";
|
8
8
|
import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
|
9
|
-
import { Mathf } from "../engine_math.js";
|
10
9
|
import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
|
11
10
|
import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
|
12
11
|
import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
|
@@ -17,19 +16,24 @@
|
|
17
16
|
import { TemporaryXRContext } from "./TempXRContext.js";
|
18
17
|
import type { IXRRig } from "./XRRig.js";
|
19
18
|
|
19
|
+
|
20
|
+
/** @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame/fillPoses */
|
21
|
+
declare type FillPosesFunction = (spaces: IterableIterator<XRJointSpace>, referenceSpace: XRSpace, targetArray: Float32Array) => void;
|
22
|
+
declare type NeedleXRFrame = XRFrame & { fillPoses?: FillPosesFunction };
|
23
|
+
|
20
24
|
/** NeedleXRSession event argument.
|
21
25
|
* Use `args.xr` to access the NeedleXRSession */
|
22
|
-
export type NeedleXREventArgs = { xr: NeedleXRSession }
|
26
|
+
export type NeedleXREventArgs = { readonly xr: NeedleXRSession }
|
23
27
|
export type SessionChangedEvt = (args: NeedleXREventArgs) => void;
|
24
|
-
export type SessionRequestedEvent = (args: { mode: XRSessionMode, init: XRSessionInit }) => void;
|
25
|
-
export type SessionRequestedEndEvent = (args: { mode: XRSessionMode, init: XRSessionInit, newSession: XRSession | null }) => void;
|
28
|
+
export type SessionRequestedEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit }) => void;
|
29
|
+
export type SessionRequestedEndEvent = (args: { readonly mode: XRSessionMode, readonly init: XRSessionInit, newSession: XRSession | null }) => void;
|
26
30
|
|
27
31
|
/** Result of a XR hit-test
|
28
32
|
* @property {XRHitTestResult} hit The original XRHitTestResult
|
29
33
|
* @property {Vector3} position The hit position in world space
|
30
34
|
* @property {Quaternion} quaternion The hit rotation in world space
|
31
35
|
*/
|
32
|
-
export type NeedleXRHitTestResult = { hit: XRHitTestResult, position: Vector3, quaternion: Quaternion };
|
36
|
+
export type NeedleXRHitTestResult = { readonly hit: XRHitTestResult, readonly position: Vector3, readonly quaternion: Quaternion };
|
33
37
|
|
34
38
|
const debug = getParam("debugwebxr");
|
35
39
|
const debugFPS = getParam("stats");
|
@@ -203,17 +207,28 @@
|
|
203
207
|
}
|
204
208
|
private static readonly _sessionRequestEndListeners: SessionRequestedEndEvent[] = [];
|
205
209
|
|
206
|
-
/** Listen to XR session started */
|
207
|
-
static
|
210
|
+
/** Listen to XR session started. Unsubscribe with `offXRSessionStart` */
|
211
|
+
static onXRSessionStart(evt: SessionChangedEvt) {
|
208
212
|
this._xrStartListeners.push(evt);
|
209
213
|
};
|
210
214
|
/** Unsubscribe from XRSession started events */
|
211
|
-
static
|
215
|
+
static offXRSessionStart(evt: SessionChangedEvt) {
|
212
216
|
const index = this._xrStartListeners.indexOf(evt);
|
213
217
|
if (index >= 0) this._xrStartListeners.splice(index, 1);
|
214
218
|
}
|
215
219
|
private static readonly _xrStartListeners: SessionChangedEvt[] = [];
|
216
220
|
|
221
|
+
/** Listen to XR session ended. Unsubscribe with `offXRSessionEnd` */
|
222
|
+
static onXRSessionEnd(evt: SessionChangedEvt) {
|
223
|
+
this._xrEndListeners.push(evt);
|
224
|
+
};
|
225
|
+
/** Unsubscribe from XRSession started events */
|
226
|
+
static offXRSessionEnd(evt: SessionChangedEvt) {
|
227
|
+
const index = this._xrEndListeners.indexOf(evt);
|
228
|
+
if (index >= 0) this._xrEndListeners.splice(index, 1);
|
229
|
+
}
|
230
|
+
private static readonly _xrEndListeners: SessionChangedEvt[] = [];
|
231
|
+
|
217
232
|
/** Listen to controller added events.
|
218
233
|
* Events are cleared when starting a new session
|
219
234
|
**/
|
@@ -452,7 +467,7 @@
|
|
452
467
|
* The current XR frame
|
453
468
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRFrame
|
454
469
|
*/
|
455
|
-
get frame():
|
470
|
+
get frame(): NeedleXRFrame { return this.context.xrFrame! as NeedleXRFrame; }
|
456
471
|
|
457
472
|
/** The currently active/connected controllers */
|
458
473
|
readonly controllers: NeedleXRController[] = [];
|
@@ -820,6 +835,10 @@
|
|
820
835
|
this.context.renderer.xr.enabled = false;
|
821
836
|
this.context.mainCameraComponent?.applyClearFlags();
|
822
837
|
|
838
|
+
for (const listener of NeedleXRSession._xrEndListeners) {
|
839
|
+
listener({ xr: this });
|
840
|
+
}
|
841
|
+
|
823
842
|
// make sure we disconnect all controllers
|
824
843
|
for (let i = 0; i < this.controllers.length; i++) {
|
825
844
|
this.disconnectInputSource(this.controllers[i].inputSource);
|
@@ -898,15 +917,6 @@
|
|
898
917
|
this.updateActiveXRRig();
|
899
918
|
}
|
900
919
|
|
901
|
-
if ((debug || debugFPS) && this.rig) {
|
902
|
-
const pos = this.rig.gameObject.worldPosition;
|
903
|
-
const forward = this.rig.gameObject.worldForward;
|
904
|
-
pos.add(forward.multiplyScalar(1.5));
|
905
|
-
const upwards = this.rig.gameObject.worldUp;
|
906
|
-
pos.add(upwards.multiplyScalar(2.5));
|
907
|
-
Gizmos.DrawLabel(pos, this.context.time.smoothedFps.toFixed(1));
|
908
|
-
}
|
909
|
-
|
910
920
|
// make sure the camera is parented to the active rig
|
911
921
|
if (this.rig && this._mainCamera?.gameObject) {
|
912
922
|
const currentParent = this._mainCamera?.gameObject?.parent;
|
@@ -1037,11 +1047,30 @@
|
|
1037
1047
|
|
1038
1048
|
this.sync?.onUpdate(this);
|
1039
1049
|
|
1050
|
+
this.onRenderDebug();
|
1051
|
+
}
|
1052
|
+
|
1053
|
+
private onRenderDebug() {
|
1040
1054
|
if (debug) {
|
1041
1055
|
for (const controller of this.controllers) {
|
1042
1056
|
controller.onRenderDebug();
|
1043
1057
|
}
|
1044
1058
|
}
|
1059
|
+
if ((debug || debugFPS) && this.rig) {
|
1060
|
+
const pos = this.rig.gameObject.worldPosition;
|
1061
|
+
const forward = this.rig.gameObject.worldForward;
|
1062
|
+
pos.add(forward.multiplyScalar(1.5));
|
1063
|
+
const upwards = this.rig.gameObject.worldUp;
|
1064
|
+
pos.add(upwards.multiplyScalar(2.5));
|
1065
|
+
let debugLabel = "";
|
1066
|
+
debugLabel += this.context.time.smoothedFps.toFixed(1);
|
1067
|
+
if (debug) {
|
1068
|
+
for (const ctrl of this.controllers) {
|
1069
|
+
debugLabel += `\n${ctrl.hand ? "hand" : "ctrl"} ${ctrl.inputSource.handedness}[${ctrl.index}] con:${ctrl.connected} tr:${ctrl.isTracking}`;
|
1070
|
+
}
|
1071
|
+
}
|
1072
|
+
Gizmos.DrawLabel(pos, debugLabel);
|
1073
|
+
}
|
1045
1074
|
}
|
1046
1075
|
|
1047
1076
|
private onBeforeRender = () => {
|
@@ -180,6 +180,9 @@
|
|
180
180
|
@syncField(PlayerState.prototype.onOwnerChange)
|
181
181
|
owner?: string;
|
182
182
|
|
183
|
+
/** when enabled PlayerSync will not destroy itself when not connected anymore */
|
184
|
+
dontDestroy: boolean = false;
|
185
|
+
|
183
186
|
get isLocalPlayer(): boolean {
|
184
187
|
return this.owner === this.context.connection.connectionId;
|
185
188
|
}
|
@@ -267,8 +270,11 @@
|
|
267
270
|
// we could also do this in a timeout and check if the owner is still not assigned after a second (but that would be a hack)
|
268
271
|
setTimeout(() => {
|
269
272
|
if (!this.destroyed && !this.owner) {
|
270
|
-
if (
|
271
|
-
|
273
|
+
if (!this.dontDestroy) {
|
274
|
+
if (debug) console.warn(`PlayerState.start → owner is still undefined: destroying \"${this.name}\" instance now`);
|
275
|
+
this.doDestroy();
|
276
|
+
}
|
277
|
+
else if (debug) console.warn("PlayerState.start → owner is still undefined but dontDestroy is set to true", this.name);
|
272
278
|
}
|
273
279
|
else console.log("PlayerState.start → owner is assigned", this.owner);
|
274
280
|
}, 2000);
|
@@ -8,6 +8,7 @@
|
|
8
8
|
import { Behaviour, GameObject } from "../../Component.js";
|
9
9
|
import { Renderer } from "../../Renderer.js"
|
10
10
|
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
|
11
|
+
import { NeedleWebXRHtmlElement } from "../../webxr/WebXRButtons.js";
|
11
12
|
import { XRFlag, XRState, XRStateFlag } from "../../webxr/XRFlag.js";
|
12
13
|
import type { IUSDExporterExtension } from "./Extension.js";
|
13
14
|
import { AnimationExtension } from "./extensions/Animation.js"
|
@@ -76,6 +77,7 @@
|
|
76
77
|
extensions: IUSDExporterExtension[] = [];
|
77
78
|
|
78
79
|
private link!: HTMLAnchorElement;
|
80
|
+
private button?: HTMLButtonElement;
|
79
81
|
|
80
82
|
start() {
|
81
83
|
if (debug) {
|
@@ -113,6 +115,8 @@
|
|
113
115
|
const ios = isiOS()
|
114
116
|
const safari = isSafari();
|
115
117
|
if (debug || (ios && safari)) {
|
118
|
+
this.button = this.createQuicklookButton();
|
119
|
+
|
116
120
|
this.lastCallback = this.quicklookCallback.bind(this);
|
117
121
|
this.link = ensureQuicklookLinkIsCreated(this.context);
|
118
122
|
this.link.addEventListener('message', this.lastCallback);
|
@@ -124,6 +128,7 @@
|
|
124
128
|
}
|
125
129
|
|
126
130
|
onDisable() {
|
131
|
+
this.button?.remove();
|
127
132
|
this.link?.removeEventListener('message', this.lastCallback);
|
128
133
|
// const ios = isiOS()
|
129
134
|
// const safari = isSafari();
|
@@ -407,4 +412,12 @@
|
|
407
412
|
target.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
|
408
413
|
}
|
409
414
|
}
|
415
|
+
|
416
|
+
|
417
|
+
private createQuicklookButton() {
|
418
|
+
const buttoncontainer = NeedleWebXRHtmlElement.getOrCreate(this.context);
|
419
|
+
const button = buttoncontainer.createQuicklookButton();
|
420
|
+
if(!button.parentNode) buttoncontainer.appendChild(button);
|
421
|
+
return button;
|
422
|
+
}
|
410
423
|
}
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { AxesHelper, DoubleSide, Matrix4, Mesh, MeshBasicMaterial, Object3D, Plane, Quaternion, Raycaster, RingGeometry, Scene, Vector3 } from "three";
|
2
2
|
|
3
3
|
import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
|
4
|
+
import { AssetReference } from "../../engine/engine_addressables.js";
|
4
5
|
import { Context } from "../../engine/engine_context.js";
|
5
6
|
import { ObjectUtils, PrimitiveType } from "../../engine/engine_create_objects.js";
|
6
|
-
import { destroy } from "../../engine/engine_gameobject.js";
|
7
|
+
import { destroy, instantiate } from "../../engine/engine_gameobject.js";
|
7
8
|
import { NEPointerEvent } from "../../engine/engine_input.js";
|
8
9
|
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
9
10
|
import { IComponent, IGameObject } from "../../engine/engine_types.js";
|
@@ -40,6 +41,10 @@
|
|
40
41
|
@serializable()
|
41
42
|
invertForward: boolean = false;
|
42
43
|
|
44
|
+
/** When assigned this asset will be loaded and visualize the placement while in AR */
|
45
|
+
@serializable(AssetReference)
|
46
|
+
customReticle?: AssetReference;
|
47
|
+
|
43
48
|
/** When enabled we will create a XR anchor for the scene placement
|
44
49
|
* and make sure the scene is at that anchored point during a XR session */
|
45
50
|
@serializable()
|
@@ -77,6 +82,10 @@
|
|
77
82
|
/** user input is used for ar touch transform */
|
78
83
|
private userInput?: WebXRSessionRootUserInput;
|
79
84
|
|
85
|
+
onEnable(): void {
|
86
|
+
this.customReticle?.preload();
|
87
|
+
}
|
88
|
+
|
80
89
|
supportsXR(mode: XRSessionMode): boolean {
|
81
90
|
return mode === "immersive-ar";
|
82
91
|
}
|
@@ -216,17 +225,28 @@
|
|
216
225
|
|
217
226
|
let reticle = this._reticle[i];
|
218
227
|
if (!reticle) {
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
228
|
+
if (this.customReticle) {
|
229
|
+
if (this.customReticle.asset) {
|
230
|
+
reticle = instantiate(this.customReticle.asset);
|
231
|
+
}
|
232
|
+
else {
|
233
|
+
this.customReticle.loadAssetAsync();
|
234
|
+
return;
|
235
|
+
}
|
236
|
+
}
|
237
|
+
else {
|
238
|
+
reticle = new Mesh(
|
239
|
+
new RingGeometry(0.07, 0.09, 32).rotateX(- Math.PI / 2),
|
240
|
+
new MeshBasicMaterial({ side: DoubleSide })
|
241
|
+
) as any as IGameObject;
|
242
|
+
reticle.name = "AR Placement Reticle";
|
243
|
+
}
|
223
244
|
if (debug) {
|
224
245
|
const axes = new AxesHelper(1);
|
225
246
|
axes.position.y += .01;
|
226
247
|
reticle.add(axes);
|
227
248
|
}
|
228
249
|
this._reticle[i] = reticle;
|
229
|
-
reticle.name = "AR Placement Reticle";
|
230
250
|
reticle.matrixAutoUpdate = false;
|
231
251
|
reticle.visible = false;
|
232
252
|
}
|
@@ -264,8 +264,7 @@
|
|
264
264
|
* @returns the Needle WebXR button container */
|
265
265
|
getButtonsContainer(): NeedleWebXRHtmlElement {
|
266
266
|
if (!this._container) {
|
267
|
-
this._container = NeedleWebXRHtmlElement.
|
268
|
-
this.context.domElement.shadowRoot?.appendChild(this._container);
|
267
|
+
this._container = NeedleWebXRHtmlElement.getOrCreate(this.context);
|
269
268
|
}
|
270
269
|
return this._container;
|
271
270
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { isDevEnvironment } from "../../engine/debug/index.js";
|
2
|
+
import { Context } from "../../engine/engine_context.js";
|
2
3
|
import { generateQRCode } from "../../engine/engine_utils.js";
|
3
4
|
import { isMozillaXR } from "../../engine/engine_utils.js";
|
4
5
|
import { NeedleXRSession } from "../../engine/engine_xr.js";
|
@@ -15,6 +16,16 @@
|
|
15
16
|
return document.createElement(webXRElementName) as NeedleWebXRHtmlElement;
|
16
17
|
}
|
17
18
|
|
19
|
+
static getOrCreate(context: Context) {
|
20
|
+
const domElement = context.domElement;
|
21
|
+
let el = domElement.querySelector(webXRElementName);
|
22
|
+
if (!el) {
|
23
|
+
el = NeedleWebXRHtmlElement.create();
|
24
|
+
domElement.appendChild(el);
|
25
|
+
};
|
26
|
+
return el as NeedleWebXRHtmlElement;
|
27
|
+
}
|
28
|
+
|
18
29
|
private readonly root: HTMLElement;
|
19
30
|
|
20
31
|
constructor() {
|
@@ -310,6 +321,14 @@
|
|
310
321
|
button.disabled = button["was-disabled"];
|
311
322
|
// button.innerText = button["original-text"];
|
312
323
|
});
|
324
|
+
NeedleXRSession.onXRSessionStart(_ => {
|
325
|
+
button["previous-display"] = button.style.display;
|
326
|
+
button.style.display = "none";
|
327
|
+
});
|
328
|
+
NeedleXRSession.onXRSessionEnd(_ => {
|
329
|
+
if (button["previous-display"] != undefined)
|
330
|
+
button.style.display = button["previous-display"];
|
331
|
+
});
|
313
332
|
}
|
314
333
|
}
|
315
334
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AxesHelper, Group, Material, Mesh, Object3D } from "three";
|
1
|
+
import { AxesHelper, Group, Material, Mesh, Object3D, XRHandSpace } from "three";
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
3
3
|
import { XRControllerModelFactory } from "three/examples/jsm/webxr/XRControllerModelFactory.js";
|
4
4
|
import { XRHandMeshModel } from "three/examples/jsm/webxr/XRHandMeshModel.js";
|
@@ -16,6 +16,9 @@
|
|
16
16
|
|
17
17
|
const debug = getParam("debugwebxr");
|
18
18
|
|
19
|
+
const handsJointBuffer = new Float32Array(16 * 25);
|
20
|
+
const renderingUpdateTimings = new Array<number>();
|
21
|
+
|
19
22
|
export class XRControllerModel extends Behaviour {
|
20
23
|
|
21
24
|
@serializable()
|
@@ -75,8 +78,7 @@
|
|
75
78
|
this.scene.add(model);
|
76
79
|
// The controller mesh should by default inherit layers.
|
77
80
|
model.traverse(child => {
|
78
|
-
child.layers.
|
79
|
-
child.layers.enable(2);
|
81
|
+
child.layers.set(2);
|
80
82
|
});
|
81
83
|
}
|
82
84
|
else {
|
@@ -103,8 +105,31 @@
|
|
103
105
|
onBeforeRender() {
|
104
106
|
if (!NeedleXRSession.active) return;
|
105
107
|
|
106
|
-
|
108
|
+
if (debug) renderingUpdateTimings[0] = Date.now();
|
109
|
+
// update model
|
110
|
+
this.updateRendering(NeedleXRSession.active);
|
107
111
|
|
112
|
+
if (debug) {
|
113
|
+
const dt = Date.now() - renderingUpdateTimings[0];
|
114
|
+
renderingUpdateTimings.push(dt);
|
115
|
+
if (renderingUpdateTimings.length >= 30) {
|
116
|
+
renderingUpdateTimings[0] = 0;
|
117
|
+
const avrg = renderingUpdateTimings.reduce((a, b) => a + b, 0) / renderingUpdateTimings.length;
|
118
|
+
renderingUpdateTimings.length = 0;
|
119
|
+
console.log("[XRControllerModel] " + avrg.toFixed(2) + " ms");
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
onLeaveXR(_args: NeedleXREventArgs): void {
|
124
|
+
for (const entry of this._models) {
|
125
|
+
if (!entry) continue;
|
126
|
+
entry.model?.removeFromParent();
|
127
|
+
}
|
128
|
+
this._models = [];
|
129
|
+
}
|
130
|
+
|
131
|
+
private updateRendering(xr: NeedleXRSession) {
|
132
|
+
|
108
133
|
for (let i = 0; i < this._models.length; i++) {
|
109
134
|
const entry = this._models[i];
|
110
135
|
if (!entry) continue;
|
@@ -130,19 +155,35 @@
|
|
130
155
|
else if (ctrl.inputSource.hand && entry.handmesh) {
|
131
156
|
const referenceSpace = xr.referenceSpace;
|
132
157
|
const hand = this.context.renderer.xr.getHand(ctrl.index);
|
158
|
+
// if (referenceSpace && xr.frame.fillPoses) {
|
159
|
+
// xr.frame.fillPoses(ctrl.inputSource.hand.values(), referenceSpace, handsJointBuffer);
|
160
|
+
// let j = 0;
|
161
|
+
// for (const space of ctrl.inputSource.hand.values()) {
|
162
|
+
// const joint = hand.joints[space.jointName];
|
163
|
+
// if (joint) {
|
164
|
+
// joint.matrix.fromArray(handsJointBuffer, j * 16);
|
165
|
+
// joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
|
166
|
+
// joint.visible = true;
|
167
|
+
// }
|
168
|
+
// j++;
|
169
|
+
// }
|
170
|
+
// }
|
171
|
+
// else
|
133
172
|
if (referenceSpace && xr.frame.getJointPose) {
|
134
173
|
for (const inputjoint of ctrl.inputSource.hand.values()) {
|
135
|
-
// Update the joints groups with the XRJoint poses
|
136
|
-
const jointPose = xr.frame.getJointPose(inputjoint, referenceSpace);
|
137
174
|
// The transform of this joint will be updated with the joint pose on each frame
|
138
175
|
const joint = hand.joints[inputjoint.jointName];
|
139
176
|
if (joint) {
|
177
|
+
// Update the joints groups with the XRJoint poses
|
178
|
+
const jointPose = ctrl.getHandJointPose(inputjoint);
|
140
179
|
if (jointPose) {
|
180
|
+
// joint.matrixAutoUpdate = false;
|
181
|
+
// joint.matrix.fromArray(jointPose.transform.matrix);
|
182
|
+
// joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
|
141
183
|
const { position, quaternion } = xr.convertSpace(jointPose.transform);
|
142
184
|
joint.position.copy(position);
|
143
185
|
joint.quaternion.copy(quaternion);
|
144
|
-
joint.
|
145
|
-
// joint.jointRadius = jointPose.radius;
|
186
|
+
joint.matrixWorldAutoUpdate = false;
|
146
187
|
}
|
147
188
|
joint.visible = jointPose != null;
|
148
189
|
}
|
@@ -150,24 +191,17 @@
|
|
150
191
|
// ensure that the hand renders in rig space
|
151
192
|
if (entry.model) {
|
152
193
|
entry.model.visible = ctrl.isTracking;
|
153
|
-
if (entry.model.parent !== xr.rig?.gameObject) {
|
154
|
-
entry.model.position.set(0, 0, 0);
|
194
|
+
if (entry.model.visible && entry.model.parent !== xr.rig?.gameObject) {
|
155
195
|
xr.rig?.gameObject.add(entry.model);
|
156
196
|
}
|
197
|
+
entry.model.position.set(0, 0, 0);
|
157
198
|
}
|
158
199
|
|
159
|
-
entry.handmesh?.updateMesh();
|
200
|
+
if (entry.model?.visible) entry.handmesh?.updateMesh();
|
160
201
|
}
|
161
202
|
}
|
162
203
|
}
|
163
204
|
}
|
164
|
-
onLeaveXR(_args: NeedleXREventArgs): void {
|
165
|
-
for (const entry of this._models) {
|
166
|
-
if (!entry) continue;
|
167
|
-
entry.model?.removeFromParent();
|
168
|
-
}
|
169
|
-
this._models = [];
|
170
|
-
}
|
171
205
|
|
172
206
|
protected async loadModel(controller: NeedleXRController, url: string): Promise<IGameObject | null> {
|
173
207
|
if (!controller.connected) {
|
@@ -210,7 +244,7 @@
|
|
210
244
|
else {
|
211
245
|
const basePath = customHand.uri.substring(0, customHand.uri.indexOf(expectedHandModelName));
|
212
246
|
loader.setPath(basePath);
|
213
|
-
if(debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
|
247
|
+
if (debug) console.log("XRControllerModel: loading custom hand model from " + basePath);
|
214
248
|
}
|
215
249
|
}
|
216
250
|
|
@@ -221,13 +255,12 @@
|
|
221
255
|
const handmesh = new XRHandMeshModel(handObject, hand, loader.path, controller.inputSource.handedness, loader, (object: Object3D) => {
|
222
256
|
// The hand mesh should not receive raycasts
|
223
257
|
object.traverseVisible(child => {
|
224
|
-
child.layers.
|
225
|
-
child.layers.enable(2);
|
258
|
+
child.layers.set(2);
|
226
259
|
if (NeedleXRSession.active?.isPassThrough)
|
227
260
|
this.makeOccluder(child);
|
228
261
|
});
|
229
262
|
if (!controller.connected) {
|
230
|
-
if(debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
|
263
|
+
if (debug) Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
|
231
264
|
object.removeFromParent();
|
232
265
|
}
|
233
266
|
});
|
@@ -250,7 +283,7 @@
|
|
250
283
|
}
|
251
284
|
}
|
252
285
|
else {
|
253
|
-
if(debug) {
|
286
|
+
if (debug) {
|
254
287
|
Gizmos.DrawLabel(controller.rayWorldPosition, "No inputSource.hand found for index " + controller.index, .05, 5);
|
255
288
|
}
|
256
289
|
}
|
@@ -204,16 +204,29 @@
|
|
204
204
|
for (let i = 0; i < session.controllers.length; i++) {
|
205
205
|
const ctrl = session.controllers[i];
|
206
206
|
if (!ctrl.connected || !ctrl.isTracking) continue;
|
207
|
+
|
208
|
+
// save performance by only raycasting every nth frame
|
209
|
+
if (this.context.time.frame % 2 !== 0) {
|
210
|
+
const disc = this._hitDiscs[i];
|
211
|
+
// if the disc had a hit last frame, we can show it again
|
212
|
+
if (disc && disc["hit"]) disc.visible = true;
|
213
|
+
continue;
|
214
|
+
}
|
215
|
+
|
207
216
|
const hit = this.context.physics.raycastFromRay(ctrl.ray, { testObject: this.hitPointRaycastFilter })[0];
|
208
217
|
this._hitDistances[i] = hit?.distance;
|
218
|
+
|
219
|
+
let disc = this._hitDiscs[i];
|
220
|
+
if (disc) // save the hit object on the disc
|
221
|
+
disc["hit"] = hit;
|
222
|
+
|
209
223
|
if (hit) {
|
210
224
|
const rigScale = (session.rigScale ?? 1);
|
211
225
|
if (debug) {
|
212
|
-
Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000
|
226
|
+
Gizmos.DrawWireSphere(hit.point, .025 * rigScale, 0xff0000);
|
213
227
|
Gizmos.DrawLabel(getTempVector(0, .2, 0).add(hit.point), hit.object.name, .02, 0);
|
214
228
|
}
|
215
229
|
|
216
|
-
let disc = this._hitDiscs[i];
|
217
230
|
if (!disc) {
|
218
231
|
disc = this.createHitPointObject();
|
219
232
|
this._hitDiscs[i] = disc;
|
@@ -223,6 +236,7 @@
|
|
223
236
|
disc.scale.set(size, size, size);
|
224
237
|
disc.layers.disableAll();
|
225
238
|
disc.layers.enable(2);
|
239
|
+
disc["hit"] = hit;
|
226
240
|
|
227
241
|
if (hit.normal) {
|
228
242
|
const factor = 0.02 * rigScale;
|