@@ -234,9 +234,9 @@
|
|
234
234
|
writer.indent++;
|
235
235
|
|
236
236
|
for (const transformData of arr) {
|
237
|
-
|
238
|
-
|
239
|
-
|
237
|
+
const posTimesArray = transformData.pos?.times;
|
238
|
+
const rotTimesArray = transformData.rot?.times;
|
239
|
+
const scaleTimesArray = transformData.scale?.times;
|
240
240
|
|
241
241
|
// timesArray is the sorted union of all time values
|
242
242
|
let timesArray: number[] = [];
|
@@ -73,10 +73,15 @@
|
|
73
73
|
for (const t in animation.tracks) {
|
74
74
|
const track = animation.tracks[t];
|
75
75
|
const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
|
76
|
-
|
76
|
+
let obj = gltf.scene.getObjectByName(objectName);
|
77
77
|
if (!obj) {
|
78
|
-
//
|
79
|
-
|
78
|
+
// this finds unnamed objects that still have tracks targeting them
|
79
|
+
obj = gltf.scene.getObjectByProperty('uuid', objectName);
|
80
|
+
|
81
|
+
if (!obj) {
|
82
|
+
// console.warn("could not find " + objectName, animation, gltf.scene);
|
83
|
+
continue;
|
84
|
+
}
|
80
85
|
}
|
81
86
|
let animationComponent = findAnimationGameObjectInParent(obj);
|
82
87
|
if (!animationComponent) {
|
@@ -84,7 +89,6 @@
|
|
84
89
|
}
|
85
90
|
const animations = animationComponent.animations = animationComponent.animations || [];
|
86
91
|
animation["name_animator"] = animationComponent.name;
|
87
|
-
// console.log(objectName, obj, animator.name, animations.length);
|
88
92
|
if (animations.indexOf(animation) < 0) {
|
89
93
|
animations.push(animation);
|
90
94
|
}
|
@@ -23,7 +23,7 @@
|
|
23
23
|
if(!this._startPosition) {
|
24
24
|
this._startPosition = this.gameObject.position.clone();
|
25
25
|
}
|
26
|
-
|
26
|
+
const t = freq / 100;
|
27
27
|
this.gameObject.position.y = this._startPosition.y + t * 0.07;
|
28
28
|
}
|
29
29
|
}
|
@@ -108,7 +108,7 @@
|
|
108
108
|
this.gameObject.add(container);
|
109
109
|
}
|
110
110
|
else {
|
111
|
-
|
111
|
+
const targetShadowComponent = this._parentComponent.shadowComponent;
|
112
112
|
if (targetShadowComponent) {
|
113
113
|
// console.log("ADD", this.name, "to", this._parentComponent.name, targetShadowComponent);
|
114
114
|
targetShadowComponent?.add(container);
|
@@ -19,35 +19,35 @@
|
|
19
19
|
// console.log(this);
|
20
20
|
|
21
21
|
// find center
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
const toPos = utils.getWorldPosition(this.to).clone();
|
23
|
+
const fromPos = utils.getWorldPosition(this.from).clone();
|
24
|
+
const dist = toPos.distanceTo(fromPos);
|
25
25
|
|
26
|
-
|
26
|
+
const dir0 = toPos.clone();
|
27
27
|
dir0.sub(fromPos);
|
28
|
-
|
28
|
+
const center = fromPos.clone();
|
29
29
|
center.add(toPos);
|
30
30
|
center.multiplyScalar(0.5);
|
31
31
|
|
32
32
|
// find direction we should offset in
|
33
|
-
|
33
|
+
const hintDir = utils.getWorldPosition(this.hint).clone();
|
34
34
|
hintDir.sub(center);
|
35
35
|
|
36
|
-
|
36
|
+
const offsetDir = new Vector3();
|
37
37
|
offsetDir.crossVectors(hintDir, dir0);
|
38
38
|
offsetDir.crossVectors(dir0, offsetDir);
|
39
39
|
offsetDir.normalize();
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
const halfDist = dist * 0.5;
|
42
|
+
const stretchDistance = Math.max(this.desiredDistance, halfDist);
|
43
|
+
const offsetLength = Math.sqrt(stretchDistance * stretchDistance - halfDist * halfDist);
|
44
44
|
|
45
|
-
|
45
|
+
const resultPos = offsetDir.clone();
|
46
46
|
resultPos.multiplyScalar(offsetLength);
|
47
47
|
resultPos.add(center);
|
48
48
|
utils.setWorldPosition(this.gameObject, resultPos);
|
49
49
|
|
50
|
-
|
50
|
+
const lookPos = center.clone();
|
51
51
|
lookPos.sub(offsetDir);
|
52
52
|
this.gameObject.lookAt(lookPos);
|
53
53
|
}
|
@@ -176,7 +176,7 @@
|
|
176
176
|
private static _origin: THREE.Vector3 = new Vector3();
|
177
177
|
private static _direction: THREE.Vector3 = new Vector3();
|
178
178
|
public screenPointToRay(x: number, y: number, ray?: Ray): Ray {
|
179
|
-
|
179
|
+
const cam = this.cam;
|
180
180
|
const origin = Camera._origin;
|
181
181
|
origin.set(x, y, -1);
|
182
182
|
this.context.input.convertScreenspaceToRaycastSpace(origin);
|
@@ -253,7 +253,7 @@
|
|
253
253
|
if (debugLayout && matrixWorldChanged) console.log("Canvas Layout changed", this.context.time.frameCount, this.name);
|
254
254
|
|
255
255
|
// TODO: optimize this, we should only need to update a subhierarchy of the parts where layout has changed
|
256
|
-
|
256
|
+
const didLog = false;
|
257
257
|
for (const ch of this._rectTransforms) {
|
258
258
|
if (matrixWorldChanged) ch.markDirty();
|
259
259
|
let layout = this._layoutGroups.get(ch.gameObject);
|
@@ -34,7 +34,7 @@
|
|
34
34
|
}
|
35
35
|
|
36
36
|
onEnable() {
|
37
|
-
|
37
|
+
const rb = this.rigidbody;
|
38
38
|
let collider = this.gameObject.getComponent(CapsuleCollider);
|
39
39
|
if (!collider)
|
40
40
|
collider = this.gameObject.addNewComponent(CapsuleCollider) as CapsuleCollider;
|
@@ -42,7 +42,7 @@
|
|
42
42
|
};
|
43
43
|
window.addEventListener("error", (event) => {
|
44
44
|
if (!event) return;
|
45
|
-
|
45
|
+
const message = event.error;
|
46
46
|
if (message === undefined) {
|
47
47
|
if (isLocalNetwork())
|
48
48
|
console.warn("Received unknown error", event, event.target);
|
@@ -135,7 +135,7 @@
|
|
135
135
|
if (!current) return false;
|
136
136
|
if (current === search) return true;
|
137
137
|
if (current.children) {
|
138
|
-
for (
|
138
|
+
for (const child of current.children) {
|
139
139
|
if (this.isInChildren(child, search)) {
|
140
140
|
return true;
|
141
141
|
}
|
@@ -44,7 +44,7 @@
|
|
44
44
|
}
|
45
45
|
|
46
46
|
dispose() {
|
47
|
-
for (
|
47
|
+
for (const key in this._assetReferences) {
|
48
48
|
const ref = this._assetReferences[key];
|
49
49
|
ref?.unload();
|
50
50
|
}
|
@@ -402,7 +402,7 @@
|
|
402
402
|
camera.updateProjectionMatrix();
|
403
403
|
}
|
404
404
|
|
405
|
-
async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<
|
405
|
+
async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<GLTF[] | null>, opts?: LoadingOptions) {
|
406
406
|
try {
|
407
407
|
this._isCreating = true;
|
408
408
|
const res = await this.internalOnCreate(buildScene, opts);
|
@@ -610,7 +610,7 @@
|
|
610
610
|
}
|
611
611
|
|
612
612
|
|
613
|
-
private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<GLTF[] |
|
613
|
+
private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<GLTF[] | null>, opts?: LoadingOptions) {
|
614
614
|
|
615
615
|
this.clear();
|
616
616
|
// stop the animation loop if its running during creation
|
@@ -624,11 +624,15 @@
|
|
624
624
|
|
625
625
|
// load and create scene
|
626
626
|
let prepare_succeeded = true;
|
627
|
-
let loadedFiles: GLTF[] | undefined |
|
627
|
+
let loadedFiles: GLTF[] | undefined | null = undefined;
|
628
628
|
try {
|
629
629
|
Context.Current = this;
|
630
|
-
if (buildScene)
|
630
|
+
if (buildScene) {
|
631
631
|
loadedFiles = await buildScene(this, opts);
|
632
|
+
// if the files are null the loading was cancelled
|
633
|
+
// this happens when the src attribute changes during loading and we want to load other files
|
634
|
+
if(loadedFiles === null) return false;
|
635
|
+
}
|
632
636
|
}
|
633
637
|
catch (err) {
|
634
638
|
console.error(err);
|
@@ -9,7 +9,7 @@
|
|
9
9
|
const debugRendering = getParam("debugloadingrendering");
|
10
10
|
const debugLicense = getParam("debuglicense");
|
11
11
|
|
12
|
-
declare type LoadingStyleOption = "dark" | "light";
|
12
|
+
declare type LoadingStyleOption = "dark" | "light" | "auto";
|
13
13
|
|
14
14
|
export class LoadingElementOptions {
|
15
15
|
className?: string;
|
@@ -177,8 +177,14 @@
|
|
177
177
|
if (debug && !existing) console.log("Creating loading element");
|
178
178
|
this._loadingElement = existing || document.createElement("div");
|
179
179
|
|
180
|
-
|
181
|
-
|
180
|
+
let loadingStyle: LoadingStyleOption = this._element.getAttribute("loading-style") as LoadingStyleOption;
|
181
|
+
if (loadingStyle === "auto") {
|
182
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches)
|
183
|
+
loadingStyle = "dark";
|
184
|
+
else
|
185
|
+
loadingStyle = "light";
|
186
|
+
}
|
187
|
+
|
182
188
|
const hasLicense = hasProLicense();
|
183
189
|
if (!existing) {
|
184
190
|
this._loadingElement.style.position = "absolute";
|
@@ -87,6 +87,8 @@
|
|
87
87
|
private _loadingProgress01: number = 0;
|
88
88
|
private _loadingView?: ILoadingViewHandler;
|
89
89
|
private _previousSrc: string | null | string[] = null;
|
90
|
+
/** set to true after <needle-engine> did load completely at least once. Set to false when <needle-engine> is removed from the document */
|
91
|
+
private _didFullyLoad: boolean = false;
|
90
92
|
|
91
93
|
constructor() {
|
92
94
|
super();
|
@@ -101,9 +103,15 @@
|
|
101
103
|
:host {
|
102
104
|
position: relative;
|
103
105
|
display: block;
|
104
|
-
width: 600px;
|
105
|
-
height: 300px;
|
106
|
+
width: max(600px, 100vw);
|
107
|
+
height: max(300px, 100vh);
|
106
108
|
}
|
109
|
+
@media (max-width: 600px) {
|
110
|
+
:host {
|
111
|
+
width: 100vw;
|
112
|
+
height: 100vh;
|
113
|
+
}
|
114
|
+
}
|
107
115
|
:host canvas {
|
108
116
|
position: absolute;
|
109
117
|
user-select: none;
|
@@ -171,6 +179,7 @@
|
|
171
179
|
}
|
172
180
|
|
173
181
|
disconnectedCallback() {
|
182
|
+
this._didFullyLoad = false;
|
174
183
|
const keepAlive = this.getAttribute("keep-alive");
|
175
184
|
if (debug) console.warn("<needle-engine> disconnected, keep-alive:", keepAlive);
|
176
185
|
if (keepAlive === undefined || keepAlive !== "true") {
|
@@ -261,25 +270,38 @@
|
|
261
270
|
},
|
262
271
|
cancelable: true
|
263
272
|
}));
|
273
|
+
// for local development we allow overriding the loading screen - but we notify the user that it won't work in a deployed environment
|
264
274
|
if (useDefaultLoading === false && !allowOverridingDefaultLoading) {
|
265
275
|
if (!isDevEnvironment())
|
266
276
|
useDefaultLoading = true;
|
267
277
|
console.warn("Needle Engine: You need a commercial license to override the default loading view. Visit https://needle.tools/pricing");
|
268
278
|
if (isDevEnvironment()) showBalloonWarning("You need a <a target=\"_blank\" href=\"https://needle.tools/pricing\">commercial license</a> to override the default loading view. This will not work in production.");
|
269
279
|
}
|
280
|
+
// create the loading view idf necessary
|
270
281
|
if (!this._loadingView && useDefaultLoading)
|
271
282
|
this._loadingView = new EngineLoadingView(this);
|
272
|
-
if (useDefaultLoading)
|
273
|
-
|
274
|
-
|
275
|
-
|
283
|
+
if (useDefaultLoading) {
|
284
|
+
// Only show the loading screen immedialty if we haven't loaded anything before
|
285
|
+
if (this._didFullyLoad !== true)
|
286
|
+
this._loadingView?.onLoadingBegin("begin load");
|
287
|
+
else {
|
288
|
+
// If we have loaded a glb previously and are loading a new glb due to e.g. src change
|
289
|
+
// we don't want to show the loading screen immediately to avoid blinking if the glb to be loaded is tiny
|
290
|
+
// so we wait a bit and only show the loading screen if the loading takes longer than a short moment
|
291
|
+
setTimeout(() => {
|
292
|
+
// If the loading progress is already above ~ 70% we also don't need to show the loading screen anymore
|
293
|
+
if (this._loadingView && this._loadingProgress01 < 0.3 && this._loadId === loadId)
|
294
|
+
this._loadingView.onLoadingBegin("begin load");
|
295
|
+
}, 300)
|
296
|
+
}
|
297
|
+
}
|
298
|
+
if (debug) console.warn("--------------", loadId, "Needle Engine: Begin loading", alias ?? "", filesToLoad);
|
276
299
|
this.onBeforeBeginLoading();
|
277
300
|
|
278
|
-
|
301
|
+
type LoadFunctionResult = (ctx: Context) => Promise<LoadedGLTF[] | null>;
|
302
|
+
let loadFunction: null | LoadFunctionResult = null;
|
303
|
+
const loadedFiles: Array<LoadedGLTF> = [];
|
279
304
|
|
280
|
-
let loadFunction: any = null;
|
281
|
-
let loadedFiles: Array<LoadedGLTF> = [];
|
282
|
-
|
283
305
|
if (filesToLoad?.length > 0) {
|
284
306
|
loadFunction = async (ctx: Context) => {
|
285
307
|
let hash = 0;
|
@@ -312,6 +334,7 @@
|
|
312
334
|
}
|
313
335
|
}
|
314
336
|
const res = await loader.loadSync(ctx, url, url, hash, prog => {
|
337
|
+
if(this._loadId !== loadId) return;
|
315
338
|
// Calc progress
|
316
339
|
progress.progress = prog;
|
317
340
|
this._loadingProgress01 = calculateProgress01(progress);
|
@@ -332,13 +355,13 @@
|
|
332
355
|
// if the loading ID changed but the scene has already been loaded we want to dispose it
|
333
356
|
if (this._loadId !== loadId) {
|
334
357
|
destroy(obj, true, true)
|
335
|
-
return
|
358
|
+
return null;
|
336
359
|
}
|
337
360
|
GameObject.add(obj, ctx.scene, ctx);
|
338
361
|
}
|
339
362
|
}
|
340
363
|
}
|
341
|
-
if (this._loadId !== loadId) return
|
364
|
+
if (this._loadId !== loadId) return null;
|
342
365
|
return loadedFiles;
|
343
366
|
};
|
344
367
|
}
|
@@ -347,7 +370,7 @@
|
|
347
370
|
if (currentHash !== null && currentHash !== undefined)
|
348
371
|
this._context.hash = currentHash;
|
349
372
|
this._context.alias = alias;
|
350
|
-
await this._context.onCreate(loadFunction);
|
373
|
+
await this._context.onCreate(loadFunction as any);
|
351
374
|
if (this._loadId !== loadId) return;
|
352
375
|
if (debug)
|
353
376
|
console.warn("--------------", loadId, "Needle Engine: finished loading", alias ?? "", filesToLoad);
|
@@ -355,6 +378,7 @@
|
|
355
378
|
if (useDefaultLoading) {
|
356
379
|
this._loadingView?.onLoadingUpdate(1, "creating scene");
|
357
380
|
}
|
381
|
+
this._didFullyLoad = true;
|
358
382
|
this.classList.remove("loading");
|
359
383
|
this.classList.add("loading-finished");
|
360
384
|
this.dispatchEvent(new CustomEvent("loadfinished", {
|
@@ -554,7 +578,7 @@
|
|
554
578
|
const parts = str.split("/");
|
555
579
|
let name = parts[parts.length - 1];
|
556
580
|
// Remove params
|
557
|
-
|
581
|
+
const index = name.indexOf("?")
|
558
582
|
if (index > 0)
|
559
583
|
name = name.substring(0, index);
|
560
584
|
return decodeURIComponent(name);
|
@@ -317,8 +317,7 @@
|
|
317
317
|
instance.userData = {};
|
318
318
|
const children = instance.children;
|
319
319
|
instance.children = [];
|
320
|
-
|
321
|
-
clone = instance.clone(false);
|
320
|
+
const clone: Object3D | GameObject = instance.clone(false);
|
322
321
|
apply(clone);
|
323
322
|
// if(instance[$originalGuid])
|
324
323
|
// clone[$originalGuid] = instance[$originalGuid];
|
@@ -102,7 +102,7 @@
|
|
102
102
|
const instancesOfType = instances.get(newType.name);
|
103
103
|
|
104
104
|
let hotReloadMessage = "[Needle Engine] Updating type: " + key;
|
105
|
-
|
105
|
+
const typesCount = instancesOfType?.length ?? -1;
|
106
106
|
if (typesCount > 0) hotReloadMessage += " x" + typesCount;
|
107
107
|
else hotReloadMessage += " - no instances";
|
108
108
|
console.log(hotReloadMessage);
|
@@ -9,7 +9,7 @@
|
|
9
9
|
document.removeEventListener('touchstart', fn);
|
10
10
|
res();
|
11
11
|
};
|
12
|
-
|
12
|
+
const fn = callback;
|
13
13
|
document.addEventListener('pointerdown', fn);
|
14
14
|
document.addEventListener('click', fn);
|
15
15
|
document.addEventListener('dragstart', fn);
|
@@ -487,20 +487,20 @@
|
|
487
487
|
private onMouseDown = (evt: MouseEvent) => {
|
488
488
|
if (evt.defaultPrevented) return;
|
489
489
|
if (this.canReceiveInput(evt) === false) return;
|
490
|
-
|
490
|
+
const i = evt.button;
|
491
491
|
this.onDown({ button: i, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, source: evt });
|
492
492
|
}
|
493
493
|
|
494
494
|
private onMouseMove = (evt: MouseEvent) => {
|
495
495
|
if (evt.defaultPrevented) return;
|
496
|
-
|
496
|
+
const i = evt.button;
|
497
497
|
const args: PointerEventArgs = { button: i, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, source: evt, movementX: evt.movementX, movementY: evt.movementY };
|
498
498
|
this.onMove(args);
|
499
499
|
}
|
500
500
|
|
501
501
|
private onMouseUp = (evt: MouseEvent) => {
|
502
502
|
if (evt.defaultPrevented) return;
|
503
|
-
|
503
|
+
const i = evt.button;
|
504
504
|
if (!this.isNewEvent(evt.timeStamp, i, this._pointerUpTimestamp)) return;
|
505
505
|
this.onUp({ button: i, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, source: evt });
|
506
506
|
}
|
@@ -38,7 +38,7 @@
|
|
38
38
|
sendUsageMessageToAnalyticsBackend();
|
39
39
|
});
|
40
40
|
|
41
|
-
export let runtimeLicenseCheckPromise: Promise<void
|
41
|
+
export let runtimeLicenseCheckPromise: Promise<void> | undefined = undefined;
|
42
42
|
async function checkLicense() {
|
43
43
|
// Only perform the runtime license check once
|
44
44
|
if (runtimeLicenseCheckPromise) return runtimeLicenseCheckPromise;
|
@@ -107,7 +107,7 @@
|
|
107
107
|
}
|
108
108
|
}, 100);
|
109
109
|
|
110
|
-
|
110
|
+
const svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
|
111
111
|
const logoElement = document.createElement("div");
|
112
112
|
logoElement.innerHTML = svg;
|
113
113
|
logoElement.classList.add("logo");
|
@@ -158,7 +158,7 @@
|
|
158
158
|
border: 2px solid rgba(160,160,160,.5);
|
159
159
|
`;
|
160
160
|
// url must contain https for firefox to make it clickable
|
161
|
-
|
161
|
+
const licenseText = "Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options.";
|
162
162
|
console.log("%c " + licenseText, style);
|
163
163
|
}
|
164
164
|
|
@@ -295,7 +295,7 @@
|
|
295
295
|
try {
|
296
296
|
const analyticsBackendUrlForward = "https://urls.needle.tools/analytics-endpoint-v2";
|
297
297
|
const res = await fetch(analyticsBackendUrlForward);
|
298
|
-
|
298
|
+
const analyticsUrl = await res.text();
|
299
299
|
if (analyticsUrl) {
|
300
300
|
if (debug) console.log("Analytics backend url", analyticsUrl);
|
301
301
|
|
@@ -346,7 +346,7 @@
|
|
346
346
|
|
347
347
|
if (setter) {
|
348
348
|
descriptor.set = function (value: T) {
|
349
|
-
|
349
|
+
const valueChanged = false;
|
350
350
|
|
351
351
|
const syncer = getSyncer(this);
|
352
352
|
|
@@ -28,7 +28,7 @@
|
|
28
28
|
}
|
29
29
|
|
30
30
|
private trySetupHost(id: string) {
|
31
|
-
|
31
|
+
const host = new Peer(id);
|
32
32
|
host.on("error", err => {
|
33
33
|
console.error(err);
|
34
34
|
this._host = undefined;
|
@@ -467,7 +467,7 @@
|
|
467
467
|
}
|
468
468
|
if(serverUrl === undefined){
|
469
469
|
console.log("Fetch default backend url: " + defaultNetworkingBackendUrlProvider);
|
470
|
-
|
470
|
+
const failed = false;
|
471
471
|
const defaultUrlResponse = await fetch(defaultNetworkingBackendUrlProvider);
|
472
472
|
serverUrl = await defaultUrlResponse.text();
|
473
473
|
if(failed) return;
|
@@ -154,7 +154,7 @@
|
|
154
154
|
}
|
155
155
|
|
156
156
|
function getPatches(prototype, fieldName: string) {
|
157
|
-
|
157
|
+
const patchesMap = patches().get(prototype);
|
158
158
|
if (!patchesMap) {
|
159
159
|
return null;
|
160
160
|
}
|
@@ -947,8 +947,8 @@
|
|
947
947
|
this.calculateJointRelativeMatrices(body1.gameObject, body2.gameObject, this._tempMatrix);
|
948
948
|
this._tempMatrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
|
949
949
|
|
950
|
-
|
951
|
-
|
950
|
+
const params = RAPIER.JointData.revolute(anchor, this._tempPosition, axis);
|
951
|
+
const joint = this.world.createImpulseJoint(params, b1, b2, true);
|
952
952
|
if (debugPhysics)
|
953
953
|
console.log("ADD HINGE JOINT", joint)
|
954
954
|
}
|
@@ -233,7 +233,7 @@
|
|
233
233
|
// fallback
|
234
234
|
if (this._waitPromise) return this._waitPromise;
|
235
235
|
this._waitPromise = new Promise((res, _rej) => {
|
236
|
-
|
236
|
+
const interval = setInterval(async () => {
|
237
237
|
const ex = this.internalGetReflection(sourceId);
|
238
238
|
if (ex) {
|
239
239
|
clearInterval(interval);
|
@@ -112,7 +112,7 @@
|
|
112
112
|
// independent of order of loading
|
113
113
|
let res: GameObject | Behaviour | undefined | null = undefined;
|
114
114
|
// first try to search in the current gltf scene (if any)
|
115
|
-
|
115
|
+
const gltfScene = context.gltf?.scene;
|
116
116
|
if (gltfScene) {
|
117
117
|
res = GameObject.findByGuid(data.guid, gltfScene);
|
118
118
|
}
|
@@ -273,9 +273,8 @@
|
|
273
273
|
printWarningMethodNotFound();
|
274
274
|
}
|
275
275
|
}
|
276
|
-
|
276
|
+
|
277
277
|
let args = call.argument;
|
278
|
-
|
279
278
|
if (args !== undefined) {
|
280
279
|
if (typeof args === "object") {
|
281
280
|
// Try to deserialize the call argument to either a object or a component reference
|
@@ -287,7 +286,7 @@
|
|
287
286
|
|
288
287
|
// This is the final method we pass to the call info (or undefined if the method couldnt be resolved)
|
289
288
|
const eventMethod = hasMethod ? this.createEventMethod(target, call.method, args) : undefined;
|
290
|
-
fn = new CallInfo(eventMethod, call.enabled);
|
289
|
+
const fn = new CallInfo(eventMethod, call.enabled);
|
291
290
|
|
292
291
|
|
293
292
|
if (!fn.method)
|
@@ -54,7 +54,7 @@
|
|
54
54
|
console.log("FOUND " + name, type.name, type.constructor.name, res, this.typeMap);
|
55
55
|
return res;
|
56
56
|
}
|
57
|
-
|
57
|
+
const parent = Object.getPrototypeOf(type);
|
58
58
|
const hasPrototypeOrConstructor = parent.prototype || parent.constructor;
|
59
59
|
// console.log(name, type, parent);
|
60
60
|
if (!hasPrototypeOrConstructor) {
|
@@ -221,7 +221,7 @@
|
|
221
221
|
if (types === undefined) return null;
|
222
222
|
const res = {};
|
223
223
|
for (const key in types) {
|
224
|
-
|
224
|
+
const val = obj[key];
|
225
225
|
|
226
226
|
// if the object bein serialized is some type of object check if we have special handling registered for it
|
227
227
|
if (val !== undefined && val !== null && typeof val === "object") {
|
@@ -609,7 +609,7 @@
|
|
609
609
|
if (source === undefined || source === null) return;
|
610
610
|
if (target === undefined || target === null) return;
|
611
611
|
|
612
|
-
|
612
|
+
const onlyDeclared = false;
|
613
613
|
// if (onlyDeclared === true && target.constructor) {
|
614
614
|
// if (target.constructor[ALL_PROPERTIES_MARKER] === true)
|
615
615
|
// onlyDeclared = false;
|
@@ -1,10 +1,22 @@
|
|
1
|
+
import { ContextRegistry } from "./engine_context_registry.js";
|
1
2
|
import { Context } from "./engine_setup.js";
|
2
3
|
import { PerspectiveCamera, Camera } from "three";
|
3
4
|
|
4
5
|
declare type ImageMimeType = "image/webp" | "image/png";
|
5
6
|
|
6
|
-
|
7
|
+
/**
|
8
|
+
* Take a screenshot from the current scene
|
9
|
+
*/
|
10
|
+
export function screenshot(context?: Context, width?: number, height?: number, mimeType: ImageMimeType = "image/webp", camera?: Camera | null): string | null {
|
7
11
|
|
12
|
+
if (!context) {
|
13
|
+
context = ContextRegistry.Current as Context;
|
14
|
+
if (!context) {
|
15
|
+
console.error("Can not save screenshot: No needle-engine context found or provided.");
|
16
|
+
return null;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
8
20
|
if (!camera) {
|
9
21
|
camera = context.mainCamera;
|
10
22
|
if (!camera) {
|
@@ -15,6 +27,9 @@
|
|
15
27
|
const prevWidth = context.renderer.domElement.width;
|
16
28
|
const prevHeight = context.renderer.domElement.height;
|
17
29
|
|
30
|
+
if (!width) width = prevWidth;
|
31
|
+
if (!height) height = prevHeight;
|
32
|
+
|
18
33
|
try {
|
19
34
|
const canvas = context.renderer.domElement;
|
20
35
|
|
@@ -39,4 +54,23 @@
|
|
39
54
|
}
|
40
55
|
|
41
56
|
return null;
|
57
|
+
}
|
58
|
+
|
59
|
+
let saveImageElement: HTMLAnchorElement | null = null;
|
60
|
+
|
61
|
+
/** Download a image (must be a data url) */
|
62
|
+
export function saveImage(dataUrl: string | null, filename: string) {
|
63
|
+
if (!dataUrl) {
|
64
|
+
return;
|
65
|
+
}
|
66
|
+
if (!dataUrl.startsWith("data:image")) {
|
67
|
+
console.error("Can not save image: Data url is not an image", dataUrl);
|
68
|
+
return;
|
69
|
+
}
|
70
|
+
if (!saveImageElement) {
|
71
|
+
saveImageElement = document.createElement("a");
|
72
|
+
}
|
73
|
+
saveImageElement.href = dataUrl;
|
74
|
+
saveImageElement.download = filename;
|
75
|
+
saveImageElement.click();
|
42
76
|
}
|
@@ -252,7 +252,7 @@
|
|
252
252
|
// Take the source uri as the base path
|
253
253
|
const basePath = source.substring(0, pathIndex + 1);
|
254
254
|
// Append the relative uri
|
255
|
-
|
255
|
+
const newUri = basePath + uri;
|
256
256
|
// newUri = new URL(newUri, globalThis.location.href).href;
|
257
257
|
if (debugGetPath) console.log("source:", source, "- changed uri \nfrom", uri, "\n→ ", newUri, "\n" + basePath);
|
258
258
|
return newUri;
|
@@ -97,7 +97,7 @@
|
|
97
97
|
if (!reader) return null;
|
98
98
|
|
99
99
|
let received: number = 0;
|
100
|
-
|
100
|
+
const chunks: Uint8Array[] = [];
|
101
101
|
while (true) {
|
102
102
|
const { done, value } = await reader.read();
|
103
103
|
if (value) {
|
@@ -112,7 +112,7 @@
|
|
112
112
|
}
|
113
113
|
const final = new Uint8Array(received);
|
114
114
|
let position = 0;
|
115
|
-
for (
|
115
|
+
for (const chunk of chunks) {
|
116
116
|
final.set(chunk, position);
|
117
117
|
position += chunk.length;
|
118
118
|
}
|
@@ -43,7 +43,7 @@
|
|
43
43
|
if (this.key !== undefined) {
|
44
44
|
let temp = "";
|
45
45
|
let foundFirstLetter = false;
|
46
|
-
for (
|
46
|
+
for (const c of this.key) {
|
47
47
|
if (foundFirstLetter && isUpperCase(c))
|
48
48
|
temp += "-";
|
49
49
|
foundFirstLetter = true;
|
@@ -11,7 +11,7 @@
|
|
11
11
|
console.warn("No prototype found", obj, obj.prototype, obj.constructor);
|
12
12
|
return;
|
13
13
|
}
|
14
|
-
|
14
|
+
const handler = handlers.get(prototype);
|
15
15
|
if (handler) {
|
16
16
|
// console.log("OK", prototype);
|
17
17
|
handler.apply(obj);
|
@@ -150,7 +150,7 @@
|
|
150
150
|
if (this._currentlyCreatingPanel) return;
|
151
151
|
this._currentlyCreatingPanel = true;
|
152
152
|
|
153
|
-
|
153
|
+
const offset = .015;
|
154
154
|
// if (this.Root) offset = .02 * (1 / this.Root.gameObject.scale.z);
|
155
155
|
const opts = {
|
156
156
|
backgroundColor: this.color,
|
@@ -269,13 +269,13 @@
|
|
269
269
|
}
|
270
270
|
|
271
271
|
const size = isHorizontal ? rt.width : rt.height;
|
272
|
-
|
272
|
+
const halfSize = size * .5;
|
273
273
|
start += halfSize;
|
274
274
|
|
275
275
|
// TODO: this isnt correct yet!
|
276
276
|
if (forceExpandSize) {
|
277
277
|
// this is the center of the cell
|
278
|
-
|
278
|
+
const preferredStart = sizePerChild * k - sizePerChild * .5;
|
279
279
|
if (preferredStart > start) {
|
280
280
|
start = preferredStart - sizePerChild * .5 + size + this.padding.left;
|
281
281
|
start -= halfSize;
|
@@ -67,7 +67,7 @@
|
|
67
67
|
|
68
68
|
if (this.lodModels && Array.isArray(this.lodModels)) {
|
69
69
|
let maxDistance = 0;
|
70
|
-
|
70
|
+
const renderers: Renderer[] = [];
|
71
71
|
for (const model of this.lodModels) {
|
72
72
|
maxDistance = Math.max(model.distance, maxDistance);
|
73
73
|
const lod = new LOD(model);
|
@@ -104,7 +104,7 @@
|
|
104
104
|
}
|
105
105
|
|
106
106
|
writeNode(node: Object3D, nodeDef) {
|
107
|
-
|
107
|
+
const nodeIndex = this.writer.json.nodes.length;
|
108
108
|
console.log(node.name, nodeIndex, node.uuid);
|
109
109
|
const context = new ExportData(node, nodeIndex, nodeDef);
|
110
110
|
this.exportContext[nodeIndex] = context;
|
@@ -43,7 +43,7 @@
|
|
43
43
|
|
44
44
|
const promises: Promise<Texture | null>[] = [];
|
45
45
|
|
46
|
-
for (
|
46
|
+
for (const slot of Object.keys(material)) {
|
47
47
|
const val = material[slot];
|
48
48
|
if (val?.isTexture) {
|
49
49
|
const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
|
@@ -53,7 +53,7 @@
|
|
53
53
|
|
54
54
|
if (material instanceof RawShaderMaterial) {
|
55
55
|
// iterate uniforms
|
56
|
-
for (
|
56
|
+
for (const slot of Object.keys(material.uniforms)) {
|
57
57
|
const val = material.uniforms[slot].value;
|
58
58
|
if (val?.isTexture) {
|
59
59
|
const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
|
@@ -1,50 +1,50 @@
|
|
1
|
-
import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
|
2
|
-
makeErrorsVisibleForDevelopment();
|
3
|
-
|
4
|
-
import "./engine/engine_element.js";
|
5
|
-
import "./engine/engine_setup.js";
|
6
|
-
export * from "./engine/api.js";
|
7
|
-
export * from "./engine-components/api.js";
|
8
|
-
export * from "./engine-components-experimental/api.js";
|
9
|
-
export * from "./engine-schemes/api.js";
|
10
|
-
|
11
|
-
// make accessible for external javascript
|
12
|
-
import { Context } from "./engine/engine_setup.js";
|
13
|
-
const Needle = { Context: Context };
|
14
|
-
if (globalThis["Needle"] !== undefined) {
|
15
|
-
console.warn("Needle Engine is already imported");
|
16
|
-
}
|
17
|
-
globalThis["Needle"] = Needle;
|
18
|
-
function registerGlobal(obj: object) {
|
19
|
-
for (const key in obj) {
|
20
|
-
Needle[key] = obj[key];
|
21
|
-
}
|
22
|
-
}
|
23
|
-
import * as Component from "./engine-components/Component.js";
|
24
|
-
registerGlobal(Component);
|
25
|
-
|
26
|
-
import * as Components from "./engine-components/codegen/components.js";
|
27
|
-
registerGlobal(Components);
|
28
|
-
|
29
|
-
|
30
|
-
import { GameObject } from "./engine-components/Component.js";
|
31
|
-
for (const method of Object.getOwnPropertyNames(GameObject)) {
|
32
|
-
switch (method) {
|
33
|
-
case "prototype":
|
34
|
-
case "constructor":
|
35
|
-
case "length":
|
36
|
-
case "name":
|
37
|
-
continue;
|
38
|
-
default:
|
39
|
-
Needle[method] = GameObject[method];
|
40
|
-
break;
|
41
|
-
}
|
42
|
-
}
|
43
|
-
|
44
|
-
// make three accessible
|
45
|
-
import * as THREE from "three";
|
46
|
-
if (!globalThis["THREE"]) {
|
47
|
-
globalThis["THREE"] = THREE;
|
48
|
-
}
|
49
|
-
else console.warn("Threejs is already imported");
|
50
|
-
|
1
|
+
import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
|
2
|
+
makeErrorsVisibleForDevelopment();
|
3
|
+
|
4
|
+
import "./engine/engine_element.js";
|
5
|
+
import "./engine/engine_setup.js";
|
6
|
+
export * from "./engine/api.js";
|
7
|
+
export * from "./engine-components/api.js";
|
8
|
+
export * from "./engine-components-experimental/api.js";
|
9
|
+
export * from "./engine-schemes/api.js";
|
10
|
+
|
11
|
+
// make accessible for external javascript
|
12
|
+
import { Context } from "./engine/engine_setup.js";
|
13
|
+
const Needle = { Context: Context };
|
14
|
+
if (globalThis["Needle"] !== undefined) {
|
15
|
+
console.warn("Needle Engine is already imported");
|
16
|
+
}
|
17
|
+
globalThis["Needle"] = Needle;
|
18
|
+
function registerGlobal(obj: object) {
|
19
|
+
for (const key in obj) {
|
20
|
+
Needle[key] = obj[key];
|
21
|
+
}
|
22
|
+
}
|
23
|
+
import * as Component from "./engine-components/Component.js";
|
24
|
+
registerGlobal(Component);
|
25
|
+
|
26
|
+
import * as Components from "./engine-components/codegen/components.js";
|
27
|
+
registerGlobal(Components);
|
28
|
+
|
29
|
+
|
30
|
+
import { GameObject } from "./engine-components/Component.js";
|
31
|
+
for (const method of Object.getOwnPropertyNames(GameObject)) {
|
32
|
+
switch (method) {
|
33
|
+
case "prototype":
|
34
|
+
case "constructor":
|
35
|
+
case "length":
|
36
|
+
case "name":
|
37
|
+
continue;
|
38
|
+
default:
|
39
|
+
Needle[method] = GameObject[method];
|
40
|
+
break;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
// make three accessible
|
45
|
+
import * as THREE from "three";
|
46
|
+
if (!globalThis["THREE"]) {
|
47
|
+
globalThis["THREE"] = THREE;
|
48
|
+
}
|
49
|
+
else console.warn("Threejs is already imported");
|
50
|
+
|
@@ -52,7 +52,7 @@
|
|
52
52
|
const quat = new Quaternion().setFromEuler(euler);
|
53
53
|
if(this.affectRotation) utils.setWorldQuaternion(this.gameObject, rot.multiply(quat));
|
54
54
|
|
55
|
-
|
55
|
+
const lookDirection = new Vector3();
|
56
56
|
this.from.getWorldDirection(lookDirection).multiplyScalar(50);
|
57
57
|
if (this.levelLookDirection) lookDirection.y = 0;
|
58
58
|
if (this.alignLookDirection) this.gameObject.lookAt(lookDirection);
|
@@ -451,7 +451,7 @@
|
|
451
451
|
//////////////////////
|
452
452
|
// calculate speed
|
453
453
|
const baseVelocity = particle[$startVelocity];
|
454
|
-
|
454
|
+
const gravityFactor = particle[$gravityFactor];
|
455
455
|
if (gravityFactor !== 0) {
|
456
456
|
const factor = gravityFactor * particle[$gravitySpeed];
|
457
457
|
temp3.copy(this._gravityDirection).multiplyScalar(factor);
|
@@ -973,7 +973,7 @@
|
|
973
973
|
worldSpace: boolean = false;
|
974
974
|
|
975
975
|
getWidth(size: number, _life01: number, pos01: number, t : number) {
|
976
|
-
|
976
|
+
const res = this.widthOverTrail.evaluate(pos01, t);
|
977
977
|
size *= res;
|
978
978
|
return size;
|
979
979
|
}
|
@@ -1346,7 +1346,7 @@
|
|
1346
1346
|
const speed = baseVelocity.length();
|
1347
1347
|
if (speed > max) {
|
1348
1348
|
this._temp.copy(baseVelocity).normalize().multiplyScalar(max);
|
1349
|
-
|
1349
|
+
const t = this.dampen * .5;
|
1350
1350
|
// t *= scale;
|
1351
1351
|
baseVelocity.x = Mathf.lerp(baseVelocity.x, this._temp.x, t);
|
1352
1352
|
baseVelocity.y = Mathf.lerp(baseVelocity.y, this._temp.y, t);
|
@@ -35,7 +35,7 @@
|
|
35
35
|
|
36
36
|
const instance = await this.asset?.instantiateSynced({ parent: this.gameObject }, true);
|
37
37
|
if (instance) {
|
38
|
-
|
38
|
+
const pl = GameObject.getComponent(instance, PlayerState);
|
39
39
|
if (pl) {
|
40
40
|
pl.owner = this.context.connection.connectionId!;
|
41
41
|
this.onPlayerSpawned?.invoke(instance);
|
@@ -140,8 +140,8 @@
|
|
140
140
|
|
141
141
|
// make sure we have the currently assigned material cached (and an up to date clone of that)
|
142
142
|
// TODO: this is causing problems with progressive textures sometimes (depending on the order) when the version changes and we re-create materials over and over. We might want to just set the material envmap instead of making a clone
|
143
|
-
|
144
|
-
|
143
|
+
const isCachedInstance = material === cached?.copy;
|
144
|
+
const hasChanged = !cached || cached.material.uuid !== material.uuid || cached.copy.version !== material.version;
|
145
145
|
if (!isCachedInstance && hasChanged) {
|
146
146
|
if (debug) {
|
147
147
|
let reason = "";
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { TypeStore } from "./../engine_typestore.js"
|
2
|
-
|
2
|
+
|
3
3
|
// Import types
|
4
4
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
5
5
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -217,7 +217,7 @@
|
|
217
217
|
import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering.js";
|
218
218
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
219
219
|
import { XRState } from "../../engine-components/XRFlag.js";
|
220
|
-
|
220
|
+
|
221
221
|
// Register types
|
222
222
|
TypeStore.add("__Ignore", __Ignore);
|
223
223
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -853,7 +853,7 @@
|
|
853
853
|
|
854
854
|
private autoUpdateInstanceMatrix(obj: Object3D) {
|
855
855
|
const original = obj.matrixWorld["multiplyMatrices"].bind(obj.matrixWorld);
|
856
|
-
|
856
|
+
const previousMatrix: THREE.Matrix4 = obj.matrixWorld.clone();
|
857
857
|
const matrixChangeWrapper = (a: Matrix4, b: Matrix4) => {
|
858
858
|
const newMatrixWorld = original(a, b);
|
859
859
|
if (obj[NEED_UPDATE_INSTANCE_KEY] || previousMatrix.equals(newMatrixWorld) === false) {
|
@@ -147,7 +147,7 @@
|
|
147
147
|
|
148
148
|
private onPopState = async (_state: PopStateEvent) => {
|
149
149
|
if (!this.useHistory) return;
|
150
|
-
|
150
|
+
const wasUsingHistory = this.useHistory;
|
151
151
|
try {
|
152
152
|
this.useHistory = false;
|
153
153
|
let didResolve = false;
|
@@ -366,7 +366,7 @@
|
|
366
366
|
|
367
367
|
function sceneUriToName(uri: string): string {
|
368
368
|
const name = uri.split("/").pop();
|
369
|
-
|
369
|
+
const value = name?.split(".").shift();
|
370
370
|
if (value?.length) return value;
|
371
371
|
return uri;
|
372
372
|
}
|
@@ -398,7 +398,7 @@
|
|
398
398
|
let searchDistance: number;
|
399
399
|
let searchCall: number;
|
400
400
|
const array = this._switcher.scenes;
|
401
|
-
|
401
|
+
const interval = setInterval(() => {
|
402
402
|
if (this.allLoaded()) {
|
403
403
|
if (debug)
|
404
404
|
console.log("All scenes loaded");
|
@@ -419,7 +419,7 @@
|
|
419
419
|
searchCall += 1;
|
420
420
|
const maxSearchDistance = searchForward ? this.maxLoadAhead : this.maxLoadBehind;
|
421
421
|
if (searchDistance > maxSearchDistance) return;
|
422
|
-
|
422
|
+
const roomIndex = searchForward ? lastRoom + searchDistance : lastRoom - searchDistance;
|
423
423
|
if (roomIndex < 0) return;
|
424
424
|
// if (roomIndex < 0) roomIndex = array.length + roomIndex;
|
425
425
|
if (roomIndex < 0 || roomIndex >= array.length) return;
|
@@ -556,7 +556,7 @@
|
|
556
556
|
call.on("stream", () => {
|
557
557
|
// workaround for https://github.com/peers/peerjs/issues/636
|
558
558
|
let intervalCounter = 0;
|
559
|
-
|
559
|
+
const closeInterval = setInterval(() => {
|
560
560
|
const isFirstInterval = intervalCounter === 0;
|
561
561
|
if (!handle.isOpen && isFirstInterval) {
|
562
562
|
intervalCounter += 1;
|
@@ -64,7 +64,7 @@
|
|
64
64
|
|
65
65
|
invoke(sig: SignalAsset | string) {
|
66
66
|
if (!this.events || !Array.isArray(this.events)) return;
|
67
|
-
|
67
|
+
const id = typeof sig === "object" ? sig.guid : sig;
|
68
68
|
for (const evt of this.events) {
|
69
69
|
if (evt.signal.guid === id) {
|
70
70
|
try {
|
@@ -40,7 +40,7 @@
|
|
40
40
|
const context = args.context;
|
41
41
|
const skyboxImage = context.domElement.getAttribute("skybox-image");
|
42
42
|
const environmentImage = context.domElement.getAttribute("environment-image");
|
43
|
-
|
43
|
+
const promises = new Array<Promise<any>>();
|
44
44
|
|
45
45
|
if (skyboxImage) {
|
46
46
|
if (debug) console.log("Creating remote skybox to load " + skyboxImage);
|
@@ -174,7 +174,7 @@
|
|
174
174
|
const cached = tryGetPreviouslyLoadedTexture(url);
|
175
175
|
if (cached) {
|
176
176
|
const res = await cached;
|
177
|
-
if (res.source?.data?.length > 0) return res;
|
177
|
+
if (res.source?.data?.length > 0 || res.source?.data?.data?.length) return res;
|
178
178
|
}
|
179
179
|
const isEXR = url.endsWith(".exr");
|
180
180
|
const isHdr = url.endsWith(".hdr");
|
@@ -106,7 +106,7 @@
|
|
106
106
|
if (index < 0 || index >= this.spriteSheet.sprites.length)
|
107
107
|
return;
|
108
108
|
const slice = this.spriteSheet.sprites[index];
|
109
|
-
|
109
|
+
const tex = slice?.texture;
|
110
110
|
if (!tex) return;
|
111
111
|
tex.colorSpace = THREE.SRGBColorSpace;
|
112
112
|
if (tex.minFilter == NearestFilter && tex.magFilter == NearestFilter)
|
@@ -1,208 +1,208 @@
|
|
1
|
-
import { NetworkConnection } from "../engine/engine_networking.js";
|
2
|
-
import { Behaviour, GameObject } from "./Component.js";
|
3
|
-
import { Camera } from "./Camera.js";
|
4
|
-
import * as utils from "../engine/engine_three_utils.js"
|
5
|
-
import { WebXR } from "./webxr/WebXR.js";
|
6
|
-
import { Builder } from "flatbuffers";
|
7
|
-
import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
|
8
|
-
import { Vec3 } from "../engine-schemes/vec3.js";
|
9
|
-
import { registerBinaryType } from "../engine-schemes/schemes.js";
|
10
|
-
import { InstancingUtil } from "../engine/engine_instancing.js";
|
11
|
-
import { serializable } from "../engine/engine_serialization_decorator.js";
|
12
|
-
import { Object3D } from "three";
|
13
|
-
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
14
|
-
import { AssetReference } from "../engine/engine_addressables.js";
|
15
|
-
import { ViewDevice } from "../engine/engine_playerview.js";
|
16
|
-
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
17
|
-
|
18
|
-
const SyncedCameraModelIdentifier = "SCAM";
|
19
|
-
registerBinaryType(SyncedCameraModelIdentifier, SyncedCameraModel.getRootAsSyncedCameraModel);
|
20
|
-
const builder = new Builder();
|
21
|
-
|
22
|
-
// enum CameraSyncEvent {
|
23
|
-
// Update = "sync-update-camera",
|
24
|
-
// }
|
25
|
-
|
26
|
-
class CameraModel {
|
27
|
-
userId: string;
|
28
|
-
guid: string;
|
29
|
-
// dontSave: boolean = true;
|
30
|
-
// pos: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
|
31
|
-
// rot: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
|
32
|
-
|
33
|
-
constructor(connectionId: string, guid: string) {
|
34
|
-
this.guid = guid;
|
35
|
-
this.userId = connectionId;
|
36
|
-
}
|
37
|
-
|
38
|
-
send(cam: THREE.Camera | null | undefined, con: NetworkConnection) {
|
39
|
-
if (cam) {
|
40
|
-
builder.clear();
|
41
|
-
const guid = builder.createString(this.guid);
|
42
|
-
const userId = builder.createString(this.userId);
|
43
|
-
SyncedCameraModel.startSyncedCameraModel(builder);
|
44
|
-
SyncedCameraModel.addGuid(builder, guid);
|
45
|
-
SyncedCameraModel.addUserId(builder, userId);
|
46
|
-
const p = utils.getWorldPosition(cam);
|
47
|
-
const r = utils.getWorldRotation(cam);
|
48
|
-
SyncedCameraModel.addPos(builder, Vec3.createVec3(builder, p.x, p.y, p.z));
|
49
|
-
SyncedCameraModel.addRot(builder, Vec3.createVec3(builder, r.x, r.y, r.z));
|
50
|
-
const offset = SyncedCameraModel.endSyncedCameraModel(builder);
|
51
|
-
builder.finish(offset, SyncedCameraModelIdentifier);
|
52
|
-
con.sendBinary(builder.asUint8Array());
|
53
|
-
}
|
54
|
-
}
|
55
|
-
}
|
56
|
-
|
57
|
-
declare type UserCamInfo = {
|
58
|
-
obj: THREE.Object3D,
|
59
|
-
lastUpdate: number;
|
60
|
-
userId: string;
|
61
|
-
};
|
62
|
-
|
63
|
-
export class SyncedCamera extends Behaviour {
|
64
|
-
|
65
|
-
static instances: UserCamInfo[] = [];
|
66
|
-
|
67
|
-
getCameraObject(userId: string): THREE.Object3D | null {
|
68
|
-
const guid = this.userToCamMap[userId];
|
69
|
-
if (!guid) return null;
|
70
|
-
return this.remoteCams[guid].obj;
|
71
|
-
}
|
72
|
-
|
73
|
-
@serializable([Object3D, AssetReference])
|
74
|
-
public cameraPrefab: THREE.Object3D | null | AssetReference = null;
|
75
|
-
|
76
|
-
private _lastWorldPosition!: THREE.Vector3;
|
77
|
-
private _lastWorldQuaternion!: THREE.Quaternion;
|
78
|
-
private _model: CameraModel | null = null;
|
79
|
-
private _needsUpdate: boolean = true;
|
80
|
-
private _lastUpdateTime: number = 0;
|
81
|
-
|
82
|
-
private remoteCams: { [id: string]: UserCamInfo } = {};
|
83
|
-
private userToCamMap: { [id: string]: string } = {};
|
84
|
-
private _camTimeoutInSeconds = 10;
|
85
|
-
private _receiveCallback: Function | null = null;
|
86
|
-
|
87
|
-
async awake() {
|
88
|
-
this._lastWorldPosition = this.worldPosition.clone();
|
89
|
-
this._lastWorldQuaternion = this.worldQuaternion.clone();
|
90
|
-
|
91
|
-
if (this.cameraPrefab) {
|
92
|
-
|
93
|
-
if ("uri" in this.cameraPrefab) {
|
94
|
-
this.cameraPrefab = await this.cameraPrefab.instantiate(this.gameObject);
|
95
|
-
}
|
96
|
-
|
97
|
-
if (this.cameraPrefab && "isObject3D" in this.cameraPrefab) {
|
98
|
-
this.cameraPrefab.visible = false;
|
99
|
-
}
|
100
|
-
}
|
101
|
-
|
102
|
-
}
|
103
|
-
|
104
|
-
onEnable(): void {
|
105
|
-
this._receiveCallback = this.context.connection.beginListenBinary(SyncedCameraModelIdentifier, this.onReceivedRemoteCameraInfoBin.bind(this));
|
106
|
-
}
|
107
|
-
|
108
|
-
onDisable(): void {
|
109
|
-
this.context.connection.stopListenBinary(SyncedCameraModelIdentifier, this._receiveCallback);
|
110
|
-
}
|
111
|
-
|
112
|
-
update(): void {
|
113
|
-
|
114
|
-
for (const guid in this.remoteCams) {
|
115
|
-
const cam = this.remoteCams[guid];
|
116
|
-
const timeDiff = this.context.time.realtimeSinceStartup - cam.lastUpdate;
|
117
|
-
if (!cam || (timeDiff) > this._camTimeoutInSeconds) {
|
118
|
-
console.log("Remote cam timeout", cam, timeDiff);
|
119
|
-
if (cam?.obj) {
|
120
|
-
GameObject.destroy(cam.obj);
|
121
|
-
}
|
122
|
-
delete this.remoteCams[guid];
|
123
|
-
if (cam)
|
124
|
-
delete this.userToCamMap[cam.userId];
|
125
|
-
|
126
|
-
SyncedCamera.instances.push(cam);
|
127
|
-
this.context.players.removePlayerView(cam.userId, ViewDevice.Browser);
|
128
|
-
continue;
|
129
|
-
}
|
130
|
-
}
|
131
|
-
|
132
|
-
if (WebXR.IsInWebXR) return;
|
133
|
-
|
134
|
-
const cam = this.context.mainCamera
|
135
|
-
if (cam === null) {
|
136
|
-
this.enabled = false;
|
137
|
-
return;
|
138
|
-
}
|
139
|
-
|
140
|
-
if (!this.context.connection.isConnected || this.context.connection.connectionId === null) return;
|
141
|
-
|
142
|
-
if (this._model === null) {
|
143
|
-
this._model = new CameraModel(this.context.connection.connectionId, this.context.connection.connectionId + "_camera");
|
144
|
-
}
|
145
|
-
|
146
|
-
const wp = utils.getWorldPosition(cam);
|
147
|
-
const wq = utils.getWorldQuaternion(cam);
|
148
|
-
if (wp.distanceTo(this._lastWorldPosition) > 0.001 || wq.angleTo(this._lastWorldQuaternion) > 0.01) {
|
149
|
-
this._needsUpdate = true;
|
150
|
-
}
|
151
|
-
this._lastWorldPosition.copy(wp);
|
152
|
-
this._lastWorldQuaternion.copy(wq);
|
153
|
-
|
154
|
-
if (!this._needsUpdate || this.context.time.frameCount % 2 !== 0) {
|
155
|
-
if (this.context.time.realtimeSinceStartup - this._lastUpdateTime > this._camTimeoutInSeconds * .5) {
|
156
|
-
// send update anyways to avoid timeout
|
157
|
-
}
|
158
|
-
else return;
|
159
|
-
}
|
160
|
-
|
161
|
-
this._lastUpdateTime = this.context.time.realtimeSinceStartup;
|
162
|
-
this._needsUpdate = false;
|
163
|
-
this._model.send(cam, this.context.connection);
|
164
|
-
if (!this.context.isInXR)
|
165
|
-
this.context.players.setPlayerView(this.context.connection.connectionId, cam, ViewDevice.Browser);
|
166
|
-
}
|
167
|
-
|
168
|
-
private onReceivedRemoteCameraInfoBin(model: SyncedCameraModel) {
|
169
|
-
const guid = model.guid();
|
170
|
-
if (!guid) return;
|
171
|
-
const userId = model.userId();
|
172
|
-
if (!userId) return;
|
173
|
-
if (!this.context.connection.userIsInRoom(userId)) return;
|
174
|
-
if (!this.cameraPrefab) return;
|
175
|
-
let rc = this.remoteCams[guid];
|
176
|
-
if (!rc) {
|
177
|
-
if ("isObject3D" in this.cameraPrefab) {
|
178
|
-
const opt = new InstantiateOptions();
|
179
|
-
opt.context = this.context;
|
180
|
-
const instance = GameObject.instantiate(this.cameraPrefab, opt) as GameObject;
|
181
|
-
rc = this.remoteCams[guid] = { obj: instance, lastUpdate: this.context.time.realtimeSinceStartup, userId: userId };
|
182
|
-
rc.obj.visible = true;
|
183
|
-
this.gameObject.add(instance);
|
184
|
-
this.userToCamMap[userId] = guid;
|
185
|
-
SyncedCamera.instances.push(rc);
|
186
|
-
|
187
|
-
const marker = GameObject.getOrAddComponent(instance, AvatarMarker);
|
188
|
-
marker.connectionId = userId;
|
189
|
-
marker.avatar = instance;
|
190
|
-
|
191
|
-
}
|
192
|
-
else {
|
193
|
-
return;
|
194
|
-
}
|
195
|
-
// console.log(this.remoteCams);
|
196
|
-
}
|
197
|
-
const obj = rc.obj;
|
198
|
-
this.context.players.setPlayerView(userId, obj, ViewDevice.Browser);
|
199
|
-
rc.lastUpdate = this.context.time.realtimeSinceStartup;
|
200
|
-
InstancingUtil.markDirty(obj);
|
201
|
-
const pos = model.pos();
|
202
|
-
if (pos)
|
203
|
-
utils.setWorldPositionXYZ(obj, pos.x(), pos.y(), pos.z());
|
204
|
-
const rot = model.rot();
|
205
|
-
if (rot)
|
206
|
-
utils.setWorldRotationXYZ(obj, rot.x(), rot.y(), rot.z());
|
207
|
-
}
|
1
|
+
import { NetworkConnection } from "../engine/engine_networking.js";
|
2
|
+
import { Behaviour, GameObject } from "./Component.js";
|
3
|
+
import { Camera } from "./Camera.js";
|
4
|
+
import * as utils from "../engine/engine_three_utils.js"
|
5
|
+
import { WebXR } from "./webxr/WebXR.js";
|
6
|
+
import { Builder } from "flatbuffers";
|
7
|
+
import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
|
8
|
+
import { Vec3 } from "../engine-schemes/vec3.js";
|
9
|
+
import { registerBinaryType } from "../engine-schemes/schemes.js";
|
10
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
11
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
12
|
+
import { Object3D } from "three";
|
13
|
+
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
14
|
+
import { AssetReference } from "../engine/engine_addressables.js";
|
15
|
+
import { ViewDevice } from "../engine/engine_playerview.js";
|
16
|
+
import { InstantiateOptions } from "../engine/engine_gameobject.js";
|
17
|
+
|
18
|
+
const SyncedCameraModelIdentifier = "SCAM";
|
19
|
+
registerBinaryType(SyncedCameraModelIdentifier, SyncedCameraModel.getRootAsSyncedCameraModel);
|
20
|
+
const builder = new Builder();
|
21
|
+
|
22
|
+
// enum CameraSyncEvent {
|
23
|
+
// Update = "sync-update-camera",
|
24
|
+
// }
|
25
|
+
|
26
|
+
class CameraModel {
|
27
|
+
userId: string;
|
28
|
+
guid: string;
|
29
|
+
// dontSave: boolean = true;
|
30
|
+
// pos: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
|
31
|
+
// rot: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
|
32
|
+
|
33
|
+
constructor(connectionId: string, guid: string) {
|
34
|
+
this.guid = guid;
|
35
|
+
this.userId = connectionId;
|
36
|
+
}
|
37
|
+
|
38
|
+
send(cam: THREE.Camera | null | undefined, con: NetworkConnection) {
|
39
|
+
if (cam) {
|
40
|
+
builder.clear();
|
41
|
+
const guid = builder.createString(this.guid);
|
42
|
+
const userId = builder.createString(this.userId);
|
43
|
+
SyncedCameraModel.startSyncedCameraModel(builder);
|
44
|
+
SyncedCameraModel.addGuid(builder, guid);
|
45
|
+
SyncedCameraModel.addUserId(builder, userId);
|
46
|
+
const p = utils.getWorldPosition(cam);
|
47
|
+
const r = utils.getWorldRotation(cam);
|
48
|
+
SyncedCameraModel.addPos(builder, Vec3.createVec3(builder, p.x, p.y, p.z));
|
49
|
+
SyncedCameraModel.addRot(builder, Vec3.createVec3(builder, r.x, r.y, r.z));
|
50
|
+
const offset = SyncedCameraModel.endSyncedCameraModel(builder);
|
51
|
+
builder.finish(offset, SyncedCameraModelIdentifier);
|
52
|
+
con.sendBinary(builder.asUint8Array());
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
declare type UserCamInfo = {
|
58
|
+
obj: THREE.Object3D,
|
59
|
+
lastUpdate: number;
|
60
|
+
userId: string;
|
61
|
+
};
|
62
|
+
|
63
|
+
export class SyncedCamera extends Behaviour {
|
64
|
+
|
65
|
+
static instances: UserCamInfo[] = [];
|
66
|
+
|
67
|
+
getCameraObject(userId: string): THREE.Object3D | null {
|
68
|
+
const guid = this.userToCamMap[userId];
|
69
|
+
if (!guid) return null;
|
70
|
+
return this.remoteCams[guid].obj;
|
71
|
+
}
|
72
|
+
|
73
|
+
@serializable([Object3D, AssetReference])
|
74
|
+
public cameraPrefab: THREE.Object3D | null | AssetReference = null;
|
75
|
+
|
76
|
+
private _lastWorldPosition!: THREE.Vector3;
|
77
|
+
private _lastWorldQuaternion!: THREE.Quaternion;
|
78
|
+
private _model: CameraModel | null = null;
|
79
|
+
private _needsUpdate: boolean = true;
|
80
|
+
private _lastUpdateTime: number = 0;
|
81
|
+
|
82
|
+
private remoteCams: { [id: string]: UserCamInfo } = {};
|
83
|
+
private userToCamMap: { [id: string]: string } = {};
|
84
|
+
private _camTimeoutInSeconds = 10;
|
85
|
+
private _receiveCallback: Function | null = null;
|
86
|
+
|
87
|
+
async awake() {
|
88
|
+
this._lastWorldPosition = this.worldPosition.clone();
|
89
|
+
this._lastWorldQuaternion = this.worldQuaternion.clone();
|
90
|
+
|
91
|
+
if (this.cameraPrefab) {
|
92
|
+
|
93
|
+
if ("uri" in this.cameraPrefab) {
|
94
|
+
this.cameraPrefab = await this.cameraPrefab.instantiate(this.gameObject);
|
95
|
+
}
|
96
|
+
|
97
|
+
if (this.cameraPrefab && "isObject3D" in this.cameraPrefab) {
|
98
|
+
this.cameraPrefab.visible = false;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
}
|
103
|
+
|
104
|
+
onEnable(): void {
|
105
|
+
this._receiveCallback = this.context.connection.beginListenBinary(SyncedCameraModelIdentifier, this.onReceivedRemoteCameraInfoBin.bind(this));
|
106
|
+
}
|
107
|
+
|
108
|
+
onDisable(): void {
|
109
|
+
this.context.connection.stopListenBinary(SyncedCameraModelIdentifier, this._receiveCallback);
|
110
|
+
}
|
111
|
+
|
112
|
+
update(): void {
|
113
|
+
|
114
|
+
for (const guid in this.remoteCams) {
|
115
|
+
const cam = this.remoteCams[guid];
|
116
|
+
const timeDiff = this.context.time.realtimeSinceStartup - cam.lastUpdate;
|
117
|
+
if (!cam || (timeDiff) > this._camTimeoutInSeconds) {
|
118
|
+
console.log("Remote cam timeout", cam, timeDiff);
|
119
|
+
if (cam?.obj) {
|
120
|
+
GameObject.destroy(cam.obj);
|
121
|
+
}
|
122
|
+
delete this.remoteCams[guid];
|
123
|
+
if (cam)
|
124
|
+
delete this.userToCamMap[cam.userId];
|
125
|
+
|
126
|
+
SyncedCamera.instances.push(cam);
|
127
|
+
this.context.players.removePlayerView(cam.userId, ViewDevice.Browser);
|
128
|
+
continue;
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
if (WebXR.IsInWebXR) return;
|
133
|
+
|
134
|
+
const cam = this.context.mainCamera
|
135
|
+
if (cam === null) {
|
136
|
+
this.enabled = false;
|
137
|
+
return;
|
138
|
+
}
|
139
|
+
|
140
|
+
if (!this.context.connection.isConnected || this.context.connection.connectionId === null) return;
|
141
|
+
|
142
|
+
if (this._model === null) {
|
143
|
+
this._model = new CameraModel(this.context.connection.connectionId, this.context.connection.connectionId + "_camera");
|
144
|
+
}
|
145
|
+
|
146
|
+
const wp = utils.getWorldPosition(cam);
|
147
|
+
const wq = utils.getWorldQuaternion(cam);
|
148
|
+
if (wp.distanceTo(this._lastWorldPosition) > 0.001 || wq.angleTo(this._lastWorldQuaternion) > 0.01) {
|
149
|
+
this._needsUpdate = true;
|
150
|
+
}
|
151
|
+
this._lastWorldPosition.copy(wp);
|
152
|
+
this._lastWorldQuaternion.copy(wq);
|
153
|
+
|
154
|
+
if (!this._needsUpdate || this.context.time.frameCount % 2 !== 0) {
|
155
|
+
if (this.context.time.realtimeSinceStartup - this._lastUpdateTime > this._camTimeoutInSeconds * .5) {
|
156
|
+
// send update anyways to avoid timeout
|
157
|
+
}
|
158
|
+
else return;
|
159
|
+
}
|
160
|
+
|
161
|
+
this._lastUpdateTime = this.context.time.realtimeSinceStartup;
|
162
|
+
this._needsUpdate = false;
|
163
|
+
this._model.send(cam, this.context.connection);
|
164
|
+
if (!this.context.isInXR)
|
165
|
+
this.context.players.setPlayerView(this.context.connection.connectionId, cam, ViewDevice.Browser);
|
166
|
+
}
|
167
|
+
|
168
|
+
private onReceivedRemoteCameraInfoBin(model: SyncedCameraModel) {
|
169
|
+
const guid = model.guid();
|
170
|
+
if (!guid) return;
|
171
|
+
const userId = model.userId();
|
172
|
+
if (!userId) return;
|
173
|
+
if (!this.context.connection.userIsInRoom(userId)) return;
|
174
|
+
if (!this.cameraPrefab) return;
|
175
|
+
let rc = this.remoteCams[guid];
|
176
|
+
if (!rc) {
|
177
|
+
if ("isObject3D" in this.cameraPrefab) {
|
178
|
+
const opt = new InstantiateOptions();
|
179
|
+
opt.context = this.context;
|
180
|
+
const instance = GameObject.instantiate(this.cameraPrefab, opt) as GameObject;
|
181
|
+
rc = this.remoteCams[guid] = { obj: instance, lastUpdate: this.context.time.realtimeSinceStartup, userId: userId };
|
182
|
+
rc.obj.visible = true;
|
183
|
+
this.gameObject.add(instance);
|
184
|
+
this.userToCamMap[userId] = guid;
|
185
|
+
SyncedCamera.instances.push(rc);
|
186
|
+
|
187
|
+
const marker = GameObject.getOrAddComponent(instance, AvatarMarker);
|
188
|
+
marker.connectionId = userId;
|
189
|
+
marker.avatar = instance;
|
190
|
+
|
191
|
+
}
|
192
|
+
else {
|
193
|
+
return;
|
194
|
+
}
|
195
|
+
// console.log(this.remoteCams);
|
196
|
+
}
|
197
|
+
const obj = rc.obj;
|
198
|
+
this.context.players.setPlayerView(userId, obj, ViewDevice.Browser);
|
199
|
+
rc.lastUpdate = this.context.time.realtimeSinceStartup;
|
200
|
+
InstancingUtil.markDirty(obj);
|
201
|
+
const pos = model.pos();
|
202
|
+
if (pos)
|
203
|
+
utils.setWorldPositionXYZ(obj, pos.x(), pos.y(), pos.z());
|
204
|
+
const rot = model.rot();
|
205
|
+
if (rot)
|
206
|
+
utils.setWorldRotationXYZ(obj, rot.x(), rot.y(), rot.z());
|
207
|
+
}
|
208
208
|
}
|
@@ -1,337 +1,337 @@
|
|
1
|
-
import * as THREE from 'three'
|
2
|
-
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
|
3
|
-
import { Behaviour, GameObject } from "./Component.js";
|
4
|
-
import { Rigidbody } from "./RigidBody.js";
|
5
|
-
import * as utils from "../engine/engine_utils.js"
|
6
|
-
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
|
7
|
-
import { InstancingUtil } from "../engine/engine_instancing.js";
|
8
|
-
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
|
9
|
-
import * as flatbuffers from "flatbuffers";
|
10
|
-
import { Transform } from '../engine-schemes/transform.js';
|
11
|
-
import { registerBinaryType } from '../engine-schemes/schemes.js';
|
12
|
-
import { setWorldEuler } from '../engine/engine_three_utils.js';
|
13
|
-
|
14
|
-
const debug = utils.getParam("debugsync");
|
15
|
-
export const SyncedTransformIdentifier = "STRS";
|
16
|
-
registerBinaryType(SyncedTransformIdentifier, SyncedTransformModel.getRootAsSyncedTransformModel);
|
17
|
-
|
18
|
-
const builder = new flatbuffers.Builder();
|
19
|
-
|
20
|
-
export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
|
21
|
-
builder.clear();
|
22
|
-
const guidObj = builder.createString(guid);
|
23
|
-
SyncedTransformModel.startSyncedTransformModel(builder);
|
24
|
-
SyncedTransformModel.addGuid(builder, guidObj);
|
25
|
-
SyncedTransformModel.addFast(builder, fast);
|
26
|
-
const p = b.worldPosition;
|
27
|
-
const r = b.worldEuler;
|
28
|
-
const s = b.gameObject.scale; // todo: world scale
|
29
|
-
// console.log(p, r, s);
|
30
|
-
SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
|
31
|
-
const res = SyncedTransformModel.endSyncedTransformModel(builder);
|
32
|
-
// SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
|
33
|
-
builder.finish(res, SyncedTransformIdentifier);
|
34
|
-
return builder.asUint8Array();
|
35
|
-
}
|
36
|
-
|
37
|
-
|
38
|
-
export class SyncedTransform extends Behaviour {
|
39
|
-
|
40
|
-
// public autoOwnership: boolean = true;
|
41
|
-
public overridePhysics: boolean = true
|
42
|
-
public interpolatePosition: boolean = true;
|
43
|
-
public interpolateRotation: boolean = true;
|
44
|
-
public fastMode: boolean = false;
|
45
|
-
public syncDestroy: boolean = false;
|
46
|
-
|
47
|
-
// private _state!: SyncedTransformModel;
|
48
|
-
private _model: OwnershipModel | null = null;
|
49
|
-
private _needsUpdate: boolean = true;
|
50
|
-
private rb: Rigidbody | null = null;
|
51
|
-
private _wasKinematic: boolean | undefined = false;
|
52
|
-
private _receivedDataBefore: boolean = false;
|
53
|
-
|
54
|
-
private _targetPosition!: THREE.Vector3;
|
55
|
-
private _targetRotation!: THREE.Quaternion;
|
56
|
-
|
57
|
-
private _receivedFastUpdate: boolean = false;
|
58
|
-
private _shouldRequestOwnership: boolean = false;
|
59
|
-
|
60
|
-
public requestOwnership() {
|
61
|
-
if (debug)
|
62
|
-
console.log("Request ownership");
|
63
|
-
if (!this._model) {
|
64
|
-
this._shouldRequestOwnership = true;
|
65
|
-
this._needsUpdate = true;
|
66
|
-
}
|
67
|
-
else
|
68
|
-
this._model.requestOwnership();
|
69
|
-
}
|
70
|
-
|
71
|
-
public hasOwnership(): boolean | undefined {
|
72
|
-
return this._model?.hasOwnership ?? undefined;
|
73
|
-
}
|
74
|
-
|
75
|
-
public isOwned(): boolean | undefined {
|
76
|
-
return this._model?.isOwned;
|
77
|
-
}
|
78
|
-
|
79
|
-
private joinedRoomCallback: any = null;
|
80
|
-
private receivedDataCallback: any = null;
|
81
|
-
|
82
|
-
awake() {
|
83
|
-
if (debug)
|
84
|
-
console.log("new instance", this.guid, this);
|
85
|
-
this._receivedDataBefore = false;
|
86
|
-
this._targetPosition = new THREE.Vector3();
|
87
|
-
this._targetRotation = new THREE.Quaternion();
|
88
|
-
|
89
|
-
// sync instantiate issue was because they shared the same last pos vector!
|
90
|
-
this.lastWorldPos = new THREE.Vector3();
|
91
|
-
this.lastWorldRotation = new THREE.Quaternion();
|
92
|
-
|
93
|
-
this.rb = GameObject.getComponentInChildren(this.gameObject, Rigidbody);
|
94
|
-
if (this.rb) {
|
95
|
-
this._wasKinematic = this.rb.isKinematic;
|
96
|
-
}
|
97
|
-
|
98
|
-
this.receivedUpdate = true;
|
99
|
-
// this._state = new TransformModel(this.guid, this);
|
100
|
-
this._model = new OwnershipModel(this.context.connection, this.guid);
|
101
|
-
if (this.context.connection.isConnected) {
|
102
|
-
this.tryGetLastState();
|
103
|
-
}
|
104
|
-
|
105
|
-
this.joinedRoomCallback = this.tryGetLastState.bind(this);
|
106
|
-
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
|
107
|
-
this.receivedDataCallback = this.onReceivedData.bind(this);
|
108
|
-
this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
|
109
|
-
}
|
110
|
-
|
111
|
-
onDestroy(): void {
|
112
|
-
// TODO: can we add a new component for this?! do we really need this?!
|
113
|
-
if (this.syncDestroy)
|
114
|
-
sendDestroyed(this.guid, this.context.connection);
|
115
|
-
this._model = null;
|
116
|
-
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
|
117
|
-
this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
|
118
|
-
}
|
119
|
-
|
120
|
-
private tryGetLastState() {
|
121
|
-
const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
|
122
|
-
if (model) this.onReceivedData(model);
|
123
|
-
}
|
124
|
-
|
125
|
-
private tempEuler: THREE.Euler = new THREE.Euler();
|
126
|
-
|
127
|
-
private onReceivedData(data: SyncedTransformModel) {
|
128
|
-
if (this.destroyed) return;
|
129
|
-
if (typeof data.guid === "function" && data.guid() === this.guid) {
|
130
|
-
if (debug)
|
131
|
-
console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
|
132
|
-
this.receivedUpdate = true;
|
133
|
-
this._receivedFastUpdate = data.fast();
|
134
|
-
const transform = data.transform();
|
135
|
-
if (transform) {
|
136
|
-
InstancingUtil.markDirty(this.gameObject, true);
|
137
|
-
const position = transform.position();
|
138
|
-
if (position) {
|
139
|
-
if (this.interpolatePosition)
|
140
|
-
this._targetPosition?.set(position.x(), position.y(), position.z());
|
141
|
-
if (!this.interpolatePosition || !this._receivedDataBefore)
|
142
|
-
this.setWorldPosition(position.x(), position.y(), position.z());
|
143
|
-
}
|
144
|
-
|
145
|
-
const rotation = transform.rotation();
|
146
|
-
if (rotation) {
|
147
|
-
this.tempEuler.set(rotation.x(), rotation.y(), rotation.z());
|
148
|
-
if (this.interpolateRotation) {
|
149
|
-
this._targetRotation.setFromEuler(this.tempEuler);
|
150
|
-
}
|
151
|
-
if (!this.interpolateRotation || !this._receivedDataBefore)
|
152
|
-
setWorldEuler(this.gameObject, this.tempEuler);
|
153
|
-
}
|
154
|
-
}
|
155
|
-
this._receivedDataBefore = true;
|
156
|
-
|
157
|
-
// if (this.rb && !this._model?.hasOwnership) {
|
158
|
-
// this.rb.setBodyFromGameObject(data.velocity)
|
159
|
-
// }
|
160
|
-
}
|
161
|
-
}
|
162
|
-
|
163
|
-
onEnable(): void {
|
164
|
-
this.lastWorldPos.copy(this.worldPosition);
|
165
|
-
this.lastWorldRotation.copy(this.worldQuaternion);
|
166
|
-
this._needsUpdate = true;
|
167
|
-
// console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
|
168
|
-
if (this._model) {
|
169
|
-
this._model.updateIsOwned();
|
170
|
-
}
|
171
|
-
}
|
172
|
-
|
173
|
-
onDisable(): void {
|
174
|
-
if (this._model)
|
175
|
-
this._model.freeOwnership();
|
176
|
-
}
|
177
|
-
|
178
|
-
|
179
|
-
private receivedUpdate = false;
|
180
|
-
private lastWorldPos!: THREE.Vector3;
|
181
|
-
private lastWorldRotation!: THREE.Quaternion;
|
182
|
-
|
183
|
-
onBeforeRender() {
|
184
|
-
if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
|
185
|
-
// console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
|
186
|
-
|
187
|
-
if (!this.context.connection.isInRoom || !this._model) {
|
188
|
-
if (debug)
|
189
|
-
console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
|
190
|
-
return;
|
191
|
-
}
|
192
|
-
|
193
|
-
if (this._shouldRequestOwnership) {
|
194
|
-
this._shouldRequestOwnership = false;
|
195
|
-
this._model.requestOwnership();
|
196
|
-
}
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
if (this._model.isOwned && !this.receivedUpdate) {
|
201
|
-
const worlddiff = wp.distanceTo(this.lastWorldPos);
|
202
|
-
const worldRot = wr.angleTo(this.lastWorldRotation);
|
203
|
-
const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
|
204
|
-
if (worlddiff > threshold || worldRot > threshold) {
|
205
|
-
// console.log(worlddiff, worldRot);
|
206
|
-
if (!this._model.hasOwnership) {
|
207
|
-
|
208
|
-
if (debug)
|
209
|
-
console.log(this.guid, "reset because not owned but", this.gameObject.name, this.lastWorldPos);
|
210
|
-
|
211
|
-
this.worldPosition = this.lastWorldPos;
|
212
|
-
wp.copy(this.lastWorldPos);
|
213
|
-
|
214
|
-
this.worldQuaternion = this.lastWorldRotation;
|
215
|
-
wr.copy(this.lastWorldRotation);
|
216
|
-
|
217
|
-
InstancingUtil.markDirty(this.gameObject, true);
|
218
|
-
this._needsUpdate = false;
|
219
|
-
}
|
220
|
-
else {
|
221
|
-
this._needsUpdate = true;
|
222
|
-
}
|
223
|
-
}
|
224
|
-
}
|
225
|
-
// else if (this._model.isOwned === false) {
|
226
|
-
// if (!this._didRequestOwnershipOnce && this.autoOwnership) {
|
227
|
-
// this._didRequestOwnershipOnce = true;
|
228
|
-
// this._model.requestOwnershipIfNotOwned();
|
229
|
-
// }
|
230
|
-
// }
|
231
|
-
|
232
|
-
|
233
|
-
if (this._model && !this._model.hasOwnership && this._model.isOwned) {
|
234
|
-
if (this._receivedDataBefore) {
|
235
|
-
const factor = this._receivedFastUpdate || this.fastMode ? .5 : .3;
|
236
|
-
const t = factor;//Mathf.clamp01(this.context.time.deltaTime * factor);
|
237
|
-
let requireMarkDirty = false;
|
238
|
-
if (this.interpolatePosition && this._targetPosition) {
|
239
|
-
const pos = this.worldPosition;
|
240
|
-
pos.lerp(this._targetPosition, t);
|
241
|
-
this.worldPosition = pos;
|
242
|
-
requireMarkDirty = true;
|
243
|
-
}
|
244
|
-
if (this.interpolateRotation && this._targetRotation) {
|
245
|
-
const rot = this.worldQuaternion;
|
246
|
-
rot.slerp(this._targetRotation, t);
|
247
|
-
this.worldQuaternion = rot;
|
248
|
-
requireMarkDirty = true;
|
249
|
-
}
|
250
|
-
if (requireMarkDirty)
|
251
|
-
InstancingUtil.markDirty(this.gameObject, true);
|
252
|
-
}
|
253
|
-
}
|
254
|
-
|
255
|
-
|
256
|
-
this.receivedUpdate = false;
|
257
|
-
this.lastWorldPos.copy(wp);
|
258
|
-
this.lastWorldRotation.copy(wr);
|
259
|
-
|
260
|
-
|
261
|
-
// if (this._model.isOwned === false && this.autoOwnership) {
|
262
|
-
// this.requestOwnership();
|
263
|
-
// }
|
264
|
-
|
265
|
-
if (!this._model) return;
|
266
|
-
// only run if we are the owner
|
267
|
-
if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
|
268
|
-
if (this.rb) {
|
269
|
-
this.rb.isKinematic = this._model.isOwned ?? false;
|
270
|
-
this.rb.setVelocity(0, 0, 0);
|
271
|
-
}
|
272
|
-
return;
|
273
|
-
}
|
274
|
-
|
275
|
-
// local user is owner:
|
276
|
-
|
277
|
-
if (this.rb) {
|
278
|
-
if (this._wasKinematic !== undefined) {
|
279
|
-
if (debug)
|
280
|
-
console.log("reset kinematic", this.rb.name, this._wasKinematic);
|
281
|
-
this.rb.isKinematic = this._wasKinematic;
|
282
|
-
}
|
283
|
-
|
284
|
-
// hacky reset if too far off
|
285
|
-
if (this.gameObject.position.distanceTo(new THREE.Vector3(0, 0, 0)) > 1000) {
|
286
|
-
if (debug)
|
287
|
-
console.log("RESET", this.name)
|
288
|
-
this.gameObject.position.set(0, 1, 0);
|
289
|
-
this.rb.setVelocity(0, 0, 0);
|
290
|
-
}
|
291
|
-
}
|
292
|
-
|
293
|
-
const updateInterval = 10;
|
294
|
-
const fastUpdate = this.rb || this.fastMode;
|
295
|
-
if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
|
296
|
-
|
297
|
-
if (debug)
|
298
|
-
console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
|
299
|
-
|
300
|
-
if (this.overridePhysics && this.rb) {
|
301
|
-
// this.rb.setBodyFromGameObject();
|
302
|
-
}
|
303
|
-
|
304
|
-
this._needsUpdate = false;
|
305
|
-
const st = createTransformModel(this.guid, this, fastUpdate ? true : false);
|
306
|
-
// this._state.update(this, this.rb);
|
307
|
-
// this._state.fast = fastUpdate ? true : false;
|
308
|
-
this.context.connection.sendBinary(st);
|
309
|
-
}
|
310
|
-
}
|
311
|
-
|
312
|
-
|
313
|
-
// private lastPosition: THREE.Vector3 = new THREE.Vector3();
|
314
|
-
|
315
|
-
// private async setPosition(pt: THREE.Vector3) {
|
316
|
-
|
317
|
-
// if (this._model.isConnected && !this._model?.hasOwnership) {
|
318
|
-
// await this._model?.requestOwnershipAsync();
|
319
|
-
// }
|
320
|
-
|
321
|
-
// if (pt.distanceTo(this.lastPosition) < .001) {
|
322
|
-
// return;
|
323
|
-
// }
|
324
|
-
// this.lastPosition.copy(pt);
|
325
|
-
|
326
|
-
// if(this.gameObject.parent) this.gameObject.parent.worldToLocal(pt);
|
327
|
-
// // this.gameObject.position.copy(pt);
|
328
|
-
// this.gameObject.position.set(pt.x, pt.y, pt.z);
|
329
|
-
// this._target.set(pt.x, pt.y, pt.z);
|
330
|
-
// this._needsUpdate = true;
|
331
|
-
// if (this.rb) {
|
332
|
-
// this.gameObject.position.set(pt.x, pt.y + .5, pt.z);
|
333
|
-
// this.rb.setVelocity(0, 0, 0);
|
334
|
-
// this.rb.setBodyFromGameObject();
|
335
|
-
// }
|
336
|
-
// }
|
1
|
+
import * as THREE from 'three'
|
2
|
+
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
|
3
|
+
import { Behaviour, GameObject } from "./Component.js";
|
4
|
+
import { Rigidbody } from "./RigidBody.js";
|
5
|
+
import * as utils from "../engine/engine_utils.js"
|
6
|
+
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
|
7
|
+
import { InstancingUtil } from "../engine/engine_instancing.js";
|
8
|
+
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
|
9
|
+
import * as flatbuffers from "flatbuffers";
|
10
|
+
import { Transform } from '../engine-schemes/transform.js';
|
11
|
+
import { registerBinaryType } from '../engine-schemes/schemes.js';
|
12
|
+
import { setWorldEuler } from '../engine/engine_three_utils.js';
|
13
|
+
|
14
|
+
const debug = utils.getParam("debugsync");
|
15
|
+
export const SyncedTransformIdentifier = "STRS";
|
16
|
+
registerBinaryType(SyncedTransformIdentifier, SyncedTransformModel.getRootAsSyncedTransformModel);
|
17
|
+
|
18
|
+
const builder = new flatbuffers.Builder();
|
19
|
+
|
20
|
+
export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
|
21
|
+
builder.clear();
|
22
|
+
const guidObj = builder.createString(guid);
|
23
|
+
SyncedTransformModel.startSyncedTransformModel(builder);
|
24
|
+
SyncedTransformModel.addGuid(builder, guidObj);
|
25
|
+
SyncedTransformModel.addFast(builder, fast);
|
26
|
+
const p = b.worldPosition;
|
27
|
+
const r = b.worldEuler;
|
28
|
+
const s = b.gameObject.scale; // todo: world scale
|
29
|
+
// console.log(p, r, s);
|
30
|
+
SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
|
31
|
+
const res = SyncedTransformModel.endSyncedTransformModel(builder);
|
32
|
+
// SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
|
33
|
+
builder.finish(res, SyncedTransformIdentifier);
|
34
|
+
return builder.asUint8Array();
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
export class SyncedTransform extends Behaviour {
|
39
|
+
|
40
|
+
// public autoOwnership: boolean = true;
|
41
|
+
public overridePhysics: boolean = true
|
42
|
+
public interpolatePosition: boolean = true;
|
43
|
+
public interpolateRotation: boolean = true;
|
44
|
+
public fastMode: boolean = false;
|
45
|
+
public syncDestroy: boolean = false;
|
46
|
+
|
47
|
+
// private _state!: SyncedTransformModel;
|
48
|
+
private _model: OwnershipModel | null = null;
|
49
|
+
private _needsUpdate: boolean = true;
|
50
|
+
private rb: Rigidbody | null = null;
|
51
|
+
private _wasKinematic: boolean | undefined = false;
|
52
|
+
private _receivedDataBefore: boolean = false;
|
53
|
+
|
54
|
+
private _targetPosition!: THREE.Vector3;
|
55
|
+
private _targetRotation!: THREE.Quaternion;
|
56
|
+
|
57
|
+
private _receivedFastUpdate: boolean = false;
|
58
|
+
private _shouldRequestOwnership: boolean = false;
|
59
|
+
|
60
|
+
public requestOwnership() {
|
61
|
+
if (debug)
|
62
|
+
console.log("Request ownership");
|
63
|
+
if (!this._model) {
|
64
|
+
this._shouldRequestOwnership = true;
|
65
|
+
this._needsUpdate = true;
|
66
|
+
}
|
67
|
+
else
|
68
|
+
this._model.requestOwnership();
|
69
|
+
}
|
70
|
+
|
71
|
+
public hasOwnership(): boolean | undefined {
|
72
|
+
return this._model?.hasOwnership ?? undefined;
|
73
|
+
}
|
74
|
+
|
75
|
+
public isOwned(): boolean | undefined {
|
76
|
+
return this._model?.isOwned;
|
77
|
+
}
|
78
|
+
|
79
|
+
private joinedRoomCallback: any = null;
|
80
|
+
private receivedDataCallback: any = null;
|
81
|
+
|
82
|
+
awake() {
|
83
|
+
if (debug)
|
84
|
+
console.log("new instance", this.guid, this);
|
85
|
+
this._receivedDataBefore = false;
|
86
|
+
this._targetPosition = new THREE.Vector3();
|
87
|
+
this._targetRotation = new THREE.Quaternion();
|
88
|
+
|
89
|
+
// sync instantiate issue was because they shared the same last pos vector!
|
90
|
+
this.lastWorldPos = new THREE.Vector3();
|
91
|
+
this.lastWorldRotation = new THREE.Quaternion();
|
92
|
+
|
93
|
+
this.rb = GameObject.getComponentInChildren(this.gameObject, Rigidbody);
|
94
|
+
if (this.rb) {
|
95
|
+
this._wasKinematic = this.rb.isKinematic;
|
96
|
+
}
|
97
|
+
|
98
|
+
this.receivedUpdate = true;
|
99
|
+
// this._state = new TransformModel(this.guid, this);
|
100
|
+
this._model = new OwnershipModel(this.context.connection, this.guid);
|
101
|
+
if (this.context.connection.isConnected) {
|
102
|
+
this.tryGetLastState();
|
103
|
+
}
|
104
|
+
|
105
|
+
this.joinedRoomCallback = this.tryGetLastState.bind(this);
|
106
|
+
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
|
107
|
+
this.receivedDataCallback = this.onReceivedData.bind(this);
|
108
|
+
this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
|
109
|
+
}
|
110
|
+
|
111
|
+
onDestroy(): void {
|
112
|
+
// TODO: can we add a new component for this?! do we really need this?!
|
113
|
+
if (this.syncDestroy)
|
114
|
+
sendDestroyed(this.guid, this.context.connection);
|
115
|
+
this._model = null;
|
116
|
+
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
|
117
|
+
this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
|
118
|
+
}
|
119
|
+
|
120
|
+
private tryGetLastState() {
|
121
|
+
const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
|
122
|
+
if (model) this.onReceivedData(model);
|
123
|
+
}
|
124
|
+
|
125
|
+
private tempEuler: THREE.Euler = new THREE.Euler();
|
126
|
+
|
127
|
+
private onReceivedData(data: SyncedTransformModel) {
|
128
|
+
if (this.destroyed) return;
|
129
|
+
if (typeof data.guid === "function" && data.guid() === this.guid) {
|
130
|
+
if (debug)
|
131
|
+
console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
|
132
|
+
this.receivedUpdate = true;
|
133
|
+
this._receivedFastUpdate = data.fast();
|
134
|
+
const transform = data.transform();
|
135
|
+
if (transform) {
|
136
|
+
InstancingUtil.markDirty(this.gameObject, true);
|
137
|
+
const position = transform.position();
|
138
|
+
if (position) {
|
139
|
+
if (this.interpolatePosition)
|
140
|
+
this._targetPosition?.set(position.x(), position.y(), position.z());
|
141
|
+
if (!this.interpolatePosition || !this._receivedDataBefore)
|
142
|
+
this.setWorldPosition(position.x(), position.y(), position.z());
|
143
|
+
}
|
144
|
+
|
145
|
+
const rotation = transform.rotation();
|
146
|
+
if (rotation) {
|
147
|
+
this.tempEuler.set(rotation.x(), rotation.y(), rotation.z());
|
148
|
+
if (this.interpolateRotation) {
|
149
|
+
this._targetRotation.setFromEuler(this.tempEuler);
|
150
|
+
}
|
151
|
+
if (!this.interpolateRotation || !this._receivedDataBefore)
|
152
|
+
setWorldEuler(this.gameObject, this.tempEuler);
|
153
|
+
}
|
154
|
+
}
|
155
|
+
this._receivedDataBefore = true;
|
156
|
+
|
157
|
+
// if (this.rb && !this._model?.hasOwnership) {
|
158
|
+
// this.rb.setBodyFromGameObject(data.velocity)
|
159
|
+
// }
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
onEnable(): void {
|
164
|
+
this.lastWorldPos.copy(this.worldPosition);
|
165
|
+
this.lastWorldRotation.copy(this.worldQuaternion);
|
166
|
+
this._needsUpdate = true;
|
167
|
+
// console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
|
168
|
+
if (this._model) {
|
169
|
+
this._model.updateIsOwned();
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
onDisable(): void {
|
174
|
+
if (this._model)
|
175
|
+
this._model.freeOwnership();
|
176
|
+
}
|
177
|
+
|
178
|
+
|
179
|
+
private receivedUpdate = false;
|
180
|
+
private lastWorldPos!: THREE.Vector3;
|
181
|
+
private lastWorldRotation!: THREE.Quaternion;
|
182
|
+
|
183
|
+
onBeforeRender() {
|
184
|
+
if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
|
185
|
+
// console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
|
186
|
+
|
187
|
+
if (!this.context.connection.isInRoom || !this._model) {
|
188
|
+
if (debug)
|
189
|
+
console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
|
190
|
+
return;
|
191
|
+
}
|
192
|
+
|
193
|
+
if (this._shouldRequestOwnership) {
|
194
|
+
this._shouldRequestOwnership = false;
|
195
|
+
this._model.requestOwnership();
|
196
|
+
}
|
197
|
+
|
198
|
+
const wp = this.worldPosition;
|
199
|
+
const wr = this.worldQuaternion;
|
200
|
+
if (this._model.isOwned && !this.receivedUpdate) {
|
201
|
+
const worlddiff = wp.distanceTo(this.lastWorldPos);
|
202
|
+
const worldRot = wr.angleTo(this.lastWorldRotation);
|
203
|
+
const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
|
204
|
+
if (worlddiff > threshold || worldRot > threshold) {
|
205
|
+
// console.log(worlddiff, worldRot);
|
206
|
+
if (!this._model.hasOwnership) {
|
207
|
+
|
208
|
+
if (debug)
|
209
|
+
console.log(this.guid, "reset because not owned but", this.gameObject.name, this.lastWorldPos);
|
210
|
+
|
211
|
+
this.worldPosition = this.lastWorldPos;
|
212
|
+
wp.copy(this.lastWorldPos);
|
213
|
+
|
214
|
+
this.worldQuaternion = this.lastWorldRotation;
|
215
|
+
wr.copy(this.lastWorldRotation);
|
216
|
+
|
217
|
+
InstancingUtil.markDirty(this.gameObject, true);
|
218
|
+
this._needsUpdate = false;
|
219
|
+
}
|
220
|
+
else {
|
221
|
+
this._needsUpdate = true;
|
222
|
+
}
|
223
|
+
}
|
224
|
+
}
|
225
|
+
// else if (this._model.isOwned === false) {
|
226
|
+
// if (!this._didRequestOwnershipOnce && this.autoOwnership) {
|
227
|
+
// this._didRequestOwnershipOnce = true;
|
228
|
+
// this._model.requestOwnershipIfNotOwned();
|
229
|
+
// }
|
230
|
+
// }
|
231
|
+
|
232
|
+
|
233
|
+
if (this._model && !this._model.hasOwnership && this._model.isOwned) {
|
234
|
+
if (this._receivedDataBefore) {
|
235
|
+
const factor = this._receivedFastUpdate || this.fastMode ? .5 : .3;
|
236
|
+
const t = factor;//Mathf.clamp01(this.context.time.deltaTime * factor);
|
237
|
+
let requireMarkDirty = false;
|
238
|
+
if (this.interpolatePosition && this._targetPosition) {
|
239
|
+
const pos = this.worldPosition;
|
240
|
+
pos.lerp(this._targetPosition, t);
|
241
|
+
this.worldPosition = pos;
|
242
|
+
requireMarkDirty = true;
|
243
|
+
}
|
244
|
+
if (this.interpolateRotation && this._targetRotation) {
|
245
|
+
const rot = this.worldQuaternion;
|
246
|
+
rot.slerp(this._targetRotation, t);
|
247
|
+
this.worldQuaternion = rot;
|
248
|
+
requireMarkDirty = true;
|
249
|
+
}
|
250
|
+
if (requireMarkDirty)
|
251
|
+
InstancingUtil.markDirty(this.gameObject, true);
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
|
256
|
+
this.receivedUpdate = false;
|
257
|
+
this.lastWorldPos.copy(wp);
|
258
|
+
this.lastWorldRotation.copy(wr);
|
259
|
+
|
260
|
+
|
261
|
+
// if (this._model.isOwned === false && this.autoOwnership) {
|
262
|
+
// this.requestOwnership();
|
263
|
+
// }
|
264
|
+
|
265
|
+
if (!this._model) return;
|
266
|
+
// only run if we are the owner
|
267
|
+
if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
|
268
|
+
if (this.rb) {
|
269
|
+
this.rb.isKinematic = this._model.isOwned ?? false;
|
270
|
+
this.rb.setVelocity(0, 0, 0);
|
271
|
+
}
|
272
|
+
return;
|
273
|
+
}
|
274
|
+
|
275
|
+
// local user is owner:
|
276
|
+
|
277
|
+
if (this.rb) {
|
278
|
+
if (this._wasKinematic !== undefined) {
|
279
|
+
if (debug)
|
280
|
+
console.log("reset kinematic", this.rb.name, this._wasKinematic);
|
281
|
+
this.rb.isKinematic = this._wasKinematic;
|
282
|
+
}
|
283
|
+
|
284
|
+
// hacky reset if too far off
|
285
|
+
if (this.gameObject.position.distanceTo(new THREE.Vector3(0, 0, 0)) > 1000) {
|
286
|
+
if (debug)
|
287
|
+
console.log("RESET", this.name)
|
288
|
+
this.gameObject.position.set(0, 1, 0);
|
289
|
+
this.rb.setVelocity(0, 0, 0);
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
const updateInterval = 10;
|
294
|
+
const fastUpdate = this.rb || this.fastMode;
|
295
|
+
if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
|
296
|
+
|
297
|
+
if (debug)
|
298
|
+
console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
|
299
|
+
|
300
|
+
if (this.overridePhysics && this.rb) {
|
301
|
+
// this.rb.setBodyFromGameObject();
|
302
|
+
}
|
303
|
+
|
304
|
+
this._needsUpdate = false;
|
305
|
+
const st = createTransformModel(this.guid, this, fastUpdate ? true : false);
|
306
|
+
// this._state.update(this, this.rb);
|
307
|
+
// this._state.fast = fastUpdate ? true : false;
|
308
|
+
this.context.connection.sendBinary(st);
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
|
313
|
+
// private lastPosition: THREE.Vector3 = new THREE.Vector3();
|
314
|
+
|
315
|
+
// private async setPosition(pt: THREE.Vector3) {
|
316
|
+
|
317
|
+
// if (this._model.isConnected && !this._model?.hasOwnership) {
|
318
|
+
// await this._model?.requestOwnershipAsync();
|
319
|
+
// }
|
320
|
+
|
321
|
+
// if (pt.distanceTo(this.lastPosition) < .001) {
|
322
|
+
// return;
|
323
|
+
// }
|
324
|
+
// this.lastPosition.copy(pt);
|
325
|
+
|
326
|
+
// if(this.gameObject.parent) this.gameObject.parent.worldToLocal(pt);
|
327
|
+
// // this.gameObject.position.copy(pt);
|
328
|
+
// this.gameObject.position.set(pt.x, pt.y, pt.z);
|
329
|
+
// this._target.set(pt.x, pt.y, pt.z);
|
330
|
+
// this._needsUpdate = true;
|
331
|
+
// if (this.rb) {
|
332
|
+
// this.gameObject.position.set(pt.x, pt.y + .5, pt.z);
|
333
|
+
// this.rb.setVelocity(0, 0, 0);
|
334
|
+
// this.rb.setBodyFromGameObject();
|
335
|
+
// }
|
336
|
+
// }
|
337
337
|
}
|
@@ -182,7 +182,7 @@
|
|
182
182
|
|
183
183
|
|
184
184
|
private getTextOpts(): object {
|
185
|
-
|
185
|
+
const fontSize = this.fontSize;
|
186
186
|
// if (this.canvas) {
|
187
187
|
// fontSize /= this.canvas?.scaleFactor;
|
188
188
|
// }
|
@@ -438,8 +438,8 @@
|
|
438
438
|
// - Arial instead of assets/arial
|
439
439
|
// - Arial should stay Arial instead of arial
|
440
440
|
if (!this.font) return;
|
441
|
-
|
442
|
-
|
441
|
+
const fontName = this.font;
|
442
|
+
const familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle);
|
443
443
|
if (debug) console.log("Selected font family:" + familyName);
|
444
444
|
|
445
445
|
// ensure a font family is register under this name
|
@@ -514,7 +514,7 @@
|
|
514
514
|
if (pathSeparatorIndex >= 0) {
|
515
515
|
fontBaseName = fontBaseName.substring(pathSeparatorIndex + 1);
|
516
516
|
}
|
517
|
-
|
517
|
+
const isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase();
|
518
518
|
const fontNameWithoutSuffix = familyName.substring(0, styleSeparator);
|
519
519
|
if (debug) console.log("Select font: ", familyName, FontStyle[style], fontBaseName, isUpperCase, fontNameWithoutSuffix);
|
520
520
|
|
@@ -654,7 +654,7 @@
|
|
654
654
|
function addResources( object, context: USDZExporterContext ) {
|
655
655
|
|
656
656
|
const geometry = object.geometry;
|
657
|
-
|
657
|
+
const material = object.material;
|
658
658
|
|
659
659
|
if ( geometry ) {
|
660
660
|
|
@@ -1146,8 +1146,8 @@
|
|
1146
1146
|
const rotation = texture.rotation;
|
1147
1147
|
|
1148
1148
|
// rotation is around the wrong point. after rotation we need to shift offset again so that we're rotating around the right spot
|
1149
|
-
|
1150
|
-
|
1149
|
+
const xRotationOffset = Math.sin(rotation);
|
1150
|
+
const yRotationOffset = Math.cos(rotation);
|
1151
1151
|
|
1152
1152
|
// texture coordinates start in the opposite corner, need to correct
|
1153
1153
|
offset.y = 1 - offset.y - repeat.y;
|
@@ -61,7 +61,7 @@
|
|
61
61
|
const model = models[index];
|
62
62
|
if (isActive || time >= model.start && time <= model.end) {
|
63
63
|
let weight = 1;
|
64
|
-
|
64
|
+
const isBlendingWithNext = false;
|
65
65
|
|
66
66
|
// this blending with next clips is already baked into easeIn/easeOut
|
67
67
|
// if (allowBlendWithNext && index + 1 < models.length) {
|
@@ -111,7 +111,7 @@
|
|
111
111
|
private flipWindingOrder(geometry) {
|
112
112
|
const index = geometry.index.array
|
113
113
|
for (let i = 0, il = index.length / 3; i < il; i++) {
|
114
|
-
|
114
|
+
const x = index[i * 3]
|
115
115
|
index[i * 3] = index[i * 3 + 2]
|
116
116
|
index[i * 3 + 2] = x
|
117
117
|
}
|
@@ -159,7 +159,7 @@
|
|
159
159
|
}
|
160
160
|
}
|
161
161
|
// TODO tint could be an uniform
|
162
|
-
|
162
|
+
const backgroundFragment: string = /* glsl */`
|
163
163
|
uniform sampler2D t2D;
|
164
164
|
|
165
165
|
varying vec2 vUv;
|
@@ -165,7 +165,7 @@
|
|
165
165
|
if (this.head) {
|
166
166
|
|
167
167
|
const device = this.webxr.IsInAR ? ViewDevice.Handheld : ViewDevice.Headset;
|
168
|
-
|
168
|
+
const viewObj = this.head;
|
169
169
|
// if (this.isLocalAvatar) {
|
170
170
|
// if (this.context.mainCamera && this.context.isInXR) {
|
171
171
|
// viewObj = this.context.renderer.xr.getCamera(this.context.mainCamera);
|
@@ -538,7 +538,7 @@
|
|
538
538
|
this.didChangeScale = true;
|
539
539
|
const rig = this.webXR.Rig;
|
540
540
|
if (rig) {
|
541
|
-
|
541
|
+
const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
|
542
542
|
doTeleport = args.doTeleport;
|
543
543
|
isInMiniatureMode = args.isInMiniatureMode;
|
544
544
|
newRigScale = args.newRigScale;
|
@@ -1,463 +1,463 @@
|
|
1
|
-
import { Behaviour, GameObject } from "../Component.js";
|
2
|
-
import { RoomEvents, OwnershipModel, NetworkConnection } from "../../engine/engine_networking.js";
|
3
|
-
import { WebXR, WebXREvent } from "./WebXR.js";
|
4
|
-
import { Group, Quaternion, Vector3, Vector4, WebXRManager } from "three";
|
5
|
-
import { getParam } from "../../engine/engine_utils.js";
|
6
|
-
import { Voip } from "../Voip.js";
|
7
|
-
import { Builder, Long } from "flatbuffers";
|
8
|
-
import { VrUserStateBuffer } from "../../engine-schemes/vr-user-state-buffer.js";
|
9
|
-
import { Vec3 } from "../../engine-schemes/vec3.js";
|
10
|
-
import { registerBinaryType } from "../../engine-schemes/schemes.js";
|
11
|
-
import { Vec4 } from "../../engine-schemes/vec4.js";
|
12
|
-
import { WebXRAvatar } from "./WebXRAvatar.js";
|
13
|
-
|
14
|
-
// for debug GUI
|
15
|
-
// import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
|
16
|
-
// import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
|
17
|
-
// import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
|
18
|
-
// import { renderer, sceneData } from "../engine/engine_setup.js";
|
19
|
-
|
20
|
-
const debugLogs = getParam("debugxr");
|
21
|
-
const debugAvatar = getParam("debugavatar");
|
22
|
-
// const debugAvatarVoip = getParam("debugavatarvoip");
|
23
|
-
|
24
|
-
enum WebXRSyncEvent {
|
25
|
-
WebXR_UserJoined = "webxr-user-joined",
|
26
|
-
WebXR_UserLeft = "webxr-user-left",
|
27
|
-
VRSessionStart = "vr-session-started",
|
28
|
-
VRSessionEnd = "vr-session-ended",
|
29
|
-
VRSessionUpdate = "vr-session-update",
|
30
|
-
}
|
31
|
-
|
32
|
-
enum XRMode {
|
33
|
-
VR = "vr",
|
34
|
-
AR = "ar",
|
35
|
-
}
|
36
|
-
|
37
|
-
const VRUserStateBufferIdentifier = "VRUS";
|
38
|
-
registerBinaryType(VRUserStateBufferIdentifier, VrUserStateBuffer.getRootAsVrUserStateBuffer);
|
39
|
-
|
40
|
-
function getTimeStampNow() {
|
41
|
-
return new Date().getTime(); // avoid sending millis in flatbuffer
|
42
|
-
}
|
43
|
-
|
44
|
-
function flatbuffers_long_from_number(num: number): Long {
|
45
|
-
|
46
|
-
|
47
|
-
return Long.create(low, high);
|
48
|
-
}
|
49
|
-
|
50
|
-
export class VRUserState {
|
51
|
-
public guid: string;
|
52
|
-
public time!: number;
|
53
|
-
public avatarId!: string;
|
54
|
-
public position: Vector3 = new Vector3();
|
55
|
-
public rotation: Vector4 = new Vector4();
|
56
|
-
public scale: number = 1;
|
57
|
-
|
58
|
-
public posLeftHand = new Vector3();
|
59
|
-
public posRightHand = new Vector3();
|
60
|
-
|
61
|
-
public rotLeftHand = new Quaternion();
|
62
|
-
public rotRightHand = new Quaternion();
|
63
|
-
|
64
|
-
public constructor(guid: string) {
|
65
|
-
this.guid = guid;
|
66
|
-
}
|
67
|
-
|
68
|
-
private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
69
|
-
|
70
|
-
public update(rig: Group, pos: DOMPointReadOnly, rot: DOMPointReadOnly, webXR: WebXR, avatarId: string) {
|
71
|
-
this.time = getTimeStampNow();
|
72
|
-
this.avatarId = avatarId;
|
73
|
-
this.position.set(pos.x, pos.y, pos.z);
|
74
|
-
if (rig)
|
75
|
-
this.position.applyMatrix4(rig.matrixWorld);
|
76
|
-
|
77
|
-
let q0 = VRUserState.quat0;
|
78
|
-
const q1 = VRUserState.quat1;
|
79
|
-
q0.set(rot.x, rot.y, rot.z, rot.w);
|
80
|
-
q0 = q0.multiplyQuaternions(q0, VRUserState.invertRotation);
|
81
|
-
|
82
|
-
if (rig) {
|
83
|
-
rig.getWorldQuaternion(q1);
|
84
|
-
q0.multiplyQuaternions(q1, q0);
|
85
|
-
}
|
86
|
-
|
87
|
-
this.rotation.set(q0.x, q0.y, q0.z, q0.w);
|
88
|
-
this.scale = rig.scale.x;
|
89
|
-
|
90
|
-
// for controllers, it seems we need grip pose
|
91
|
-
const ctrl0 = webXR.LeftController?.controllerGrip;
|
92
|
-
if (ctrl0) {
|
93
|
-
ctrl0.getWorldPosition(this.posLeftHand);
|
94
|
-
ctrl0.getWorldQuaternion(this.rotLeftHand);
|
95
|
-
}
|
96
|
-
const ctrl1 = webXR.RightController?.controllerGrip;
|
97
|
-
if (ctrl1) {
|
98
|
-
ctrl1.getWorldPosition(this.posRightHand);
|
99
|
-
ctrl1.getWorldQuaternion(this.rotRightHand);
|
100
|
-
}
|
101
|
-
|
102
|
-
// if this is a hand, we need to get the root bone of that / use that for position/rotation
|
103
|
-
if (webXR.LeftController?.hand?.visible) {
|
104
|
-
const wrist = webXR.LeftController.wrist;
|
105
|
-
if (wrist) {
|
106
|
-
wrist.getWorldPosition(this.posLeftHand);
|
107
|
-
wrist.getWorldQuaternion(this.rotLeftHand);
|
108
|
-
}
|
109
|
-
}
|
110
|
-
|
111
|
-
if (webXR.RightController?.hand?.visible) {
|
112
|
-
const wrist = webXR.RightController.wrist;
|
113
|
-
if (wrist) {
|
114
|
-
wrist.getWorldPosition(this.posRightHand);
|
115
|
-
wrist.getWorldQuaternion(this.rotRightHand);
|
116
|
-
}
|
117
|
-
}
|
118
|
-
}
|
119
|
-
|
120
|
-
private static quat0: Quaternion = new Quaternion();
|
121
|
-
private static quat1: Quaternion = new Quaternion();
|
122
|
-
|
123
|
-
public sendAsBuffer(builder: Builder, net: NetworkConnection) {
|
124
|
-
builder.clear();
|
125
|
-
const guid = builder.createString(this.guid);
|
126
|
-
const id = builder.createString(this.avatarId);
|
127
|
-
VrUserStateBuffer.startVrUserStateBuffer(builder);
|
128
|
-
VrUserStateBuffer.addGuid(builder, guid);
|
129
|
-
VrUserStateBuffer.addTime(builder, flatbuffers_long_from_number(this.time));
|
130
|
-
VrUserStateBuffer.addAvatarId(builder, id);
|
131
|
-
VrUserStateBuffer.addPosition(builder, Vec3.createVec3(builder, this.position.x, this.position.y, this.position.z));
|
132
|
-
VrUserStateBuffer.addRotation(builder, Vec4.createVec4(builder, this.rotation.x, this.rotation.y, this.rotation.z, this.rotation.w));
|
133
|
-
VrUserStateBuffer.addScale(builder, this.scale);
|
134
|
-
VrUserStateBuffer.addPosLeftHand(builder, Vec3.createVec3(builder, this.posLeftHand.x, this.posLeftHand.y, this.posLeftHand.z));
|
135
|
-
VrUserStateBuffer.addPosRightHand(builder, Vec3.createVec3(builder, this.posRightHand.x, this.posRightHand.y, this.posRightHand.z));
|
136
|
-
VrUserStateBuffer.addRotLeftHand(builder, Vec4.createVec4(builder, this.rotLeftHand.x, this.rotLeftHand.y, this.rotLeftHand.z, this.rotLeftHand.w));
|
137
|
-
VrUserStateBuffer.addRotRightHand(builder, Vec4.createVec4(builder, this.rotRightHand.x, this.rotRightHand.y, this.rotRightHand.z, this.rotRightHand.w));
|
138
|
-
const res = VrUserStateBuffer.endVrUserStateBuffer(builder);
|
139
|
-
builder.finish(res, VRUserStateBufferIdentifier);
|
140
|
-
const arr = builder.asUint8Array();
|
141
|
-
net.sendBinary(arr);
|
142
|
-
}
|
143
|
-
|
144
|
-
public setFromBuffer(guid: string, state: VrUserStateBuffer) {
|
145
|
-
if (!guid) return;
|
146
|
-
this.guid = guid;
|
147
|
-
this.time = state.time().toFloat64();
|
148
|
-
const id = state.avatarId();
|
149
|
-
if (id)
|
150
|
-
this.avatarId = id;
|
151
|
-
const pos = state.position();
|
152
|
-
if (pos)
|
153
|
-
this.position.set(pos.x(), pos.y(), pos.z());
|
154
|
-
// TODO: maybe just send one float more instead of converting back and forth
|
155
|
-
const rot = state.rotation();
|
156
|
-
if (rot)
|
157
|
-
this.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
|
158
|
-
const posLeftHand = state.posLeftHand();
|
159
|
-
if (posLeftHand)
|
160
|
-
this.posLeftHand.set(posLeftHand.x(), posLeftHand.y(), posLeftHand.z());
|
161
|
-
const posRightHand = state.posRightHand();
|
162
|
-
if (posRightHand)
|
163
|
-
this.posRightHand.set(posRightHand.x(), posRightHand.y(), posRightHand.z());
|
164
|
-
const rotLeftHand = state.rotLeftHand();
|
165
|
-
if (rotLeftHand)
|
166
|
-
this.rotLeftHand.set(rotLeftHand.x(), rotLeftHand.y(), rotLeftHand.z(), rotLeftHand.w());
|
167
|
-
const rotRightHand = state.rotRightHand();
|
168
|
-
if (rotRightHand)
|
169
|
-
this.rotRightHand.set(rotRightHand.x(), rotRightHand.y(), rotRightHand.z(), rotRightHand.w());
|
170
|
-
this.scale = state.scale();
|
171
|
-
}
|
172
|
-
}
|
173
|
-
|
174
|
-
export class WebXRSync extends Behaviour {
|
175
|
-
|
176
|
-
webXR: WebXR | null = null;
|
177
|
-
|
178
|
-
// private allowCustomAvatars: boolean | null = true;
|
179
|
-
|
180
|
-
private debugAvatarUser: WebXRAvatar | null = null;
|
181
|
-
private voip: Voip | null = null;
|
182
|
-
|
183
|
-
async awake() {
|
184
|
-
|
185
|
-
if(!this.webXR) this.webXR = GameObject.getComponent(this.gameObject, WebXR);
|
186
|
-
if(!this.webXR) this.webXR = GameObject.findObjectOfType(WebXR, this.context);
|
187
|
-
|
188
|
-
if(!this.webXR)
|
189
|
-
{
|
190
|
-
console.log("Missing webxr component");
|
191
|
-
this.webXR = GameObject.findObjectOfType(WebXR, this.context);
|
192
|
-
if(!this.webXR) {
|
193
|
-
console.error("Could not find webxr component");
|
194
|
-
return;
|
195
|
-
}
|
196
|
-
}
|
197
|
-
|
198
|
-
if (!this.voip) this.voip = GameObject.findObjectOfType(Voip, this.context);
|
199
|
-
|
200
|
-
if (debugAvatar) {
|
201
|
-
const debugGuid = "debug-avatar-" + debugAvatar;
|
202
|
-
const newUser = new WebXRAvatar(this.context, debugGuid, this.webXR);
|
203
|
-
// newUser.isLocalAvatar = true;
|
204
|
-
this.debugAvatarUser = newUser;
|
205
|
-
if (typeof debugAvatar === "string" && debugAvatar.length > 0) {
|
206
|
-
if (await newUser.setAvatarOverride(debugAvatar)) {
|
207
|
-
const debugState = new VRUserState(debugGuid);
|
208
|
-
debugState.position.y += 1;
|
209
|
-
const off = .5;
|
210
|
-
debugState.posLeftHand.y += off;
|
211
|
-
debugState.posLeftHand.x += off;
|
212
|
-
debugState.posRightHand.y += off;
|
213
|
-
debugState.posRightHand.x -= off;
|
214
|
-
newUser.tryUpdate(debugState, 0);
|
215
|
-
}
|
216
|
-
else {
|
217
|
-
newUser.destroy();
|
218
|
-
}
|
219
|
-
}
|
220
|
-
}
|
221
|
-
}
|
222
|
-
|
223
|
-
onEnable() {
|
224
|
-
// const debugUser = new WebXRAvatar(this.context, "sorry-no-guid", this.webXR!);
|
225
|
-
|
226
|
-
if (!this.webXR) {
|
227
|
-
this.webXR = GameObject.getComponent(this.gameObject, WebXR);
|
228
|
-
if (!this.webXR) {
|
229
|
-
console.warn("Missing webxr component on " + this.gameObject.name);
|
230
|
-
return;
|
231
|
-
}
|
232
|
-
}
|
233
|
-
|
234
|
-
this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
|
235
|
-
WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
236
|
-
this.eventSub_WebXRUpdateEvent = this.onXRSessionUpdate.bind(this);
|
237
|
-
WebXR.addEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
|
238
|
-
this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
|
239
|
-
WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
240
|
-
|
241
|
-
this.eventSub_ConnectionEvent = this.onConnected.bind(this);
|
242
|
-
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
|
243
|
-
this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserJoined, _evt => {
|
244
|
-
console.log("webxr user joined evt");
|
245
|
-
});
|
246
|
-
this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserLeft, evt => {
|
247
|
-
const hasId = evt.id !== null && evt.id !== undefined;
|
248
|
-
if (!hasId) return;
|
249
|
-
console.log("webxr user left evt");
|
250
|
-
if (hasId) {
|
251
|
-
const avatar = this.avatars[evt.id];
|
252
|
-
avatar?.destroy();
|
253
|
-
this.avatars[evt.id] = undefined;
|
254
|
-
}
|
255
|
-
});
|
256
|
-
this.context.connection.beginListenBinary(VRUserStateBufferIdentifier, (state: VrUserStateBuffer) => {
|
257
|
-
// console.log("BUFFER", state);
|
258
|
-
const guid = state.guid();
|
259
|
-
if (!guid) return;
|
260
|
-
const time = state.time().toFloat64();
|
261
|
-
const temp = this.tempState;
|
262
|
-
temp.setFromBuffer(guid, state);
|
263
|
-
// console.log(temp);
|
264
|
-
const user = this.onTryGetAvatar(guid, time);
|
265
|
-
user?.tryUpdate(temp, time);
|
266
|
-
});
|
267
|
-
this.context.connection.beginListen(WebXRSyncEvent.VRSessionUpdate, (state: VRUserState) => {
|
268
|
-
const guid = state.guid;
|
269
|
-
const time = state.time;
|
270
|
-
const user = this.onTryGetAvatar(guid, time);
|
271
|
-
user?.tryUpdate(state, time);
|
272
|
-
});
|
273
|
-
}
|
274
|
-
|
275
|
-
private tempState: VRUserState = new VRUserState("");
|
276
|
-
|
277
|
-
private onTryGetAvatar(guid: string, time: number) {
|
278
|
-
if (guid === this.context.connection.connectionId) return null; // ignore self in case we receive that also!
|
279
|
-
const timeDiff = new Date().getTime() - time;
|
280
|
-
if (timeDiff > 5000) {
|
281
|
-
if (debugLogs)
|
282
|
-
console.log("old data", timeDiff, guid)
|
283
|
-
return null;
|
284
|
-
}
|
285
|
-
let user = this.avatars[guid];
|
286
|
-
if (user === undefined) {
|
287
|
-
try {
|
288
|
-
console.log("create new avatar");
|
289
|
-
const newUser = new WebXRAvatar(this.context, guid, this.webXR!);
|
290
|
-
user = newUser;
|
291
|
-
this.avatars[guid] = newUser;
|
292
|
-
} catch (err) {
|
293
|
-
this.avatars[guid] = null;
|
294
|
-
console.error(err);
|
295
|
-
}
|
296
|
-
}
|
297
|
-
return user;
|
298
|
-
}
|
299
|
-
|
300
|
-
onDisable() {
|
301
|
-
if (this.eventSub_ConnectionEvent)
|
302
|
-
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
|
303
|
-
WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
304
|
-
WebXR.removeEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
|
305
|
-
WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
306
|
-
}
|
307
|
-
|
308
|
-
update(): void {
|
309
|
-
|
310
|
-
const now = getTimeStampNow();
|
311
|
-
|
312
|
-
if (this.debugAvatarUser) {
|
313
|
-
this.debugAvatarUser.lastUpdate = now;
|
314
|
-
}
|
315
|
-
|
316
|
-
this.detectPotentiallyDisconnectedAvatarsAndRemove();
|
317
|
-
|
318
|
-
for (const key in this.avatars) {
|
319
|
-
const avatar = this.avatars[key];
|
320
|
-
if (!avatar) continue;
|
321
|
-
avatar.update();
|
322
|
-
}
|
323
|
-
}
|
324
|
-
|
325
|
-
|
326
|
-
private _removeAvatarsList: string[] = [];
|
327
|
-
private detectPotentiallyDisconnectedAvatarsAndRemove() {
|
328
|
-
const utcnow = getTimeStampNow();
|
329
|
-
for (const key in this.avatars) {
|
330
|
-
const avatar = this.avatars[key];
|
331
|
-
if (!avatar) {
|
332
|
-
this._removeAvatarsList.push(key);
|
333
|
-
continue;
|
334
|
-
}
|
335
|
-
if (utcnow - avatar.lastUpdate > 10_000) {
|
336
|
-
console.log("avatar timed out (didnt receive any updates in a while) - destroying it now");
|
337
|
-
avatar.destroy();
|
338
|
-
this.avatars[key] = undefined;
|
339
|
-
}
|
340
|
-
}
|
341
|
-
for (const rem of this._removeAvatarsList) {
|
342
|
-
delete this.avatars[rem];
|
343
|
-
}
|
344
|
-
this._removeAvatarsList.length = 0;
|
345
|
-
}
|
346
|
-
|
347
|
-
private buildLocalAvatar() {
|
348
|
-
if (this.localAvatar) return;
|
349
|
-
const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
|
350
|
-
this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR!);
|
351
|
-
this.localAvatar.isLocalAvatar = true;
|
352
|
-
this.localAvatar.setAvatarOverride(this.getAvatarId());
|
353
|
-
this.avatars[this.localAvatar.guid] = this.localAvatar;
|
354
|
-
}
|
355
|
-
|
356
|
-
|
357
|
-
private eventSub_ConnectionEvent: Function | null = null;
|
358
|
-
private eventSub_WebXRStartEvent: Function | null = null;
|
359
|
-
private eventSub_WebXREndEvent: Function | null = null;
|
360
|
-
private eventSub_WebXRUpdateEvent: Function | null = null;
|
361
|
-
private avatars: { [key: string]: WebXRAvatar | undefined | null } = {}
|
362
|
-
private localAvatar: WebXRAvatar | null = null;
|
363
|
-
private k_LocalAvatarNoNetworkingGuid = "local";
|
364
|
-
|
365
|
-
private onConnected() {
|
366
|
-
// this event gets fired when we have joined a room and are ready to update
|
367
|
-
if (debugLogs)
|
368
|
-
console.log("Hey you are connected as " + this.context.connection.connectionId);
|
369
|
-
|
370
|
-
if (this.localAvatar?.guid === this.k_LocalAvatarNoNetworkingGuid) {
|
371
|
-
if (this.localAvatar) {
|
372
|
-
this.localAvatar?.destroy();
|
373
|
-
this.avatars[this.localAvatar.guid] = undefined;
|
374
|
-
}
|
375
|
-
this.localAvatar = null;
|
376
|
-
this.xrState = null;
|
377
|
-
this.ownership?.freeOwnership();
|
378
|
-
this.ownership = null;
|
379
|
-
}
|
380
|
-
}
|
381
|
-
|
382
|
-
private onXRSessionStart(_evt: { session: XRSession }) {
|
383
|
-
console.log("XR session started");
|
384
|
-
this.context.connection.send(WebXRSyncEvent.WebXR_UserJoined, { id: this.context.connection.connectionId, mode: XRMode.VR });
|
385
|
-
|
386
|
-
if (this.localAvatar) {
|
387
|
-
this.localAvatar?.destroy();
|
388
|
-
this.avatars[this.localAvatar.guid] = undefined;
|
389
|
-
this.localAvatar = null;
|
390
|
-
}
|
391
|
-
this.xrState = null;
|
392
|
-
this.ownership?.freeOwnership();
|
393
|
-
this.ownership = null;
|
394
|
-
|
395
|
-
if (this.avatars) {
|
396
|
-
for (const key in this.avatars) {
|
397
|
-
this.avatars[key]?.updateFlags();
|
398
|
-
}
|
399
|
-
}
|
400
|
-
}
|
401
|
-
|
402
|
-
private onXRSessionEnded(_evt: { session: XRSession }) {
|
403
|
-
console.log("XR session ended");
|
404
|
-
this.context.connection.send(WebXRSyncEvent.WebXR_UserLeft, { id: this.context.connection.connectionId, mode: XRMode.VR });
|
405
|
-
if(this.localAvatar){
|
406
|
-
this.localAvatar?.destroy();
|
407
|
-
this.avatars[this.localAvatar.guid] = undefined;
|
408
|
-
this.localAvatar = null;
|
409
|
-
}
|
410
|
-
}
|
411
|
-
|
412
|
-
private ownership: OwnershipModel | null = null;
|
413
|
-
private xrState: VRUserState | null = null;
|
414
|
-
private builder: Builder = new Builder(1024);
|
415
|
-
|
416
|
-
private onXRSessionUpdate(evt: { rig: Group, frame: XRFrame, xr: WebXRManager, input: XRInputSource[] }) {
|
417
|
-
|
418
|
-
this.xrState ??= new VRUserState(this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
|
419
|
-
this.ownership ??= new OwnershipModel(this.context.connection, this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
|
420
|
-
this.ownership.guid = this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
|
421
|
-
this.buildLocalAvatar();
|
422
|
-
|
423
|
-
|
424
|
-
const { frame, xr, rig } = evt;
|
425
|
-
const pose = frame.getViewerPose(xr.getReferenceSpace()!);
|
426
|
-
if (!pose) return; // e.g. if user is not wearing headset
|
427
|
-
const transform: XRRigidTransform = pose?.transform;
|
428
|
-
const pos = transform.position;
|
429
|
-
const rot = transform.orientation;
|
430
|
-
this.xrState.update(rig, pos, rot, this.webXR!, this.getAvatarId());
|
431
|
-
|
432
|
-
if (this.localAvatar) {
|
433
|
-
if (this.context.connection.connectionId) {
|
434
|
-
this.localAvatar.guid = this.context.connection.connectionId;
|
435
|
-
}
|
436
|
-
this.localAvatar.tryUpdate(this.xrState, 0);
|
437
|
-
}
|
438
|
-
|
439
|
-
if (this.ownership && !this.ownership.hasOwnership && this.context.connection.isConnected) {
|
440
|
-
if (this.context.time.frameCount % 120 === 0)
|
441
|
-
this.ownership.requestOwnership();
|
442
|
-
if (!this.ownership.hasOwnership) {
|
443
|
-
// console.log("NO OWNERSHIP", this.ownership.guid);
|
444
|
-
return;
|
445
|
-
}
|
446
|
-
}
|
447
|
-
|
448
|
-
if (!this.context.connection.isConnected || !this.context.connection.connectionId) {
|
449
|
-
return;
|
450
|
-
}
|
451
|
-
|
452
|
-
this.xrState.sendAsBuffer(this.builder, this.context.connection);
|
453
|
-
|
454
|
-
// this.context.connection.send(WebXRSyncEvent.VRSessionUpdate, this.xrState);
|
455
|
-
|
456
|
-
}
|
457
|
-
|
458
|
-
private getAvatarId() {
|
459
|
-
const urlAvatar = getParam("avatar") as string;
|
460
|
-
const avatarId = urlAvatar ?? null;
|
461
|
-
return avatarId;
|
462
|
-
}
|
463
|
-
}
|
1
|
+
import { Behaviour, GameObject } from "../Component.js";
|
2
|
+
import { RoomEvents, OwnershipModel, NetworkConnection } from "../../engine/engine_networking.js";
|
3
|
+
import { WebXR, WebXREvent } from "./WebXR.js";
|
4
|
+
import { Group, Quaternion, Vector3, Vector4, WebXRManager } from "three";
|
5
|
+
import { getParam } from "../../engine/engine_utils.js";
|
6
|
+
import { Voip } from "../Voip.js";
|
7
|
+
import { Builder, Long } from "flatbuffers";
|
8
|
+
import { VrUserStateBuffer } from "../../engine-schemes/vr-user-state-buffer.js";
|
9
|
+
import { Vec3 } from "../../engine-schemes/vec3.js";
|
10
|
+
import { registerBinaryType } from "../../engine-schemes/schemes.js";
|
11
|
+
import { Vec4 } from "../../engine-schemes/vec4.js";
|
12
|
+
import { WebXRAvatar } from "./WebXRAvatar.js";
|
13
|
+
|
14
|
+
// for debug GUI
|
15
|
+
// import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
|
16
|
+
// import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
|
17
|
+
// import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
|
18
|
+
// import { renderer, sceneData } from "../engine/engine_setup.js";
|
19
|
+
|
20
|
+
const debugLogs = getParam("debugxr");
|
21
|
+
const debugAvatar = getParam("debugavatar");
|
22
|
+
// const debugAvatarVoip = getParam("debugavatarvoip");
|
23
|
+
|
24
|
+
enum WebXRSyncEvent {
|
25
|
+
WebXR_UserJoined = "webxr-user-joined",
|
26
|
+
WebXR_UserLeft = "webxr-user-left",
|
27
|
+
VRSessionStart = "vr-session-started",
|
28
|
+
VRSessionEnd = "vr-session-ended",
|
29
|
+
VRSessionUpdate = "vr-session-update",
|
30
|
+
}
|
31
|
+
|
32
|
+
enum XRMode {
|
33
|
+
VR = "vr",
|
34
|
+
AR = "ar",
|
35
|
+
}
|
36
|
+
|
37
|
+
const VRUserStateBufferIdentifier = "VRUS";
|
38
|
+
registerBinaryType(VRUserStateBufferIdentifier, VrUserStateBuffer.getRootAsVrUserStateBuffer);
|
39
|
+
|
40
|
+
function getTimeStampNow() {
|
41
|
+
return new Date().getTime(); // avoid sending millis in flatbuffer
|
42
|
+
}
|
43
|
+
|
44
|
+
function flatbuffers_long_from_number(num: number): Long {
|
45
|
+
const low = num & 0xffffffff
|
46
|
+
const high = (num / Math.pow(2, 32)) & 0xfffff
|
47
|
+
return Long.create(low, high);
|
48
|
+
}
|
49
|
+
|
50
|
+
export class VRUserState {
|
51
|
+
public guid: string;
|
52
|
+
public time!: number;
|
53
|
+
public avatarId!: string;
|
54
|
+
public position: Vector3 = new Vector3();
|
55
|
+
public rotation: Vector4 = new Vector4();
|
56
|
+
public scale: number = 1;
|
57
|
+
|
58
|
+
public posLeftHand = new Vector3();
|
59
|
+
public posRightHand = new Vector3();
|
60
|
+
|
61
|
+
public rotLeftHand = new Quaternion();
|
62
|
+
public rotRightHand = new Quaternion();
|
63
|
+
|
64
|
+
public constructor(guid: string) {
|
65
|
+
this.guid = guid;
|
66
|
+
}
|
67
|
+
|
68
|
+
private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
69
|
+
|
70
|
+
public update(rig: Group, pos: DOMPointReadOnly, rot: DOMPointReadOnly, webXR: WebXR, avatarId: string) {
|
71
|
+
this.time = getTimeStampNow();
|
72
|
+
this.avatarId = avatarId;
|
73
|
+
this.position.set(pos.x, pos.y, pos.z);
|
74
|
+
if (rig)
|
75
|
+
this.position.applyMatrix4(rig.matrixWorld);
|
76
|
+
|
77
|
+
let q0 = VRUserState.quat0;
|
78
|
+
const q1 = VRUserState.quat1;
|
79
|
+
q0.set(rot.x, rot.y, rot.z, rot.w);
|
80
|
+
q0 = q0.multiplyQuaternions(q0, VRUserState.invertRotation);
|
81
|
+
|
82
|
+
if (rig) {
|
83
|
+
rig.getWorldQuaternion(q1);
|
84
|
+
q0.multiplyQuaternions(q1, q0);
|
85
|
+
}
|
86
|
+
|
87
|
+
this.rotation.set(q0.x, q0.y, q0.z, q0.w);
|
88
|
+
this.scale = rig.scale.x;
|
89
|
+
|
90
|
+
// for controllers, it seems we need grip pose
|
91
|
+
const ctrl0 = webXR.LeftController?.controllerGrip;
|
92
|
+
if (ctrl0) {
|
93
|
+
ctrl0.getWorldPosition(this.posLeftHand);
|
94
|
+
ctrl0.getWorldQuaternion(this.rotLeftHand);
|
95
|
+
}
|
96
|
+
const ctrl1 = webXR.RightController?.controllerGrip;
|
97
|
+
if (ctrl1) {
|
98
|
+
ctrl1.getWorldPosition(this.posRightHand);
|
99
|
+
ctrl1.getWorldQuaternion(this.rotRightHand);
|
100
|
+
}
|
101
|
+
|
102
|
+
// if this is a hand, we need to get the root bone of that / use that for position/rotation
|
103
|
+
if (webXR.LeftController?.hand?.visible) {
|
104
|
+
const wrist = webXR.LeftController.wrist;
|
105
|
+
if (wrist) {
|
106
|
+
wrist.getWorldPosition(this.posLeftHand);
|
107
|
+
wrist.getWorldQuaternion(this.rotLeftHand);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
if (webXR.RightController?.hand?.visible) {
|
112
|
+
const wrist = webXR.RightController.wrist;
|
113
|
+
if (wrist) {
|
114
|
+
wrist.getWorldPosition(this.posRightHand);
|
115
|
+
wrist.getWorldQuaternion(this.rotRightHand);
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
private static quat0: Quaternion = new Quaternion();
|
121
|
+
private static quat1: Quaternion = new Quaternion();
|
122
|
+
|
123
|
+
public sendAsBuffer(builder: Builder, net: NetworkConnection) {
|
124
|
+
builder.clear();
|
125
|
+
const guid = builder.createString(this.guid);
|
126
|
+
const id = builder.createString(this.avatarId);
|
127
|
+
VrUserStateBuffer.startVrUserStateBuffer(builder);
|
128
|
+
VrUserStateBuffer.addGuid(builder, guid);
|
129
|
+
VrUserStateBuffer.addTime(builder, flatbuffers_long_from_number(this.time));
|
130
|
+
VrUserStateBuffer.addAvatarId(builder, id);
|
131
|
+
VrUserStateBuffer.addPosition(builder, Vec3.createVec3(builder, this.position.x, this.position.y, this.position.z));
|
132
|
+
VrUserStateBuffer.addRotation(builder, Vec4.createVec4(builder, this.rotation.x, this.rotation.y, this.rotation.z, this.rotation.w));
|
133
|
+
VrUserStateBuffer.addScale(builder, this.scale);
|
134
|
+
VrUserStateBuffer.addPosLeftHand(builder, Vec3.createVec3(builder, this.posLeftHand.x, this.posLeftHand.y, this.posLeftHand.z));
|
135
|
+
VrUserStateBuffer.addPosRightHand(builder, Vec3.createVec3(builder, this.posRightHand.x, this.posRightHand.y, this.posRightHand.z));
|
136
|
+
VrUserStateBuffer.addRotLeftHand(builder, Vec4.createVec4(builder, this.rotLeftHand.x, this.rotLeftHand.y, this.rotLeftHand.z, this.rotLeftHand.w));
|
137
|
+
VrUserStateBuffer.addRotRightHand(builder, Vec4.createVec4(builder, this.rotRightHand.x, this.rotRightHand.y, this.rotRightHand.z, this.rotRightHand.w));
|
138
|
+
const res = VrUserStateBuffer.endVrUserStateBuffer(builder);
|
139
|
+
builder.finish(res, VRUserStateBufferIdentifier);
|
140
|
+
const arr = builder.asUint8Array();
|
141
|
+
net.sendBinary(arr);
|
142
|
+
}
|
143
|
+
|
144
|
+
public setFromBuffer(guid: string, state: VrUserStateBuffer) {
|
145
|
+
if (!guid) return;
|
146
|
+
this.guid = guid;
|
147
|
+
this.time = state.time().toFloat64();
|
148
|
+
const id = state.avatarId();
|
149
|
+
if (id)
|
150
|
+
this.avatarId = id;
|
151
|
+
const pos = state.position();
|
152
|
+
if (pos)
|
153
|
+
this.position.set(pos.x(), pos.y(), pos.z());
|
154
|
+
// TODO: maybe just send one float more instead of converting back and forth
|
155
|
+
const rot = state.rotation();
|
156
|
+
if (rot)
|
157
|
+
this.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
|
158
|
+
const posLeftHand = state.posLeftHand();
|
159
|
+
if (posLeftHand)
|
160
|
+
this.posLeftHand.set(posLeftHand.x(), posLeftHand.y(), posLeftHand.z());
|
161
|
+
const posRightHand = state.posRightHand();
|
162
|
+
if (posRightHand)
|
163
|
+
this.posRightHand.set(posRightHand.x(), posRightHand.y(), posRightHand.z());
|
164
|
+
const rotLeftHand = state.rotLeftHand();
|
165
|
+
if (rotLeftHand)
|
166
|
+
this.rotLeftHand.set(rotLeftHand.x(), rotLeftHand.y(), rotLeftHand.z(), rotLeftHand.w());
|
167
|
+
const rotRightHand = state.rotRightHand();
|
168
|
+
if (rotRightHand)
|
169
|
+
this.rotRightHand.set(rotRightHand.x(), rotRightHand.y(), rotRightHand.z(), rotRightHand.w());
|
170
|
+
this.scale = state.scale();
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
export class WebXRSync extends Behaviour {
|
175
|
+
|
176
|
+
webXR: WebXR | null = null;
|
177
|
+
|
178
|
+
// private allowCustomAvatars: boolean | null = true;
|
179
|
+
|
180
|
+
private debugAvatarUser: WebXRAvatar | null = null;
|
181
|
+
private voip: Voip | null = null;
|
182
|
+
|
183
|
+
async awake() {
|
184
|
+
|
185
|
+
if(!this.webXR) this.webXR = GameObject.getComponent(this.gameObject, WebXR);
|
186
|
+
if(!this.webXR) this.webXR = GameObject.findObjectOfType(WebXR, this.context);
|
187
|
+
|
188
|
+
if(!this.webXR)
|
189
|
+
{
|
190
|
+
console.log("Missing webxr component");
|
191
|
+
this.webXR = GameObject.findObjectOfType(WebXR, this.context);
|
192
|
+
if(!this.webXR) {
|
193
|
+
console.error("Could not find webxr component");
|
194
|
+
return;
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
if (!this.voip) this.voip = GameObject.findObjectOfType(Voip, this.context);
|
199
|
+
|
200
|
+
if (debugAvatar) {
|
201
|
+
const debugGuid = "debug-avatar-" + debugAvatar;
|
202
|
+
const newUser = new WebXRAvatar(this.context, debugGuid, this.webXR);
|
203
|
+
// newUser.isLocalAvatar = true;
|
204
|
+
this.debugAvatarUser = newUser;
|
205
|
+
if (typeof debugAvatar === "string" && debugAvatar.length > 0) {
|
206
|
+
if (await newUser.setAvatarOverride(debugAvatar)) {
|
207
|
+
const debugState = new VRUserState(debugGuid);
|
208
|
+
debugState.position.y += 1;
|
209
|
+
const off = .5;
|
210
|
+
debugState.posLeftHand.y += off;
|
211
|
+
debugState.posLeftHand.x += off;
|
212
|
+
debugState.posRightHand.y += off;
|
213
|
+
debugState.posRightHand.x -= off;
|
214
|
+
newUser.tryUpdate(debugState, 0);
|
215
|
+
}
|
216
|
+
else {
|
217
|
+
newUser.destroy();
|
218
|
+
}
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
onEnable() {
|
224
|
+
// const debugUser = new WebXRAvatar(this.context, "sorry-no-guid", this.webXR!);
|
225
|
+
|
226
|
+
if (!this.webXR) {
|
227
|
+
this.webXR = GameObject.getComponent(this.gameObject, WebXR);
|
228
|
+
if (!this.webXR) {
|
229
|
+
console.warn("Missing webxr component on " + this.gameObject.name);
|
230
|
+
return;
|
231
|
+
}
|
232
|
+
}
|
233
|
+
|
234
|
+
this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
|
235
|
+
WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
236
|
+
this.eventSub_WebXRUpdateEvent = this.onXRSessionUpdate.bind(this);
|
237
|
+
WebXR.addEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
|
238
|
+
this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
|
239
|
+
WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
240
|
+
|
241
|
+
this.eventSub_ConnectionEvent = this.onConnected.bind(this);
|
242
|
+
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
|
243
|
+
this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserJoined, _evt => {
|
244
|
+
console.log("webxr user joined evt");
|
245
|
+
});
|
246
|
+
this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserLeft, evt => {
|
247
|
+
const hasId = evt.id !== null && evt.id !== undefined;
|
248
|
+
if (!hasId) return;
|
249
|
+
console.log("webxr user left evt");
|
250
|
+
if (hasId) {
|
251
|
+
const avatar = this.avatars[evt.id];
|
252
|
+
avatar?.destroy();
|
253
|
+
this.avatars[evt.id] = undefined;
|
254
|
+
}
|
255
|
+
});
|
256
|
+
this.context.connection.beginListenBinary(VRUserStateBufferIdentifier, (state: VrUserStateBuffer) => {
|
257
|
+
// console.log("BUFFER", state);
|
258
|
+
const guid = state.guid();
|
259
|
+
if (!guid) return;
|
260
|
+
const time = state.time().toFloat64();
|
261
|
+
const temp = this.tempState;
|
262
|
+
temp.setFromBuffer(guid, state);
|
263
|
+
// console.log(temp);
|
264
|
+
const user = this.onTryGetAvatar(guid, time);
|
265
|
+
user?.tryUpdate(temp, time);
|
266
|
+
});
|
267
|
+
this.context.connection.beginListen(WebXRSyncEvent.VRSessionUpdate, (state: VRUserState) => {
|
268
|
+
const guid = state.guid;
|
269
|
+
const time = state.time;
|
270
|
+
const user = this.onTryGetAvatar(guid, time);
|
271
|
+
user?.tryUpdate(state, time);
|
272
|
+
});
|
273
|
+
}
|
274
|
+
|
275
|
+
private tempState: VRUserState = new VRUserState("");
|
276
|
+
|
277
|
+
private onTryGetAvatar(guid: string, time: number) {
|
278
|
+
if (guid === this.context.connection.connectionId) return null; // ignore self in case we receive that also!
|
279
|
+
const timeDiff = new Date().getTime() - time;
|
280
|
+
if (timeDiff > 5000) {
|
281
|
+
if (debugLogs)
|
282
|
+
console.log("old data", timeDiff, guid)
|
283
|
+
return null;
|
284
|
+
}
|
285
|
+
let user = this.avatars[guid];
|
286
|
+
if (user === undefined) {
|
287
|
+
try {
|
288
|
+
console.log("create new avatar");
|
289
|
+
const newUser = new WebXRAvatar(this.context, guid, this.webXR!);
|
290
|
+
user = newUser;
|
291
|
+
this.avatars[guid] = newUser;
|
292
|
+
} catch (err) {
|
293
|
+
this.avatars[guid] = null;
|
294
|
+
console.error(err);
|
295
|
+
}
|
296
|
+
}
|
297
|
+
return user;
|
298
|
+
}
|
299
|
+
|
300
|
+
onDisable() {
|
301
|
+
if (this.eventSub_ConnectionEvent)
|
302
|
+
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
|
303
|
+
WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
|
304
|
+
WebXR.removeEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
|
305
|
+
WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
|
306
|
+
}
|
307
|
+
|
308
|
+
update(): void {
|
309
|
+
|
310
|
+
const now = getTimeStampNow();
|
311
|
+
|
312
|
+
if (this.debugAvatarUser) {
|
313
|
+
this.debugAvatarUser.lastUpdate = now;
|
314
|
+
}
|
315
|
+
|
316
|
+
this.detectPotentiallyDisconnectedAvatarsAndRemove();
|
317
|
+
|
318
|
+
for (const key in this.avatars) {
|
319
|
+
const avatar = this.avatars[key];
|
320
|
+
if (!avatar) continue;
|
321
|
+
avatar.update();
|
322
|
+
}
|
323
|
+
}
|
324
|
+
|
325
|
+
|
326
|
+
private _removeAvatarsList: string[] = [];
|
327
|
+
private detectPotentiallyDisconnectedAvatarsAndRemove() {
|
328
|
+
const utcnow = getTimeStampNow();
|
329
|
+
for (const key in this.avatars) {
|
330
|
+
const avatar = this.avatars[key];
|
331
|
+
if (!avatar) {
|
332
|
+
this._removeAvatarsList.push(key);
|
333
|
+
continue;
|
334
|
+
}
|
335
|
+
if (utcnow - avatar.lastUpdate > 10_000) {
|
336
|
+
console.log("avatar timed out (didnt receive any updates in a while) - destroying it now");
|
337
|
+
avatar.destroy();
|
338
|
+
this.avatars[key] = undefined;
|
339
|
+
}
|
340
|
+
}
|
341
|
+
for (const rem of this._removeAvatarsList) {
|
342
|
+
delete this.avatars[rem];
|
343
|
+
}
|
344
|
+
this._removeAvatarsList.length = 0;
|
345
|
+
}
|
346
|
+
|
347
|
+
private buildLocalAvatar() {
|
348
|
+
if (this.localAvatar) return;
|
349
|
+
const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
|
350
|
+
this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR!);
|
351
|
+
this.localAvatar.isLocalAvatar = true;
|
352
|
+
this.localAvatar.setAvatarOverride(this.getAvatarId());
|
353
|
+
this.avatars[this.localAvatar.guid] = this.localAvatar;
|
354
|
+
}
|
355
|
+
|
356
|
+
|
357
|
+
private eventSub_ConnectionEvent: Function | null = null;
|
358
|
+
private eventSub_WebXRStartEvent: Function | null = null;
|
359
|
+
private eventSub_WebXREndEvent: Function | null = null;
|
360
|
+
private eventSub_WebXRUpdateEvent: Function | null = null;
|
361
|
+
private avatars: { [key: string]: WebXRAvatar | undefined | null } = {}
|
362
|
+
private localAvatar: WebXRAvatar | null = null;
|
363
|
+
private k_LocalAvatarNoNetworkingGuid = "local";
|
364
|
+
|
365
|
+
private onConnected() {
|
366
|
+
// this event gets fired when we have joined a room and are ready to update
|
367
|
+
if (debugLogs)
|
368
|
+
console.log("Hey you are connected as " + this.context.connection.connectionId);
|
369
|
+
|
370
|
+
if (this.localAvatar?.guid === this.k_LocalAvatarNoNetworkingGuid) {
|
371
|
+
if (this.localAvatar) {
|
372
|
+
this.localAvatar?.destroy();
|
373
|
+
this.avatars[this.localAvatar.guid] = undefined;
|
374
|
+
}
|
375
|
+
this.localAvatar = null;
|
376
|
+
this.xrState = null;
|
377
|
+
this.ownership?.freeOwnership();
|
378
|
+
this.ownership = null;
|
379
|
+
}
|
380
|
+
}
|
381
|
+
|
382
|
+
private onXRSessionStart(_evt: { session: XRSession }) {
|
383
|
+
console.log("XR session started");
|
384
|
+
this.context.connection.send(WebXRSyncEvent.WebXR_UserJoined, { id: this.context.connection.connectionId, mode: XRMode.VR });
|
385
|
+
|
386
|
+
if (this.localAvatar) {
|
387
|
+
this.localAvatar?.destroy();
|
388
|
+
this.avatars[this.localAvatar.guid] = undefined;
|
389
|
+
this.localAvatar = null;
|
390
|
+
}
|
391
|
+
this.xrState = null;
|
392
|
+
this.ownership?.freeOwnership();
|
393
|
+
this.ownership = null;
|
394
|
+
|
395
|
+
if (this.avatars) {
|
396
|
+
for (const key in this.avatars) {
|
397
|
+
this.avatars[key]?.updateFlags();
|
398
|
+
}
|
399
|
+
}
|
400
|
+
}
|
401
|
+
|
402
|
+
private onXRSessionEnded(_evt: { session: XRSession }) {
|
403
|
+
console.log("XR session ended");
|
404
|
+
this.context.connection.send(WebXRSyncEvent.WebXR_UserLeft, { id: this.context.connection.connectionId, mode: XRMode.VR });
|
405
|
+
if(this.localAvatar){
|
406
|
+
this.localAvatar?.destroy();
|
407
|
+
this.avatars[this.localAvatar.guid] = undefined;
|
408
|
+
this.localAvatar = null;
|
409
|
+
}
|
410
|
+
}
|
411
|
+
|
412
|
+
private ownership: OwnershipModel | null = null;
|
413
|
+
private xrState: VRUserState | null = null;
|
414
|
+
private builder: Builder = new Builder(1024);
|
415
|
+
|
416
|
+
private onXRSessionUpdate(evt: { rig: Group, frame: XRFrame, xr: WebXRManager, input: XRInputSource[] }) {
|
417
|
+
|
418
|
+
this.xrState ??= new VRUserState(this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
|
419
|
+
this.ownership ??= new OwnershipModel(this.context.connection, this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
|
420
|
+
this.ownership.guid = this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
|
421
|
+
this.buildLocalAvatar();
|
422
|
+
|
423
|
+
|
424
|
+
const { frame, xr, rig } = evt;
|
425
|
+
const pose = frame.getViewerPose(xr.getReferenceSpace()!);
|
426
|
+
if (!pose) return; // e.g. if user is not wearing headset
|
427
|
+
const transform: XRRigidTransform = pose?.transform;
|
428
|
+
const pos = transform.position;
|
429
|
+
const rot = transform.orientation;
|
430
|
+
this.xrState.update(rig, pos, rot, this.webXR!, this.getAvatarId());
|
431
|
+
|
432
|
+
if (this.localAvatar) {
|
433
|
+
if (this.context.connection.connectionId) {
|
434
|
+
this.localAvatar.guid = this.context.connection.connectionId;
|
435
|
+
}
|
436
|
+
this.localAvatar.tryUpdate(this.xrState, 0);
|
437
|
+
}
|
438
|
+
|
439
|
+
if (this.ownership && !this.ownership.hasOwnership && this.context.connection.isConnected) {
|
440
|
+
if (this.context.time.frameCount % 120 === 0)
|
441
|
+
this.ownership.requestOwnership();
|
442
|
+
if (!this.ownership.hasOwnership) {
|
443
|
+
// console.log("NO OWNERSHIP", this.ownership.guid);
|
444
|
+
return;
|
445
|
+
}
|
446
|
+
}
|
447
|
+
|
448
|
+
if (!this.context.connection.isConnected || !this.context.connection.connectionId) {
|
449
|
+
return;
|
450
|
+
}
|
451
|
+
|
452
|
+
this.xrState.sendAsBuffer(this.builder, this.context.connection);
|
453
|
+
|
454
|
+
// this.context.connection.send(WebXRSyncEvent.VRSessionUpdate, this.xrState);
|
455
|
+
|
456
|
+
}
|
457
|
+
|
458
|
+
private getAvatarId() {
|
459
|
+
const urlAvatar = getParam("avatar") as string;
|
460
|
+
const avatarId = urlAvatar ?? null;
|
461
|
+
return avatarId;
|
462
|
+
}
|
463
|
+
}
|