@@ -1,4 +1,4 @@
|
|
1
|
-
import { isDevEnvironment } from "../debug/debug.js";
|
1
|
+
import { isDevEnvironment, showBalloonWarning } from "../debug/debug.js";
|
2
2
|
import { IContext } from "../engine_types.js";
|
3
3
|
import { generateQRCode, isMobileDevice } from "../engine_utils.js";
|
4
4
|
import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
|
@@ -176,6 +176,9 @@
|
|
176
176
|
|
177
177
|
qrCodeButton.addEventListener("click", () => {
|
178
178
|
if (qrCodeContainer.parentNode) return hideQRCode();
|
179
|
+
if (window.location.href.includes("://localhost")) {
|
180
|
+
showBalloonWarning("To access your website from another device in the same local network you have to use the IP address instead of localhost.");
|
181
|
+
}
|
179
182
|
showQRCode();
|
180
183
|
});
|
181
184
|
|
@@ -82,12 +82,7 @@
|
|
82
82
|
const autoRotate = context.domElement.getAttribute("auto-rotate");
|
83
83
|
orbit.autoRotate = autoRotate !== undefined && (autoRotate === "" || autoRotate === "true")
|
84
84
|
orbit.autoRotateSpeed = 0.5;
|
85
|
-
|
86
|
-
const fitCamera = () => {
|
87
|
-
ctx.pre_render_callbacks.splice(ctx.pre_render_callbacks.indexOf(fitCamera), 1);
|
88
|
-
orbit.fitCamera();
|
89
|
-
}
|
90
|
-
ctx.pre_render_callbacks.push(fitCamera);
|
85
|
+
orbit.autoFit = true;
|
91
86
|
}
|
92
87
|
else {
|
93
88
|
console.warn("Missing camera object, can not add orbit controls")
|
@@ -26,8 +26,8 @@
|
|
26
26
|
export class WebARSessionRoot extends Behaviour {
|
27
27
|
|
28
28
|
/** The scale of a user in AR:
|
29
|
-
* a large value makes the scene appear smaller
|
30
|
-
* default
|
29
|
+
* Note: a large value makes the scene appear smaller
|
30
|
+
* @default 1
|
31
31
|
*/
|
32
32
|
@serializable()
|
33
33
|
get arScale(): number {
|
@@ -40,23 +40,37 @@
|
|
40
40
|
}
|
41
41
|
private _arScale: number = 1;
|
42
42
|
|
43
|
-
/** When enabled the placed scene forward direction will towards the XRRig
|
43
|
+
/** When enabled the placed scene forward direction will towards the XRRig
|
44
|
+
* @default false
|
45
|
+
*/
|
44
46
|
@serializable()
|
45
47
|
invertForward: boolean = false;
|
46
48
|
|
47
|
-
/** When assigned this asset will be loaded and visualize the placement while in AR
|
49
|
+
/** When assigned this asset will be loaded and visualize the placement while in AR
|
50
|
+
* @default null
|
51
|
+
*/
|
48
52
|
@serializable(AssetReference)
|
49
53
|
customReticle?: AssetReference;
|
50
54
|
|
51
55
|
/** When enabled we will create a XR anchor for the scene placement
|
52
|
-
* and make sure the scene is at that anchored point during a XR session
|
56
|
+
* and make sure the scene is at that anchored point during a XR session
|
57
|
+
* @default false
|
58
|
+
**/
|
53
59
|
@serializable()
|
54
60
|
useXRAnchor: boolean = false;
|
55
61
|
|
56
|
-
/** Preview feature: enable touch transform
|
62
|
+
/** Preview feature: enable touch transform
|
63
|
+
* @default false
|
64
|
+
*/
|
57
65
|
@serializable()
|
58
66
|
arTouchTransform: boolean = false;
|
59
67
|
|
68
|
+
/** When enabled the scene will be placed automatically when a point in the real world is found
|
69
|
+
* @default false
|
70
|
+
*/
|
71
|
+
@serializable()
|
72
|
+
autoPlace: boolean = false;
|
73
|
+
|
60
74
|
/** true if we're currently placing the scene */
|
61
75
|
private _isPlacing = true;
|
62
76
|
|
@@ -227,8 +241,8 @@
|
|
227
241
|
private updateReticleAndHits(_xr: NeedleXRSession, i: number, hit: NeedleXRHitTestResult, scale: number) {
|
228
242
|
// save the hit test
|
229
243
|
this._hits[i] = hit.hit;
|
244
|
+
let reticle = this._reticle[i];
|
230
245
|
|
231
|
-
let reticle = this._reticle[i];
|
232
246
|
if (!reticle) {
|
233
247
|
if (this.customReticle) {
|
234
248
|
if (this.customReticle.asset) {
|
@@ -267,11 +281,11 @@
|
|
267
281
|
reticle["lastPos"].copy(reticle.position);
|
268
282
|
reticle.quaternion.copy(reticle["lastQuat"].slerp(hit.quaternion, this.context.time.deltaTime / .05));
|
269
283
|
reticle["lastQuat"].copy(reticle.quaternion);
|
270
|
-
|
284
|
+
|
271
285
|
// TODO make sure original reticle asset scale is respected, or document it should be uniformly scaled
|
272
286
|
// scale *= this.customReticle?.asset?.scale?.x || 1;
|
273
287
|
reticle.scale.set(scale, scale, scale);
|
274
|
-
|
288
|
+
|
275
289
|
// if (this.invertForward) {
|
276
290
|
// reticle.rotateY(Math.PI);
|
277
291
|
// }
|
@@ -289,30 +303,25 @@
|
|
289
303
|
if (this._placementStartTime < 0) {
|
290
304
|
this._placementStartTime = this.context.time.realtimeSinceStartup;
|
291
305
|
}
|
306
|
+
|
307
|
+
if (this.autoPlace) {
|
308
|
+
reticle.visible = false;
|
309
|
+
this.onPlaceScene(null);
|
310
|
+
}
|
292
311
|
}
|
293
312
|
|
294
|
-
private onPlaceScene = (evt: NEPointerEvent) => {
|
313
|
+
private onPlaceScene = (evt: NEPointerEvent | null) => {
|
295
314
|
if (this._isPlacing == false) return;
|
296
|
-
if(evt
|
315
|
+
if (evt?.used) return;
|
297
316
|
|
298
317
|
let reticle: IGameObject | undefined = this._reticle[0];
|
299
|
-
let hit = this._hits[0];
|
300
318
|
|
301
|
-
if (evt.origin instanceof NeedleXRController) {
|
302
|
-
// until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
|
303
|
-
const controllerReticle = this._reticle[evt.origin.index];
|
304
|
-
if (controllerReticle) {
|
305
|
-
reticle = controllerReticle;
|
306
|
-
hit = this._hits[evt.origin.index];
|
307
|
-
}
|
308
|
-
}
|
309
|
-
|
310
319
|
if (!reticle) {
|
311
320
|
console.warn("No reticle to place...");
|
312
321
|
return;
|
313
322
|
}
|
314
323
|
|
315
|
-
if (!reticle.visible) {
|
324
|
+
if (!reticle.visible && !this.autoPlace) {
|
316
325
|
console.warn("Reticle is not visible (can not place)");
|
317
326
|
return;
|
318
327
|
}
|
@@ -322,10 +331,23 @@
|
|
322
331
|
return;
|
323
332
|
}
|
324
333
|
|
334
|
+
let hit = this._hits[0];
|
335
|
+
|
336
|
+
if (evt && evt.origin instanceof NeedleXRController) {
|
337
|
+
// until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
|
338
|
+
const controllerReticle = this._reticle[evt.origin.index];
|
339
|
+
if (controllerReticle) {
|
340
|
+
reticle = controllerReticle;
|
341
|
+
hit = this._hits[evt.origin.index];
|
342
|
+
}
|
343
|
+
}
|
344
|
+
|
325
345
|
// if we place the scene we don't want this event to be propagated to any sub-objects (via the EventSystem) anymore and trigger e.g. a click on objects for the "place tap" event
|
326
|
-
evt
|
327
|
-
|
328
|
-
|
346
|
+
if (evt) {
|
347
|
+
evt.stopImmediatePropagation();
|
348
|
+
evt.stopPropagation();
|
349
|
+
evt.use();
|
350
|
+
}
|
329
351
|
|
330
352
|
this._isPlacing = false;
|
331
353
|
this.context.input.removeEventListener("pointerup", this.onPlaceScene);
|
@@ -396,9 +418,9 @@
|
|
396
418
|
const reticleGo = reticle as GameObject;
|
397
419
|
const camWP = camGo.worldPosition;
|
398
420
|
const reticleWp = reticleGo.worldPosition;
|
399
|
-
|
400
|
-
this.upVec.set(0,1,0).applyQuaternion(reticle.quaternion);
|
401
421
|
|
422
|
+
this.upVec.set(0, 1, 0).applyQuaternion(reticle.quaternion);
|
423
|
+
|
402
424
|
// upVec may be pointing AWAY from us, we have to flip it if that's the case
|
403
425
|
const camPos = camGo.worldPosition;
|
404
426
|
if (camPos) {
|
@@ -415,9 +437,9 @@
|
|
415
437
|
// Gizmos.DrawLabel(reticle.position, upAngle.toFixed(2), 0.1);
|
416
438
|
|
417
439
|
const angleForWallPlacement = 30;
|
418
|
-
if ((upAngle > angleForWallPlacement && upAngle < 180 - angleForWallPlacement) ||
|
440
|
+
if ((upAngle > angleForWallPlacement && upAngle < 180 - angleForWallPlacement) ||
|
419
441
|
(upAngle < -angleForWallPlacement && upAngle > -180 + angleForWallPlacement)) {
|
420
|
-
|
442
|
+
|
421
443
|
this.lookPoint.copy(reticle.position).add(this.upVec);
|
422
444
|
this.lookPoint.y = reticle.position.y;
|
423
445
|
reticle.lookAt(this.lookPoint);
|
@@ -441,7 +463,7 @@
|
|
441
463
|
if (!rigObject) {
|
442
464
|
console.warn("No rig object to place");
|
443
465
|
return;
|
444
|
-
}
|
466
|
+
}
|
445
467
|
const rigScale = NeedleXRSession.active?.rigScale || 1;
|
446
468
|
|
447
469
|
// save the previous rig parent
|
@@ -479,7 +501,7 @@
|
|
479
501
|
// apply the rig modifications and add it back to the previous parent
|
480
502
|
rigObject.matrix.decompose(rigObject.position, rigObject.quaternion, rigObject.scale);
|
481
503
|
previousParent.add(rigObject);
|
482
|
-
|
504
|
+
|
483
505
|
}
|
484
506
|
}
|
485
507
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import { Object3D } from "three";
|
2
2
|
|
3
|
-
import { showBalloonWarning } from "../../engine/debug/index.js";
|
3
|
+
import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
|
4
4
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
5
|
+
import { findObjectOfType } from "../../engine/engine_components.js";
|
5
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
6
7
|
import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
|
7
8
|
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
@@ -10,6 +11,7 @@
|
|
10
11
|
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
|
11
12
|
import { Behaviour, GameObject } from "../Component.js";
|
12
13
|
import { USDZExporter } from "../export/usdz/USDZExporter.js";
|
14
|
+
import { NeedleMenu } from "../NeedleMenu.js";
|
13
15
|
import { SpatialGrabRaycaster } from "../ui/Raycaster.js";
|
14
16
|
import { Avatar } from "./Avatar.js";
|
15
17
|
import { XRControllerModel } from "./controllers/XRControllerModel.js";
|
@@ -310,12 +312,19 @@
|
|
310
312
|
}
|
311
313
|
|
312
314
|
if (this.createQRCode && !isMobileDevice()) {
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
315
|
+
const menu = findObjectOfType(NeedleMenu);
|
316
|
+
if (menu && menu.createQRCodeButton === false) {
|
317
|
+
// If the menu exists and the QRCode option is disabled we dont create it (NE-4919)
|
318
|
+
if (isDevEnvironment()) console.warn("WebXR: QRCode button is disabled in the Needle Menu component")
|
319
|
+
}
|
320
|
+
else {
|
321
|
+
NeedleXRSession.isXRSupported().then(supported => {
|
322
|
+
if (isDesktop() || !supported) {
|
323
|
+
const qrCode = ButtonsFactory.getOrCreate().createQRCode();
|
324
|
+
this.addButton(qrCode, xrButtonsPriority);
|
325
|
+
}
|
326
|
+
});
|
327
|
+
}
|
319
328
|
}
|
320
329
|
}
|
321
330
|
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import { serializable } from "../../engine/engine_serialization.js";
|
6
6
|
import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
|
7
7
|
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/api.js";
|
8
|
-
import { imageToCanvas,USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
8
|
+
import { imageToCanvas, USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
9
9
|
import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
|
10
10
|
import { Behaviour, GameObject } from "../Component.js";
|
11
11
|
import { Renderer } from "../Renderer.js";
|
@@ -97,23 +97,50 @@
|
|
97
97
|
parent: Object3D | undefined | null;
|
98
98
|
matrix: Matrix4;
|
99
99
|
}
|
100
|
-
|
100
|
+
/**
|
101
|
+
* WebXRImageTracking allows you to track images in the real world and place objects on top of them.
|
102
|
+
* This component is only available in WebXR sessions.
|
103
|
+
* The WebXRImageTrackingModel contains the image to track, the object to place on top of the image, and the size of the image as well as settings for the tracking.
|
104
|
+
* Used by the {@link WebXRImageTracking} component
|
105
|
+
*/
|
101
106
|
export class WebXRImageTrackingModel {
|
102
107
|
|
108
|
+
/**
|
109
|
+
* Tracked image marker url. Make sure the image has good contrast and unique features to improve the tracking quality.
|
110
|
+
*/
|
103
111
|
@serializable(URL)
|
104
112
|
image?: string;
|
105
113
|
|
114
|
+
/** Make sure this matches your physical marker size! Otherwise the tracked object will \"swim\" above or below the marker.
|
115
|
+
* @default 0.25 which is equivalent to 25cm
|
116
|
+
*/
|
106
117
|
@serializable()
|
107
|
-
widthInMeters
|
118
|
+
widthInMeters: number = .25;
|
108
119
|
|
120
|
+
/**
|
121
|
+
* The object moved around by the image. Make sure the size matches WidthInMeters.
|
122
|
+
*/
|
109
123
|
@serializable(AssetReference)
|
110
124
|
object?: AssetReference;
|
111
125
|
|
126
|
+
/**
|
127
|
+
* If true, a new instance of the referenced object will be created for each tracked image. Enable this if you're re-using objects for multiple markers.
|
128
|
+
*/
|
112
129
|
@serializable()
|
113
130
|
createObjectInstance: boolean = false;
|
114
131
|
|
132
|
+
/** Use this for static images (e.g. markers on the floor). Only the first few frames of new poses will be applied to the model. This will result in more stable tracking.
|
133
|
+
* @default false
|
134
|
+
*/
|
115
135
|
@serializable()
|
116
136
|
imageDoesNotMove: boolean = false;
|
137
|
+
|
138
|
+
/**
|
139
|
+
* Enable to hide the tracked object when the image is not tracked anymore. When disabled the tracked object will stay at the position it was last tracked at.
|
140
|
+
* @default true
|
141
|
+
*/
|
142
|
+
@serializable()
|
143
|
+
hideWhenTrackingIsLost: boolean = true;
|
117
144
|
}
|
118
145
|
|
119
146
|
class ImageTrackingExtension {
|
@@ -315,6 +342,8 @@
|
|
315
342
|
const hysteresis = 1000;
|
316
343
|
for (const [key, value] of this.imageToObjectMap) {
|
317
344
|
if (!value.object || !key) continue;
|
345
|
+
// If the user disallowed hiding the object when tracking is lost, skip this
|
346
|
+
if (key.hideWhenTrackingIsLost === false) continue;
|
318
347
|
let found = false;
|
319
348
|
for (const trackedImage of this.currentImages) {
|
320
349
|
if (trackedImage.model === key) {
|