@@ -281,7 +281,10 @@
|
|
281
281
|
this.dict.set(animationTarget, []);
|
282
282
|
}
|
283
283
|
const transformDataForTarget = this.dict.get(animationTarget);
|
284
|
-
if (!transformDataForTarget)
|
284
|
+
if (!transformDataForTarget) {
|
285
|
+
console.warn("no transform data found for target ", animationTarget, "at slot " + currentCount + ", this is likely a bug");
|
286
|
+
continue;
|
287
|
+
}
|
285
288
|
|
286
289
|
// this node has animation data for this clip – no need for additional padding
|
287
290
|
unregisteredNodesForThisClip.delete(animationTarget);
|
@@ -332,7 +335,7 @@
|
|
332
335
|
|
333
336
|
let model = transformDataForTarget[currentCount];
|
334
337
|
if (!model) {
|
335
|
-
if (debug) console.log("Adding padding clip for ", target, clip);
|
338
|
+
if (debug) console.log("Adding padding clip for ", target, clip, "at slot", currentCount);
|
336
339
|
model = new TransformData(root, target, clip);
|
337
340
|
transformDataForTarget[currentCount] = model;
|
338
341
|
}
|
@@ -471,20 +474,30 @@
|
|
471
474
|
|
472
475
|
// We should have a proper rectangular array,
|
473
476
|
// Where for each bone we have the same number of TransformData entries.
|
477
|
+
let numberOfEntries: number | undefined = undefined;
|
474
478
|
let allBonesHaveSameNumberOfTransformDataEntries = true;
|
475
|
-
|
479
|
+
const undefinedBoneEntries = new Map<Object3D, Array<number>>();
|
476
480
|
for (const [bone, transformDatas] of boneToTransformData) {
|
477
481
|
if (numberOfEntries === undefined) numberOfEntries = transformDatas.length;
|
478
482
|
if (numberOfEntries !== transformDatas.length) {
|
479
483
|
allBonesHaveSameNumberOfTransformDataEntries = false;
|
480
|
-
break;
|
481
484
|
}
|
485
|
+
let index = 0;
|
486
|
+
for (const transformData of transformDatas) {
|
487
|
+
index++;
|
488
|
+
if (!transformData) {
|
489
|
+
if (!undefinedBoneEntries.has(bone)) undefinedBoneEntries.set(bone, []);
|
490
|
+
undefinedBoneEntries.get(bone)!.push(index);
|
491
|
+
}
|
492
|
+
}
|
482
493
|
}
|
483
494
|
|
484
495
|
// TODO not working yet for multiple skinned characters at the same time
|
485
|
-
if (debug)
|
496
|
+
if (debug) {
|
497
|
+
console.log("Bone count: ", boneToTransformData.size, "TransformData entries per bone: ", numberOfEntries, "Undefined bone entries: ", undefinedBoneEntries);
|
498
|
+
}
|
486
499
|
console.assert(allBonesHaveSameNumberOfTransformDataEntries, "All bones should have the same number of TransformData entries", boneToTransformData);
|
487
|
-
|
500
|
+
console.assert(undefinedBoneEntries.size === 0, "All TransformData entries should be set", undefinedBoneEntries);
|
488
501
|
const times: AnimationClipFrameTimes[] = [];
|
489
502
|
|
490
503
|
for (const [bone, transformDatas] of boneToTransformData) {
|
@@ -731,6 +744,17 @@
|
|
731
744
|
onSerialize(writer, _context) {
|
732
745
|
if (!this.model) return;
|
733
746
|
|
747
|
+
// Workaround: Sanitize TransformData for this object.
|
748
|
+
// This works around an issue with wrongly detected animation roots, where some of the indices
|
749
|
+
// in the TransformData array are not property set. Reproduces with golem_yokai.glb
|
750
|
+
const arr0 = this.dict.get(this.object);
|
751
|
+
if (arr0) {
|
752
|
+
for (let i = 0; i < arr0.length; i++) {
|
753
|
+
if (arr0[i] !== undefined) continue;
|
754
|
+
arr0[i] = new TransformData(null, this.object, undefined);
|
755
|
+
}
|
756
|
+
}
|
757
|
+
|
734
758
|
this.skinnedMeshExport(writer, _context);
|
735
759
|
|
736
760
|
const object = this.object;
|
@@ -4,7 +4,7 @@
|
|
4
4
|
import { Animation } from "../../../Animation.js";
|
5
5
|
import { Animator } from "../../../Animator.js";
|
6
6
|
import { AudioSource } from "../../../AudioSource.js";
|
7
|
-
import {
|
7
|
+
import { GameObject } from "../../../Component.js";
|
8
8
|
import type { AnimationExtension } from "../extensions/Animation.js";
|
9
9
|
import type { AudioExtension } from "../extensions/behavior/AudioExtension.js";
|
10
10
|
import { PlayAnimationOnClick, PlayAudioOnClick } from "../extensions/behavior/BehaviourComponents.js";
|
@@ -31,7 +31,6 @@
|
|
31
31
|
|
32
32
|
if (ext.injectImplicitBehaviours) {
|
33
33
|
// We're registering animators with implicit PlayAnimationOnClick (with hacked "start" trigger) here.
|
34
|
-
// TODO need to remove these extra components again after export.
|
35
34
|
for (const animator of animators) {
|
36
35
|
if (!animator || !animator.runtimeAnimatorController) continue;
|
37
36
|
const activeState = animator.runtimeAnimatorController.activeState;
|
@@ -55,6 +54,9 @@
|
|
55
54
|
// the behaviour can be anywhere in the hierarchy
|
56
55
|
root.add(go);
|
57
56
|
}
|
57
|
+
|
58
|
+
// TODO add Animation handling, otherwise multi-animation files
|
59
|
+
// directly loaded from GLB may glitch due to the added rest poses
|
58
60
|
}
|
59
61
|
else {
|
60
62
|
for (const animator of animators) {
|
@@ -2,7 +2,7 @@
|
|
2
2
|
import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
|
3
3
|
|
4
4
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
|
-
import { ApplicationEvents } from "../engine/engine_application.js";
|
5
|
+
import { Application, ApplicationEvents } from "../engine/engine_application.js";
|
6
6
|
import { Mathf } from "../engine/engine_math.js";
|
7
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
8
|
import { getTempVector } from "../engine/engine_three_utils.js";
|
@@ -29,36 +29,15 @@
|
|
29
29
|
}
|
30
30
|
|
31
31
|
|
32
|
-
let userInteractionRegistered = false;
|
33
|
-
const userInteractionCallbacks: Function[] = [];
|
34
|
-
function onUserInteraction() {
|
35
|
-
if(userInteractionRegistered) return;
|
36
|
-
if(isDevEnvironment()) console.log("User interaction registered: audio can now be played");
|
37
|
-
userInteractionRegistered = true;
|
38
|
-
const copy = [...userInteractionCallbacks];
|
39
|
-
userInteractionCallbacks.length = 0;
|
40
|
-
copy.forEach(cb => cb());
|
41
|
-
}
|
42
|
-
document.addEventListener('pointerdown', onUserInteraction);
|
43
|
-
document.addEventListener('click', onUserInteraction);
|
44
|
-
document.addEventListener('dragstart', onUserInteraction);
|
45
|
-
document.addEventListener('touchstart', onUserInteraction);
|
46
32
|
|
47
33
|
export class AudioSource extends Behaviour {
|
48
34
|
|
49
35
|
public static get userInteractionRegistered(): boolean {
|
50
|
-
return userInteractionRegistered;
|
36
|
+
return Application.userInteractionRegistered;
|
51
37
|
}
|
52
38
|
|
53
39
|
public static registerWaitForAllowAudio(cb: Function) {
|
54
|
-
|
55
|
-
if (userInteractionRegistered) {
|
56
|
-
cb();
|
57
|
-
return;
|
58
|
-
}
|
59
|
-
if (userInteractionCallbacks.indexOf(cb) === -1)
|
60
|
-
userInteractionCallbacks.push(cb);
|
61
|
-
}
|
40
|
+
Application.registerWaitForAllowAudio(cb);
|
62
41
|
}
|
63
42
|
|
64
43
|
@serializable(URL)
|
@@ -55,7 +55,9 @@
|
|
55
55
|
}
|
56
56
|
|
57
57
|
private readonly targetObject = new Object3D();
|
58
|
-
|
58
|
+
/** this is a point in forward view of the user */
|
59
|
+
private readonly userForwardViewPoint = new Vector3();
|
60
|
+
private readonly oneEuroFilter = new OneEuroFilterXYZ(90, .2);
|
59
61
|
private _lastElementRemoveTime = 0;
|
60
62
|
|
61
63
|
private onBeforeRender = () => {
|
@@ -66,31 +68,40 @@
|
|
66
68
|
// TODO: need to figure out why this happens when entering VR (in the simulator at least)
|
67
69
|
if (Number.isNaN(root.position.x))
|
68
70
|
root.position.set(0, 0, 0);
|
71
|
+
if (Number.isNaN(root.quaternion.x))
|
72
|
+
root.quaternion.set(0, 0, 0, 1);
|
69
73
|
|
70
74
|
this.context.scene.add(this.targetObject);
|
71
75
|
|
72
|
-
const dist =
|
76
|
+
const dist = 3.5;
|
73
77
|
const forward = cam.worldForward;
|
74
78
|
forward.y = 0;
|
75
79
|
forward.normalize().multiplyScalar((dist * 100) / cam.fov);
|
76
|
-
this.
|
77
|
-
lookAtObject(this.targetObject, cam, false, true);
|
80
|
+
this.userForwardViewPoint.copy(cam.worldPosition).sub(forward);
|
78
81
|
|
82
|
+
const distFromForwardView = this.targetObject.position.distanceTo(this.userForwardViewPoint);
|
83
|
+
if (distFromForwardView > 2) {
|
84
|
+
this.targetObject.position.copy(this.userForwardViewPoint);
|
85
|
+
lookAtObject(this.targetObject, cam, true, true);
|
86
|
+
this.targetObject.rotateY(Math.PI);
|
87
|
+
}
|
88
|
+
|
79
89
|
this.oneEuroFilter.filter(this.targetObject.position, root.position, this.context.time.time);
|
80
|
-
const step = this.context.time.deltaTime
|
90
|
+
const step = this.context.time.deltaTime;
|
81
91
|
root.quaternion.slerp(this.targetObject.quaternion, step);
|
82
92
|
|
83
93
|
this.targetObject.removeFromParent();
|
84
94
|
this.context.scene.add(this.getRoot() as any);
|
85
95
|
|
86
|
-
if (this.context.time.time - this._lastElementRemoveTime > .
|
96
|
+
if (this.context.time.time - this._lastElementRemoveTime > .1) {
|
87
97
|
this._lastElementRemoveTime = this.context.time.time;
|
88
98
|
const now = Date.now();
|
89
|
-
for (let i = 0; i <
|
90
|
-
const el =
|
91
|
-
if (el instanceof ThreeMeshUI.Text && now - el["_activatedTime"] >
|
99
|
+
for (let i = 0; i < this._activeTexts.length; i++) {
|
100
|
+
const el = this._activeTexts[i];
|
101
|
+
if (el instanceof ThreeMeshUI.Text && now - el["_activatedTime"] > 20000) {
|
92
102
|
el.removeFromParent();
|
93
103
|
this._textBuffer.push(el);
|
104
|
+
this._activeTexts.splice(i, 1);
|
94
105
|
break;
|
95
106
|
}
|
96
107
|
}
|
@@ -121,19 +132,19 @@
|
|
121
132
|
break;
|
122
133
|
}
|
123
134
|
|
124
|
-
text.set({
|
125
|
-
backgroundColor: backgroundColor,
|
126
|
-
color: fontColor,
|
127
|
-
});
|
128
|
-
|
129
135
|
if (message.length > 1000) message = message.substring(0, 1000) + "...";
|
130
136
|
|
131
|
-
const
|
132
|
-
text.textContent = "[" +
|
137
|
+
const minuteSecondMilliSecond = new Date().toISOString().split("T")[1].split(".")[0];
|
138
|
+
text.textContent = "[" + minuteSecondMilliSecond + "] " + message;
|
133
139
|
text.visible = true;
|
134
140
|
text["_activatedTime"] = Date.now();
|
135
141
|
root.add(text as any);
|
142
|
+
this._activeTexts.push(text);
|
136
143
|
if (this.context) this.context.scene.add(root as any);
|
144
|
+
text.set({
|
145
|
+
backgroundColor: backgroundColor,
|
146
|
+
color: fontColor,
|
147
|
+
});
|
137
148
|
|
138
149
|
ThreeMeshUI.update();
|
139
150
|
}
|
@@ -162,6 +173,7 @@
|
|
162
173
|
borderRadius: .03,
|
163
174
|
};
|
164
175
|
private readonly _textBuffer: ThreeMeshUI.Text[] = [];
|
176
|
+
private readonly _activeTexts: ThreeMeshUI.Text[] = [];
|
165
177
|
private getText() {
|
166
178
|
const root = this.getRoot();
|
167
179
|
if (this._textBuffer.length > 0) {
|
@@ -171,7 +183,8 @@
|
|
171
183
|
return text;
|
172
184
|
}
|
173
185
|
if (root.children.length > 20) {
|
174
|
-
|
186
|
+
const active = this._activeTexts[0];
|
187
|
+
return active;
|
175
188
|
}
|
176
189
|
const newText = new ThreeMeshUI.Text(this.textOptions);
|
177
190
|
setTimeout(() => this.disableDepthTestRecursive(newText as any), 500);
|
@@ -1,4 +1,6 @@
|
|
1
|
+
import { isDevEnvironment } from "./debug/debug.js";
|
1
2
|
import { Context } from "./engine_setup.js";
|
3
|
+
import { NeedleXRSession } from "./engine_xr.js";
|
2
4
|
|
3
5
|
export enum ApplicationEvents {
|
4
6
|
Visible = "application-visible",
|
@@ -6,8 +8,41 @@
|
|
6
8
|
MuteChanged = "application-mutechanged",
|
7
9
|
}
|
8
10
|
|
11
|
+
let userInteractionRegistered = false;
|
12
|
+
const userInteractionCallbacks: Function[] = [];
|
13
|
+
function onUserInteraction() {
|
14
|
+
if (userInteractionRegistered) return;
|
15
|
+
if (isDevEnvironment()) console.log("User interaction registered: audio can now be played");
|
16
|
+
userInteractionRegistered = true;
|
17
|
+
const copy = [...userInteractionCallbacks];
|
18
|
+
userInteractionCallbacks.length = 0;
|
19
|
+
copy.forEach(cb => cb());
|
20
|
+
}
|
21
|
+
document.addEventListener('pointerdown', onUserInteraction);
|
22
|
+
document.addEventListener('click', onUserInteraction);
|
23
|
+
document.addEventListener('dragstart', onUserInteraction);
|
24
|
+
document.addEventListener('touchstart', onUserInteraction);
|
25
|
+
NeedleXRSession.onXRSessionStart(() => {
|
26
|
+
onUserInteraction();
|
27
|
+
})
|
28
|
+
|
9
29
|
export class Application extends EventTarget {
|
10
30
|
|
31
|
+
public static get userInteractionRegistered(): boolean {
|
32
|
+
return userInteractionRegistered;
|
33
|
+
}
|
34
|
+
|
35
|
+
public static registerWaitForAllowAudio(cb: Function) {
|
36
|
+
if (cb !== null) {
|
37
|
+
if (userInteractionRegistered) {
|
38
|
+
cb();
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
if (userInteractionCallbacks.indexOf(cb) === -1)
|
42
|
+
userInteractionCallbacks.push(cb);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
11
46
|
private _mute: boolean = false;
|
12
47
|
/** audio muted? */
|
13
48
|
get muted() { return this._mute; }
|
@@ -33,7 +68,6 @@
|
|
33
68
|
constructor(context: Context) {
|
34
69
|
super();
|
35
70
|
this.context = context;
|
36
|
-
// console.log("APP");
|
37
71
|
window.addEventListener("visibilitychange", this.onVisiblityChanged.bind(this), false);
|
38
72
|
}
|
39
73
|
|
@@ -178,7 +178,8 @@
|
|
178
178
|
this._loadingElement = existing || document.createElement("div");
|
179
179
|
|
180
180
|
let loadingStyle: LoadingStyleOption = this._element.getAttribute("loading-style") as LoadingStyleOption;
|
181
|
-
if
|
181
|
+
// if nothing is defined OR loadingStyle is set to auto
|
182
|
+
if (!loadingStyle || loadingStyle === "auto") {
|
182
183
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches)
|
183
184
|
loadingStyle = "dark";
|
184
185
|
else
|
@@ -205,9 +206,9 @@
|
|
205
206
|
this._loadingElement.style.color = "white";
|
206
207
|
this._loadingElement.style.fontFamily = "Roboto, sans-serif, Arial";
|
207
208
|
if (loadingStyle === "light")
|
208
|
-
this._loadingElement.style.color = "rgba(0,0,0,.
|
209
|
+
this._loadingElement.style.color = "rgba(0,0,0,.6)";
|
209
210
|
else
|
210
|
-
this._loadingElement.style.color = "rgba(255,255,255,.
|
211
|
+
this._loadingElement.style.color = "rgba(255,255,255,.3)";
|
211
212
|
if (hasLicense && this._element) {
|
212
213
|
const loadingBackgroundColor = this._element.getAttribute("loading-background-color");
|
213
214
|
if (loadingBackgroundColor) {
|
@@ -228,46 +229,21 @@
|
|
228
229
|
}
|
229
230
|
}
|
230
231
|
|
231
|
-
const
|
232
|
-
|
233
|
-
display: flex;
|
234
|
-
flex-direction: column;
|
235
|
-
align-items: center;
|
236
|
-
justify-content: center;
|
237
|
-
width: 100%;
|
238
|
-
opacity: 0;
|
239
|
-
transition: opacity 1.2s ease-in-out .2s;
|
240
|
-
`;
|
241
|
-
setTimeout(() => { container.style.opacity = "1"; }, 1);
|
242
|
-
this._loadingElement.appendChild(container);
|
232
|
+
const content = document.createElement("div");
|
233
|
+
this._loadingElement.appendChild(content);
|
243
234
|
|
244
|
-
const loadingBarContainer = document.createElement("div");
|
245
|
-
const maxWidth = 30;
|
246
|
-
loadingBarContainer.style.display = "flex";
|
247
|
-
loadingBarContainer.style.width = maxWidth + "%";
|
248
|
-
loadingBarContainer.style.height = "3px";
|
249
|
-
if (loadingStyle === "light")
|
250
|
-
loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
|
251
|
-
else
|
252
|
-
loadingBarContainer.style.backgroundColor = "rgba(255,255,255,.2)"
|
253
|
-
// loadingBarContainer.style.alignItems = "center";
|
254
|
-
|
255
235
|
const logo = document.createElement("img");
|
256
|
-
const logoSize =
|
236
|
+
const logoSize = 120;
|
257
237
|
logo.style.width = `${logoSize}px`;
|
258
238
|
logo.style.height = `${logoSize}px`;
|
259
|
-
logo.style.marginBottom = "
|
239
|
+
logo.style.marginBottom = "10px";
|
260
240
|
logo.style.userSelect = "none";
|
261
241
|
logo.style.objectFit = "contain";
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
logo.style.
|
266
|
-
|
267
|
-
logo.style.transform = "translateY(0px)";
|
268
|
-
logo.style.opacity = "1";
|
269
|
-
}, 1);
|
270
|
-
}
|
242
|
+
logo.style.transition = "transform 2s ease-out, opacity 1s ease-in-out";
|
243
|
+
logo.style.transform = "translateY(10px)";
|
244
|
+
setTimeout(() => {
|
245
|
+
logo.style.transform = "translateY(0px)";
|
246
|
+
}, 1);
|
271
247
|
logo.src = logoSVG;
|
272
248
|
let isUsingCustomLogo = false;
|
273
249
|
if (hasLicense && this._element) {
|
@@ -282,17 +258,48 @@
|
|
282
258
|
logo.style.pointerEvents = "all";
|
283
259
|
logo.addEventListener("click", () => window.open("https://needle.tools", "_blank"));
|
284
260
|
}
|
285
|
-
|
286
|
-
container.appendChild(loadingBarContainer);
|
261
|
+
content.appendChild(logo);
|
287
262
|
|
263
|
+
const details = document.createElement("div");
|
264
|
+
details.style.cssText = `
|
265
|
+
display: flex;
|
266
|
+
flex-direction: column;
|
267
|
+
align-items: center;
|
268
|
+
justify-content: center;
|
269
|
+
width: 100%;
|
270
|
+
opacity: 0;
|
271
|
+
transition: opacity 1s ease-in-out 4s;
|
272
|
+
`;
|
273
|
+
setTimeout(() => { details.style.opacity = "1"; }, 1);
|
274
|
+
this._loadingElement.appendChild(details);
|
288
275
|
|
276
|
+
const loadingBarContainer = document.createElement("div");
|
277
|
+
const maxWidth = 100;
|
278
|
+
loadingBarContainer.style.display = "flex";
|
279
|
+
loadingBarContainer.style.width = maxWidth + "%";
|
280
|
+
loadingBarContainer.style.height = "3px";
|
281
|
+
loadingBarContainer.style.position = "absolute";
|
282
|
+
loadingBarContainer.style.left = "0";
|
283
|
+
loadingBarContainer.style.bottom = "0px";
|
284
|
+
loadingBarContainer.style.opacity = "0";
|
285
|
+
loadingBarContainer.style.transition = "opacity 1s ease-in-out 2s";
|
286
|
+
setTimeout(() => { loadingBarContainer.style.opacity = "1"; }, 1);
|
287
|
+
if (loadingStyle === "light")
|
288
|
+
loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)"
|
289
|
+
else
|
290
|
+
loadingBarContainer.style.backgroundColor = "rgba(255,255,255,.2)"
|
291
|
+
// loadingBarContainer.style.alignItems = "center";
|
292
|
+
|
293
|
+
this._loadingElement.appendChild(loadingBarContainer);
|
294
|
+
|
295
|
+
|
289
296
|
this._loadingBar = document.createElement("div");
|
290
297
|
loadingBarContainer.appendChild(this._loadingBar);
|
291
298
|
const getGradientPos = function (t: number): string {
|
292
|
-
return Mathf.lerp(
|
299
|
+
return Mathf.lerp(0, maxWidth, t) + "%";
|
293
300
|
}
|
294
301
|
this._loadingBar.style.background =
|
295
|
-
`linear-gradient(90deg, #
|
302
|
+
`linear-gradient(90deg, #204f49 ${getGradientPos(0)}, #0BA398 ${getGradientPos(.3)}, #66A22F ${getGradientPos(.6)}, #D7DB0A ${getGradientPos(1)})`;
|
296
303
|
this._loadingBar.style.backgroundAttachment = "fixed";
|
297
304
|
this._loadingBar.style.width = "0%";
|
298
305
|
this._loadingBar.style.height = "100%";
|
@@ -313,18 +320,18 @@
|
|
313
320
|
this._loadingTextContainer = document.createElement("div");
|
314
321
|
this._loadingTextContainer.style.display = "flex";
|
315
322
|
this._loadingTextContainer.style.justifyContent = "center";
|
316
|
-
this._loadingTextContainer.style.marginTop = "
|
317
|
-
|
323
|
+
this._loadingTextContainer.style.marginTop = ".2rem";
|
324
|
+
details.appendChild(this._loadingTextContainer);
|
318
325
|
|
319
326
|
const messageContainer = document.createElement("div");
|
320
327
|
this._messageContainer = messageContainer;
|
321
328
|
messageContainer.style.display = "flex";
|
322
329
|
messageContainer.style.fontSize = ".8em";
|
323
|
-
messageContainer.style.paddingTop = ".
|
330
|
+
messageContainer.style.paddingTop = ".2rem";
|
324
331
|
messageContainer.style.fontWeight = "200";
|
325
332
|
// messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
|
326
333
|
messageContainer.style.justifyContent = "center";
|
327
|
-
|
334
|
+
details.appendChild(messageContainer);
|
328
335
|
|
329
336
|
if (hasLicense && this._element) {
|
330
337
|
const loadingTextColor = this._element.getAttribute("loading-text-color");
|
@@ -333,7 +340,7 @@
|
|
333
340
|
}
|
334
341
|
}
|
335
342
|
|
336
|
-
this.handleRuntimeLicense(
|
343
|
+
this.handleRuntimeLicense(details);
|
337
344
|
|
338
345
|
return this._loadingElement;
|
339
346
|
}
|
@@ -34,7 +34,9 @@
|
|
34
34
|
setWorldQuaternion(object, getWorldQuaternion(target));
|
35
35
|
// look at forward again so we don't get any roll
|
36
36
|
if (keepUpDirection) {
|
37
|
+
const ypos = lookFrom.y;
|
37
38
|
const forwardPoint = lookFrom.sub(getWorldDirection(object));
|
39
|
+
forwardPoint.y = ypos;
|
38
40
|
object.lookAt(forwardPoint);
|
39
41
|
}
|
40
42
|
return;
|
@@ -714,11 +714,8 @@
|
|
714
714
|
this.mode = mode;
|
715
715
|
this.context = context;
|
716
716
|
|
717
|
-
if(debug || getParam("console")) enableSpatialConsole(true);
|
717
|
+
if (debug || getParam("console")) enableSpatialConsole(true);
|
718
718
|
|
719
|
-
this.context.xr = this;
|
720
|
-
this.context.renderer.xr.setSession(this.session).then(this.onRendererSessionSet)
|
721
|
-
|
722
719
|
this._xr_scripts = [...extra.scripts];
|
723
720
|
this._xr_update_scripts = this._xr_scripts.filter(e => typeof e.onUpdateXR === "function");
|
724
721
|
this._controllerAdded = extra.controller_added;
|
@@ -769,6 +766,11 @@
|
|
769
766
|
this.onInputSourceAdded(newInputSource);
|
770
767
|
}
|
771
768
|
});
|
769
|
+
|
770
|
+
// we set the session on the webxr manager at the end because we want to receive inputsource events first
|
771
|
+
// e.g. in case there's a bug in the threejs codebase
|
772
|
+
this.context.xr = this;
|
773
|
+
this.context.renderer.xr.setSession(this.session).then(this.onRendererSessionSet)
|
772
774
|
}
|
773
775
|
|
774
776
|
/** called when renderer.setSession is fulfilled */
|
@@ -793,14 +795,33 @@
|
|
793
795
|
}
|
794
796
|
// check if an xr controller for this input source already exists
|
795
797
|
// in case we have both an event from inputsourceschange and from the construtor initial input sources
|
796
|
-
if (this.controllers.find(c => c.inputSource === newInputSource))
|
797
|
-
|
798
|
+
if (this.controllers.find(c => c.inputSource === newInputSource)) {
|
799
|
+
console.warn("Controller already exists for input source", index);
|
800
|
+
return;
|
801
|
+
}
|
798
802
|
const newController = new NeedleXRController(this, newInputSource, index);
|
799
|
-
this.controllers.push(newController);
|
800
|
-
this.controllers.sort((a, b) => a.index - b.index);
|
801
803
|
this._newControllers.push(newController);
|
802
|
-
|
804
|
+
}
|
803
805
|
|
806
|
+
/** Disconnects the controller, invokes events and notifies previou controller (if any) */
|
807
|
+
private disconnectInputSource(inputSource: XRInputSource) {
|
808
|
+
for (let i = this.controllers.length - 1; i >= 0; i--) {
|
809
|
+
const oldController = this.controllers[i];
|
810
|
+
if (oldController.inputSource === inputSource) {
|
811
|
+
console.log("Disconnecting controller", oldController.index);
|
812
|
+
this.controllers.splice(i, 1);
|
813
|
+
this.invokeControllerEvent(oldController, this._controllerRemoved, "removed");
|
814
|
+
const args: NeedleXRControllerEventArgs = {
|
815
|
+
xr: this,
|
816
|
+
controller: oldController,
|
817
|
+
change: "removed"
|
818
|
+
};
|
819
|
+
for (const script of this._xr_scripts) {
|
820
|
+
if (script.onXRControllerRemoved) script.onXRControllerRemoved(args);
|
821
|
+
}
|
822
|
+
oldController.onDisconnected();
|
823
|
+
}
|
824
|
+
}
|
804
825
|
}
|
805
826
|
|
806
827
|
/** End the XR Session */
|
@@ -880,26 +901,6 @@
|
|
880
901
|
enableSpatialConsole(false);
|
881
902
|
};
|
882
903
|
|
883
|
-
/** Disconnects the controller, invokes events and notifies previou controller (if any) */
|
884
|
-
private disconnectInputSource(inputSource: XRInputSource) {
|
885
|
-
for (let i = this.controllers.length - 1; i >= 0; i--) {
|
886
|
-
const oldController = this.controllers[i];
|
887
|
-
if (oldController.inputSource === inputSource) {
|
888
|
-
this.controllers.splice(i, 1);
|
889
|
-
this.invokeControllerEvent(oldController, this._controllerRemoved, "removed");
|
890
|
-
const args: NeedleXRControllerEventArgs = {
|
891
|
-
xr: this,
|
892
|
-
controller: oldController,
|
893
|
-
change: "removed"
|
894
|
-
};
|
895
|
-
for (const script of this._xr_scripts) {
|
896
|
-
if (script.onXRControllerRemoved) script.onXRControllerRemoved(args);
|
897
|
-
}
|
898
|
-
oldController.onDisconnected();
|
899
|
-
}
|
900
|
-
}
|
901
|
-
}
|
902
|
-
|
903
904
|
private _didStart: boolean = false;
|
904
905
|
|
905
906
|
/** Called every frame by the engine */
|
@@ -1010,16 +1011,27 @@
|
|
1010
1011
|
}
|
1011
1012
|
|
1012
1013
|
// handle when new controllers have been added
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1014
|
+
if (this._newControllers.length > 0) {
|
1015
|
+
const copy = [...this._newControllers];
|
1016
|
+
this._newControllers.length = 0;
|
1017
|
+
for (const controller of copy) {
|
1018
|
+
if (!controller.connected) continue;
|
1019
|
+
this.controllers.push(controller);
|
1020
|
+
for (const script of this._xr_scripts) {
|
1021
|
+
if (script.destroyed) {
|
1022
|
+
this._script_to_remove.push(script);
|
1023
|
+
continue;
|
1024
|
+
}
|
1025
|
+
if (script.activeAndEnabled === false) {
|
1026
|
+
continue;
|
1027
|
+
}
|
1028
|
+
this.invokeCallback_ControllerAdded(script, controller);
|
1029
|
+
// if (script.onXRControllerAdded)
|
1030
|
+
// script.onXRControllerAdded({ xr: this, controller, change: "added" });
|
1018
1031
|
}
|
1019
|
-
if (script.onXRControllerAdded) script.onXRControllerAdded({ xr: this, controller, change: "added" });
|
1020
1032
|
}
|
1033
|
+
this.controllers.sort((a, b) => a.index - b.index);
|
1021
1034
|
}
|
1022
|
-
this._newControllers.length = 0;
|
1023
1035
|
|
1024
1036
|
// invoke update on all scripts
|
1025
1037
|
for (const script of this._xr_update_scripts) {
|
@@ -300,8 +300,9 @@
|
|
300
300
|
quickLookCompatible: this.quickLookCompatible,
|
301
301
|
maxTextureSize: this.maxTextureSize,
|
302
302
|
});
|
303
|
-
const blob = new Blob([arraybuffer], { type: 'application/octet-stream' });
|
304
303
|
|
304
|
+
const blob = new Blob([arraybuffer], { type: 'vnd.usdz+zip' });
|
305
|
+
|
305
306
|
// cleanup – implicit animation behaviors need to be removed again
|
306
307
|
for (const go of implicitBehaviors) {
|
307
308
|
GameObject.destroy(go);
|
@@ -44,6 +44,7 @@
|
|
44
44
|
bottom: 100px;
|
45
45
|
left: 50%;
|
46
46
|
transform: translateX(-50%);
|
47
|
+
pointer-events: none;
|
47
48
|
}
|
48
49
|
:host button {
|
49
50
|
font-family: Roboto, sans-serif, Arial;
|
@@ -59,6 +60,7 @@
|
|
59
60
|
border: rgba(255, 255, 255, 0.2) solid 1px;
|
60
61
|
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
61
62
|
font-weight: normal;
|
63
|
+
pointer-events: all;
|
62
64
|
}
|
63
65
|
:host button:hover {
|
64
66
|
cursor: pointer;
|
@@ -41,7 +41,7 @@
|
|
41
41
|
return mode === "immersive-vr" || mode === "immersive-ar";
|
42
42
|
}
|
43
43
|
|
44
|
-
private _models = new Array<{ model?: IGameObject, controller: NeedleXRController, handmesh?: XRHandMeshModel }>();
|
44
|
+
private readonly _models = new Array<{ model?: IGameObject, controller: NeedleXRController, handmesh?: XRHandMeshModel }>();
|
45
45
|
|
46
46
|
|
47
47
|
async onXRControllerAdded(args: NeedleXRControllerEventArgs) {
|
@@ -57,7 +57,8 @@
|
|
57
57
|
if (controller.hand) {
|
58
58
|
if (this.createHandModel) {
|
59
59
|
const res = await this.loadHandModel(controller);
|
60
|
-
if
|
60
|
+
// check if the model doesnt exist, the hand disconnected or it's suddenly a controller
|
61
|
+
if (!res || !controller.connected || !controller.isHand) {
|
61
62
|
res?.handObject?.removeFromParent();
|
62
63
|
res?.handmesh?.controller?.removeFromParent();
|
63
64
|
return;
|
@@ -72,7 +73,10 @@
|
|
72
73
|
const assetUrl = await controller.getModelUrl();
|
73
74
|
if (assetUrl) {
|
74
75
|
const model = await this.loadModel(controller, assetUrl);
|
75
|
-
if
|
76
|
+
// check if the model doesnt exist, the controller disconnected or it's suddenly a hand
|
77
|
+
if (!model || !controller.connected || controller.isHand) {
|
78
|
+
return;
|
79
|
+
}
|
76
80
|
this._models.push({ controller: controller, model });
|
77
81
|
this._models.sort((a, b) => a.controller.index - b.controller.index);
|
78
82
|
this.scene.add(model);
|
@@ -90,16 +94,19 @@
|
|
90
94
|
}
|
91
95
|
onXRControllerRemoved(args: NeedleXRControllerEventArgs): void {
|
92
96
|
// 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
|
93
|
-
const indexInArray = this._models.findIndex(m => m
|
97
|
+
const indexInArray = this._models.findIndex(m => m.controller === args.controller);
|
94
98
|
const entry = this._models[indexInArray];
|
95
99
|
if (!entry) return;
|
100
|
+
|
96
101
|
this._models.splice(indexInArray, 1);
|
97
|
-
|
102
|
+
|
98
103
|
if (entry.handmesh) {
|
99
104
|
entry.handmesh.handModel?.removeFromParent();
|
105
|
+
entry.handmesh = undefined;
|
100
106
|
}
|
101
107
|
if (entry.model) {
|
102
108
|
entry.model.removeFromParent();
|
109
|
+
entry.model = undefined;
|
103
110
|
}
|
104
111
|
}
|
105
112
|
onBeforeRender() {
|
@@ -125,7 +132,7 @@
|
|
125
132
|
if (!entry) continue;
|
126
133
|
entry.model?.removeFromParent();
|
127
134
|
}
|
128
|
-
this._models =
|
135
|
+
this._models.length = 0;
|
129
136
|
}
|
130
137
|
|
131
138
|
private updateRendering(xr: NeedleXRSession) {
|