@needle-tools/engine 5.1.8 → 5.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/SKILL.md +62 -4
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-CaD5rOJ0.js → needle-engine.bundle-DP--RGAU.js} +6126 -5969
- package/dist/{needle-engine.bundle-X7Y4A-EO.min.js → needle-engine.bundle-DZD-GG7l.min.js} +144 -143
- package/dist/{needle-engine.bundle-D8gP8Nd0.umd.cjs → needle-engine.bundle-Dv5TidOk.umd.cjs} +141 -140
- package/dist/needle-engine.d.ts +133 -36
- package/dist/needle-engine.js +617 -613
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/three-examples.js +727 -781
- package/dist/three-examples.min.js +12 -12
- package/dist/three-examples.umd.cjs +9 -9
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +71 -71
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_materialpropertyblock.d.ts +13 -4
- package/lib/engine/engine_materialpropertyblock.js +16 -5
- package/lib/engine/engine_materialpropertyblock.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/postprocessing/postprocessing.d.ts +18 -0
- package/lib/engine/postprocessing/postprocessing.js +31 -2
- package/lib/engine/postprocessing/postprocessing.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.ar-overlay.js +11 -0
- package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.d.ts +7 -2
- package/lib/engine/xr/NeedleXRSession.js +65 -5
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine/xr/XRHandMeshModel.d.ts +31 -0
- package/lib/engine/xr/XRHandMeshModel.js +153 -0
- package/lib/engine/xr/XRHandMeshModel.js.map +1 -0
- package/lib/engine-components/Component.d.ts +2 -1
- package/lib/engine-components/Component.js.map +1 -1
- package/lib/engine-components/ReflectionProbe.js +3 -3
- package/lib/engine-components/ReflectionProbe.js.map +1 -1
- package/lib/engine-components/RendererLightmap.js +1 -1
- package/lib/engine-components/RendererLightmap.js.map +1 -1
- package/lib/engine-components/SyncedRoom.js +5 -0
- package/lib/engine-components/SyncedRoom.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/postprocessing/Effects/DepthOfField.js +10 -2
- package/lib/engine-components/postprocessing/Effects/DepthOfField.js.map +1 -1
- package/lib/engine-components/postprocessing/index.d.ts +4 -0
- package/lib/engine-components/postprocessing/index.js +7 -0
- package/lib/engine-components/postprocessing/index.js.map +1 -1
- package/lib/engine-components/utils/LookAt.d.ts +13 -4
- package/lib/engine-components/utils/LookAt.js +13 -4
- package/lib/engine-components/utils/LookAt.js.map +1 -1
- package/lib/engine-components/webxr/WebXR.js +1 -1
- package/lib/engine-components/webxr/WebXR.js.map +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.d.ts +35 -18
- package/lib/engine-components/webxr/WebXRImageTracking.js +93 -78
- package/lib/engine-components/webxr/WebXRImageTracking.js.map +1 -1
- package/lib/engine-components/webxr/WebXRImageTracking.utils.d.ts +35 -0
- package/lib/engine-components/webxr/WebXRImageTracking.utils.js +96 -0
- package/lib/engine-components/webxr/WebXRImageTracking.utils.js.map +1 -0
- package/lib/engine-components/webxr/controllers/XRControllerModel.d.ts +1 -1
- package/lib/engine-components/webxr/controllers/XRControllerModel.js +12 -7
- package/lib/engine-components/webxr/controllers/XRControllerModel.js.map +1 -1
- package/package.json +2 -2
- package/plugins/common/license.js +4 -4
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +68 -68
- package/src/engine/engine_materialpropertyblock.ts +17 -5
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/postprocessing/postprocessing.ts +32 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.ar-overlay.ts +13 -0
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/NeedleXRSession.ts +68 -8
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine/xr/XRHandMeshModel.ts +179 -0
- package/src/engine-components/Component.ts +2 -1
- package/src/engine-components/ReflectionProbe.ts +3 -3
- package/src/engine-components/RendererLightmap.ts +1 -1
- package/src/engine-components/SyncedRoom.ts +5 -0
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/postprocessing/Effects/DepthOfField.ts +10 -2
- package/src/engine-components/postprocessing/index.ts +7 -0
- package/src/engine-components/utils/LookAt.ts +13 -4
- package/src/engine-components/webxr/WebXR.ts +1 -1
- package/src/engine-components/webxr/WebXRImageTracking.ts +120 -66
- package/src/engine-components/webxr/WebXRImageTracking.utils.ts +105 -0
- package/src/engine-components/webxr/controllers/XRControllerModel.ts +13 -7
|
@@ -83,12 +83,20 @@ export class DepthOfField extends PostProcessingEffect {
|
|
|
83
83
|
// inverted: true,
|
|
84
84
|
// // blendFunction: BlendFunction.SET,
|
|
85
85
|
// });
|
|
86
|
+
// Seed from the parameters, the same way BloomEffect does. The
|
|
87
|
+
// onValueChanged handlers below only fire on a LATER assignment, so
|
|
88
|
+
// hardcoding defaults here discarded anything set before the effect
|
|
89
|
+
// was created — which is the usual case, since components are
|
|
90
|
+
// configured right after addComponent but the effect is built when
|
|
91
|
+
// the composer asks for it. Values applied in awake/start were lost
|
|
92
|
+
// and only took hold if the same value was assigned a second time.
|
|
86
93
|
const dof = new MODULES.POSTPROCESSING.MODULE.DepthOfFieldEffect(this.context.mainCamera!, {
|
|
87
|
-
worldFocusRange: .
|
|
94
|
+
worldFocusRange: this.focalLength.value,
|
|
88
95
|
focalLength: 1,
|
|
89
|
-
bokehScale:
|
|
96
|
+
bokehScale: this.aperture.value,
|
|
90
97
|
resolutionScale: this.resolutionScale.value,
|
|
91
98
|
});
|
|
99
|
+
dof.cocMaterial.worldFocusDistance = this.focusDistance.value;
|
|
92
100
|
|
|
93
101
|
this.focusDistance.onValueChanged = v => {
|
|
94
102
|
dof.cocMaterial.worldFocusDistance = v;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export * from "./PostProcessingEffect.js";
|
|
2
2
|
export * from "./PostProcessingHandler.js"
|
|
3
3
|
export { PostProcessingEffectOrder } from "./utils.js";
|
|
4
|
+
/* Effect option enums. The effect components themselves are exported from
|
|
5
|
+
codegen/components.ts, but these were unreachable: `DepthOfField.mode`
|
|
6
|
+
could not be set to a named value from outside the package. */
|
|
7
|
+
export { DepthOfFieldMode } from "./Effects/DepthOfField.js";
|
|
8
|
+
export { QualityLevel } from "./Effects/Antialiasing.js";
|
|
9
|
+
export { ScreenSpaceAmbientOcclusionN8QualityMode } from "./Effects/ScreenspaceAmbientOcclusionN8.js";
|
|
10
|
+
export { NEToneMappingMode } from "./Effects/Tonemapping.utils.js";
|
|
4
11
|
export { PostProcessingManager } from "./Volume.js"
|
|
5
12
|
export { Volume } from "./Volume.js"
|
|
6
13
|
export * from "./VolumeParameter.js"
|
|
@@ -20,25 +20,34 @@ import { Behaviour } from "../Component.js";
|
|
|
20
20
|
export class LookAt extends Behaviour implements UsdzBehaviour {
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* The target object to look at.
|
|
23
|
+
* The target object to look at.
|
|
24
|
+
* @default undefined — when no target is set the main camera is used
|
|
24
25
|
*/
|
|
25
26
|
@serializable(Object3D)
|
|
26
27
|
target?: Object3D;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Inverts the forward direction
|
|
30
|
+
* Inverts the forward direction, so the object turns its back to the target
|
|
31
|
+
* instead of facing it. Useful for models authored facing -Z.
|
|
32
|
+
* @default false
|
|
30
33
|
*/
|
|
31
34
|
@serializable()
|
|
32
35
|
invertForward: boolean = false;
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
*
|
|
38
|
+
* Keeps the object upright while turning, so it only rotates around the up
|
|
39
|
+
* axis rather than tilting to point directly at a target above or below it.
|
|
40
|
+
* @default true
|
|
36
41
|
*/
|
|
37
42
|
@serializable()
|
|
38
43
|
keepUpDirection: boolean = true;
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
|
-
*
|
|
46
|
+
* Copies the target's rotation instead of aiming at its position. Use this to
|
|
47
|
+
* match a target's orientation, for example to align with the camera.
|
|
48
|
+
*
|
|
49
|
+
* Ignored during a VR or AR session.
|
|
50
|
+
* @default false
|
|
42
51
|
*/
|
|
43
52
|
@serializable()
|
|
44
53
|
copyTargetRotation: boolean = false;
|
|
@@ -520,7 +520,7 @@ export class WebXR extends Behaviour {
|
|
|
520
520
|
models = this.gameObject.addComponent(XRControllerModel)!;
|
|
521
521
|
this._createdComponentsInSession.push(models);
|
|
522
522
|
models.createControllerModel = this.showControllerModels;
|
|
523
|
-
models.createHandModel
|
|
523
|
+
models.createHandModel = this.showHandModels;
|
|
524
524
|
}
|
|
525
525
|
if (models) models.enabled = enabled;
|
|
526
526
|
return models;
|
|
@@ -4,6 +4,7 @@ import { Object3DEventMap } from "three";
|
|
|
4
4
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
|
|
5
5
|
import { AssetReference } from "../../engine/engine_addressables.js";
|
|
6
6
|
import { Context } from "../../engine/engine_context.js";
|
|
7
|
+
import { Telemetry } from "../../engine/engine_license.js";
|
|
7
8
|
import { serializable } from "../../engine/engine_serialization.js";
|
|
8
9
|
import { IGameObject } from "../../engine/engine_types.js";
|
|
9
10
|
import { CircularBuffer, delay, DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
|
@@ -14,6 +15,7 @@ import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.j
|
|
|
14
15
|
import { Behaviour, GameObject } from "../Component.js";
|
|
15
16
|
import { EventList } from "../EventList.js";
|
|
16
17
|
import { Renderer } from "../Renderer.js";
|
|
18
|
+
import { _imageElements, collectTrackedImages, loadImage } from "./WebXRImageTracking.utils.js";
|
|
17
19
|
|
|
18
20
|
// https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
|
|
19
21
|
|
|
@@ -216,9 +218,9 @@ export class WebXRImageTrackingModel {
|
|
|
216
218
|
* @param params.imageDoesNotMove Enable for static markers (floor/wall mounted) to improve tracking stability
|
|
217
219
|
* @param params.hideWhenTrackingIsLost If true, hides the object when tracking is lost; if false, leaves it at the last known position
|
|
218
220
|
*/
|
|
219
|
-
constructor(params:
|
|
221
|
+
constructor(params: Omit<WebXRImageTrackingOptions, "asPrimary">) {
|
|
220
222
|
this.image = params.url;
|
|
221
|
-
this.widthInMeters = params.widthInMeters;
|
|
223
|
+
if (params.widthInMeters !== undefined) this.widthInMeters = params.widthInMeters;
|
|
222
224
|
if (params.object instanceof Object3D) {
|
|
223
225
|
this.object = new AssetReference({ asset: params.object });
|
|
224
226
|
}
|
|
@@ -322,6 +324,27 @@ export class WebXRImageTrackingModel {
|
|
|
322
324
|
}
|
|
323
325
|
}
|
|
324
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Options for adding a trackable image marker via {@link WebXRImageTracking.addImage}.
|
|
329
|
+
* A convenience shape that also accepts a plain {@link Object3D} for `object`.
|
|
330
|
+
*/
|
|
331
|
+
export interface WebXRImageTrackingOptions {
|
|
332
|
+
/** URL to the marker image to track. Use high-contrast images with unique features for reliable tracking. */
|
|
333
|
+
url?: string;
|
|
334
|
+
/** The 3D object (or {@link AssetReference}) to display when the marker is detected. */
|
|
335
|
+
object: AssetReference | Object3D;
|
|
336
|
+
/** Physical width of the printed marker in meters. Must match the real marker size. @default 0.25 */
|
|
337
|
+
widthInMeters?: number;
|
|
338
|
+
/** Create a new instance of the object for each detection. @default false */
|
|
339
|
+
createObjectInstance?: boolean;
|
|
340
|
+
/** Enable for static markers (walls/floor) to improve tracking stability. @default false */
|
|
341
|
+
imageDoesNotMove?: boolean;
|
|
342
|
+
/** Hide the object when tracking is lost. @default true */
|
|
343
|
+
hideWhenTrackingIsLost?: boolean;
|
|
344
|
+
/** Make this the primary marker (used in QuickLook fallback mode). @default false */
|
|
345
|
+
asPrimary?: boolean;
|
|
346
|
+
}
|
|
347
|
+
|
|
325
348
|
/** Data passed to image tracking event listeners. */
|
|
326
349
|
export interface WebXRImageTrackingEvent {
|
|
327
350
|
/** The images currently being tracked this frame. */
|
|
@@ -619,27 +642,37 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
619
642
|
* **Tip:** Add all your markers upfront. In WebXR mode they all work simultaneously.
|
|
620
643
|
* In QuickLook mode, the first (primary) marker is used.
|
|
621
644
|
*
|
|
622
|
-
* @param image The marker configuration
|
|
623
|
-
*
|
|
645
|
+
* @param image The marker configuration - either a plain options object or a {@link WebXRImageTrackingModel}.
|
|
646
|
+
* Set `asPrimary: true` to make this the primary marker (for QuickLook fallback).
|
|
624
647
|
*
|
|
625
648
|
* @example
|
|
626
649
|
* ```ts
|
|
627
|
-
* // Super simple -
|
|
628
|
-
*
|
|
650
|
+
* // Super simple - just pass the marker url and the object to show
|
|
651
|
+
* imageTracking.addImage({
|
|
629
652
|
* url: "https://mysite.com/poster.png",
|
|
630
|
-
*
|
|
631
|
-
*
|
|
653
|
+
* object: cool3DModel,
|
|
654
|
+
* widthInMeters: 0.42, // A3 poster width (optional, defaults to 0.25)
|
|
632
655
|
* });
|
|
633
|
-
* imageTracking.addImage(marker);
|
|
634
656
|
* // That's it! Needle does the rest.
|
|
635
657
|
* ```
|
|
636
658
|
*/
|
|
637
|
-
addImage(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
659
|
+
addImage(options: WebXRImageTrackingOptions): void;
|
|
660
|
+
addImage(model: WebXRImageTrackingModel): void;
|
|
661
|
+
addImage(image: WebXRImageTrackingOptions | WebXRImageTrackingModel): void {
|
|
662
|
+
// Accept either a ready-made model or a plain options object (which allows `object` to be a
|
|
663
|
+
// plain Object3D). Normalize to a WebXRImageTrackingModel; the constructor wraps Object3D in
|
|
664
|
+
// an AssetReference for us.
|
|
665
|
+
const model = image instanceof WebXRImageTrackingModel ? image : new WebXRImageTrackingModel(image);
|
|
666
|
+
if (!this.trackedImages.includes(model)) {
|
|
667
|
+
this.trackedImages.push(model);
|
|
668
|
+
loadImage(model.image!);
|
|
669
|
+
if(isDevEnvironment()) {
|
|
670
|
+
if(model.widthInMeters <= 0) {
|
|
671
|
+
this.onInvalidImageSizeDetected(model.image!);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
641
674
|
}
|
|
642
|
-
if (asPrimary) this.setPrimaryImage(
|
|
675
|
+
if (!(image instanceof WebXRImageTrackingModel) && image.asPrimary) this.setPrimaryImage(model);
|
|
643
676
|
}
|
|
644
677
|
|
|
645
678
|
|
|
@@ -689,6 +722,9 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
689
722
|
for (const trackedImage of this.trackedImages) {
|
|
690
723
|
if (trackedImage.image) {
|
|
691
724
|
loadImage(trackedImage.image);
|
|
725
|
+
if(isDevEnvironment() && trackedImage.widthInMeters <= 0) {
|
|
726
|
+
this.onInvalidImageSizeDetected(trackedImage.image);
|
|
727
|
+
}
|
|
692
728
|
}
|
|
693
729
|
}
|
|
694
730
|
}
|
|
@@ -701,6 +737,16 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
701
737
|
USDZExporter.beforeExport.removeEventListener(this.onBeforeUSDZExport);
|
|
702
738
|
}
|
|
703
739
|
|
|
740
|
+
|
|
741
|
+
private onInvalidImageSizeDetected(url:string) {
|
|
742
|
+
if(globalThis["__NEEDLE_DEBUG_WebARImageSizeInvalid__"]) return;
|
|
743
|
+
globalThis["__NEEDLE_DEBUG_WebARImageSizeInvalid__"] = true;
|
|
744
|
+
console.warn(`[WebXRImageTracking] Invalid marker size detected for image ${url}. Ensure 'widthInMeters' is greater than 0 for accurate tracking.`);
|
|
745
|
+
if(isDevEnvironment()) {
|
|
746
|
+
showBalloonWarning(`[WebXRImageTracking] Invalid marker size detected for image ${url}. Ensure 'widthInMeters' is greater than 0 for accurate tracking.`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
704
750
|
private onBeforeUSDZExport = (args: { exporter: USDZExporter }) => {
|
|
705
751
|
if (this.activeAndEnabled && this.trackedImages?.length) {
|
|
706
752
|
args.exporter.extensions.push(new ImageTrackingExtension(args.exporter, this));
|
|
@@ -709,31 +755,30 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
709
755
|
|
|
710
756
|
|
|
711
757
|
/** @internal */
|
|
712
|
-
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): void {
|
|
713
|
-
|
|
714
|
-
if (this.trackedImages) {
|
|
715
|
-
args.optionalFeatures = args.optionalFeatures || [];
|
|
716
|
-
if (!args.optionalFeatures.includes("image-tracking"))
|
|
717
|
-
args.optionalFeatures.push("image-tracking");
|
|
758
|
+
async onBeforeXR(_mode: XRSessionMode, args: XRSessionInit & { trackedImages: Array<any> }): Promise<void> {
|
|
759
|
+
if (!this.trackedImages) return;
|
|
718
760
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
761
|
+
args.optionalFeatures = args.optionalFeatures || [];
|
|
762
|
+
if (!args.optionalFeatures.includes("image-tracking"))
|
|
763
|
+
args.optionalFeatures.push("image-tracking");
|
|
764
|
+
|
|
765
|
+
// Resolve the marker bitmaps *before* the session is requested.
|
|
766
|
+
// onBeforeXR runs the moment "Enter AR" is tapped. Marker images load asynchronously
|
|
767
|
+
// (network fetch + createImageBitmap), so without awaiting here a marker that hasn't finished
|
|
768
|
+
// decoding yet would be silently dropped from `trackedImages` and never registered with the
|
|
769
|
+
// native/WebXR session - it would then only start working after exiting and re-entering AR,
|
|
770
|
+
// once the bitmap has been cached. Awaiting the (idempotent) loads closes that race.
|
|
771
|
+
// A marker that can not be loaded resolves to null and is skipped individually, so a single
|
|
772
|
+
// unavailable image never rejects the whole XR session request. See forum topic #2859.
|
|
773
|
+
this.trackedImageIndexMap.clear();
|
|
774
|
+
const indexMap = await collectTrackedImages(this.trackedImages, args);
|
|
775
|
+
for (const [index, model] of indexMap) {
|
|
776
|
+
this.trackedImageIndexMap.set(index, model);
|
|
732
777
|
}
|
|
733
778
|
}
|
|
734
779
|
|
|
735
780
|
/** @internal */
|
|
736
|
-
onEnterXR(
|
|
781
|
+
onEnterXR(args: NeedleXREventArgs): void {
|
|
737
782
|
if (this.trackedImages) {
|
|
738
783
|
for (const trackedImage of this.trackedImages) {
|
|
739
784
|
if (trackedImage.object?.asset) {
|
|
@@ -753,11 +798,52 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
753
798
|
for (const trackedData of this.imageToObjectMap.values()) {
|
|
754
799
|
trackedData.frames = 0;
|
|
755
800
|
}
|
|
801
|
+
|
|
802
|
+
// A session GRANTED before the scene finished loading (App Clip / NeedleGo
|
|
803
|
+
// handoff, Quest link traversal) was requested without this component's
|
|
804
|
+
// markers — onBeforeXR never contributed to its init, and the WebXR spec has
|
|
805
|
+
// no way to add tracked images to a running session. The NeedleGo polyfill
|
|
806
|
+
// CAN (ARKit re-runs its configuration with new detection images), so
|
|
807
|
+
// register late where supported and surface the misconfiguration otherwise.
|
|
808
|
+
if (this.trackedImages?.length && this.trackedImageIndexMap.size === 0) {
|
|
809
|
+
if (isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] No marker images made it into the session init (granted session?) — attempting runtime registration for ${this.trackedImages.length} image(s)`);
|
|
810
|
+
this.registerImagesWithRunningSession(args.xr.session);
|
|
811
|
+
}
|
|
812
|
+
else if (this.trackedImages?.length && (isDevEnvironment() || debug)) {
|
|
813
|
+
console.debug(`[WebXRImageTracking] ${this.trackedImageIndexMap.size} marker image(s) registered via the session init`);
|
|
814
|
+
}
|
|
756
815
|
};
|
|
757
816
|
|
|
817
|
+
/** Late-registers this component's marker images into an already running session
|
|
818
|
+
* via the NeedleGo polyfill's runtime hook. See {@link onEnterXR}. */
|
|
819
|
+
private async registerImagesWithRunningSession(session: XRSession) {
|
|
820
|
+
const register = (session as XRSession & { nonStandard_registerTrackedImages?: (images: Array<{ image: ImageBitmap, widthInMeters: number }>) => number }).nonStandard_registerTrackedImages;
|
|
821
|
+
if (typeof register !== "function") {
|
|
822
|
+
console.warn("[WebXRImageTracking] The XR session was started before the scene finished loading and does not support registering marker images at runtime — image tracking will not work in this session. Exit and re-enter AR from the loaded page.");
|
|
823
|
+
Telemetry.sendEvent(this.context, "xr", { action: "image_tracking_missed_session_init" });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const args: { trackedImages?: Array<{ image: ImageBitmap, widthInMeters: number }> } = {};
|
|
827
|
+
const indexMap = await collectTrackedImages(this.trackedImages, args);
|
|
828
|
+
if (!args.trackedImages?.length) return;
|
|
829
|
+
// the session may have ended (or been replaced) while marker bitmaps loaded
|
|
830
|
+
if (this.context.xr?.session !== session) return;
|
|
831
|
+
const baseIndex = register.call(session, args.trackedImages);
|
|
832
|
+
if (baseIndex < 0) return;
|
|
833
|
+
for (const [index, model] of indexMap) {
|
|
834
|
+
this.trackedImageIndexMap.set(baseIndex + index, model);
|
|
835
|
+
}
|
|
836
|
+
if (isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Registered ${args.trackedImages.length} marker image(s) with the running session (granted before the scene loaded)`);
|
|
837
|
+
}
|
|
838
|
+
|
|
758
839
|
/** @internal */
|
|
759
840
|
onLeaveXR(_args: NeedleXREventArgs): void {
|
|
760
841
|
|
|
842
|
+
// result indices are scoped to the session that registered the images — a new
|
|
843
|
+
// session rebuilds the map (onBeforeXR, or the late registration in onEnterXR
|
|
844
|
+
// for granted sessions, which detects the empty map)
|
|
845
|
+
this.trackedImageIndexMap.clear();
|
|
846
|
+
|
|
761
847
|
if (!this.supported && DeviceUtilities.isAndroidDevice()) {
|
|
762
848
|
showBalloonWarning(this.webXRIncubationsWarning);
|
|
763
849
|
}
|
|
@@ -942,36 +1028,4 @@ export class WebXRImageTracking extends Behaviour {
|
|
|
942
1028
|
}
|
|
943
1029
|
}
|
|
944
1030
|
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const _imageElements: Map<string, ImageBitmap | null> = new Map();
|
|
951
|
-
const _imageLoadingPromises: Map<string, Promise<boolean>> = new Map();
|
|
952
|
-
|
|
953
|
-
async function loadImage(url: string) {
|
|
954
|
-
if (_imageElements.has(url)) {
|
|
955
|
-
if (_imageLoadingPromises.has(url)) return _imageLoadingPromises.get(url);
|
|
956
|
-
return Promise.resolve(true);
|
|
957
|
-
}
|
|
958
|
-
const promise = new Promise<boolean>(res => {
|
|
959
|
-
_imageElements.set(url, null);
|
|
960
|
-
if(isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Start loading image for tracking: ${url}`);
|
|
961
|
-
const imageElement = document.createElement("img") as HTMLImageElement;
|
|
962
|
-
imageElement.src = url;
|
|
963
|
-
imageElement.addEventListener("load", async () => {
|
|
964
|
-
if(isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Loaded image for tracking: ${url}`);
|
|
965
|
-
const img = await createImageBitmap(imageElement);
|
|
966
|
-
_imageElements.set(url, img);
|
|
967
|
-
res(true);
|
|
968
|
-
});
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
_imageLoadingPromises.set(url, promise);
|
|
972
|
-
promise.finally(() => {
|
|
973
|
-
_imageLoadingPromises.delete(url);
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
return promise;
|
|
977
1031
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
|
2
|
+
import { getParam } from "../../engine/engine_utils.js";
|
|
3
|
+
|
|
4
|
+
import type { WebXRImageTrackingModel } from "./WebXRImageTracking.js";
|
|
5
|
+
|
|
6
|
+
// Kept in a dedicated module (with only lightweight imports) so the marker-loading logic can be unit
|
|
7
|
+
// tested without pulling in the heavy WebXRImageTracking / USDZ exporter import graph.
|
|
8
|
+
|
|
9
|
+
const debug = getParam("debugimagetracking");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cache of decoded marker bitmaps, keyed by image url.
|
|
13
|
+
* Also read by the USDZ exporter (see {@link WebXRImageTracking}) to embed the marker into the .usdz.
|
|
14
|
+
*/
|
|
15
|
+
export const _imageElements: Map<string, ImageBitmap | null> = new Map();
|
|
16
|
+
const _imageLoadingPromises: Map<string, Promise<ImageBitmap | null>> = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve marker bitmaps for the given tracked-image models and append them to `args.trackedImages`
|
|
20
|
+
* (the list handed to `navigator.xr.requestSession`).
|
|
21
|
+
*
|
|
22
|
+
* Every valid marker is awaited so that images which are still decoding when the XR session is
|
|
23
|
+
* requested get included instead of being silently dropped (which previously caused markers to only
|
|
24
|
+
* work after re-entering AR). Loading happens in parallel and never rejects: a marker that can not be
|
|
25
|
+
* loaded is skipped individually so a single unavailable image can not fail the whole session request.
|
|
26
|
+
*
|
|
27
|
+
* @param models The configured tracked-image models
|
|
28
|
+
* @param args The session init object; `trackedImages` is created if missing and appended to
|
|
29
|
+
* @param load Injectable loader (defaults to {@link loadImage}); resolves the decoded bitmap or null
|
|
30
|
+
* @returns Map of `trackedImages` index -> model for the appended images (used to resolve results)
|
|
31
|
+
*/
|
|
32
|
+
export async function collectTrackedImages(
|
|
33
|
+
models: readonly WebXRImageTrackingModel[] | undefined | null,
|
|
34
|
+
args: { trackedImages?: Array<{ image: ImageBitmap, widthInMeters: number }> },
|
|
35
|
+
load: (url: string) => Promise<ImageBitmap | null> = loadImage,
|
|
36
|
+
): Promise<Map<number, WebXRImageTrackingModel>> {
|
|
37
|
+
const indexMap = new Map<number, WebXRImageTrackingModel>();
|
|
38
|
+
if (!args.trackedImages) args.trackedImages = [];
|
|
39
|
+
if (!models) return indexMap;
|
|
40
|
+
|
|
41
|
+
const valid = models.filter(m => !!m.image?.length && m.widthInMeters > 0);
|
|
42
|
+
// Load in parallel and wait for all of them. Guard every load with a catch so that a rejecting
|
|
43
|
+
// loader is treated as "unavailable" (null) rather than aborting the whole session request.
|
|
44
|
+
const bitmaps = await Promise.all(valid.map(m => load(m.image!).catch(() => null)));
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < valid.length; i++) {
|
|
47
|
+
const model = valid[i];
|
|
48
|
+
const bitmap = bitmaps[i];
|
|
49
|
+
if (bitmap) {
|
|
50
|
+
indexMap.set(args.trackedImages.length, model);
|
|
51
|
+
args.trackedImages.push({ image: bitmap, widthInMeters: model.widthInMeters });
|
|
52
|
+
}
|
|
53
|
+
else if (isDevEnvironment() || debug) {
|
|
54
|
+
console.warn(`[WebXRImageTracking] Marker image could not be loaded, skipping: ${model.image}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return indexMap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load and decode a marker image into an {@link ImageBitmap}, caching it in {@link _imageElements}.
|
|
62
|
+
* Idempotent: concurrent or repeated calls for the same URL share a single in-flight load.
|
|
63
|
+
*
|
|
64
|
+
* Resolves to `null` and never rejects when the image can not be fetched or decoded, so callers can
|
|
65
|
+
* skip an unavailable marker without failing the surrounding XR session request. A failed load clears
|
|
66
|
+
* its cache entry so a later attempt (e.g. re-entering AR) can retry.
|
|
67
|
+
*/
|
|
68
|
+
export async function loadImage(url: string): Promise<ImageBitmap | null> {
|
|
69
|
+
const cached = _imageElements.get(url);
|
|
70
|
+
if (cached) return cached;
|
|
71
|
+
const pending = _imageLoadingPromises.get(url);
|
|
72
|
+
if (pending) return pending;
|
|
73
|
+
|
|
74
|
+
const promise = new Promise<ImageBitmap | null>(res => {
|
|
75
|
+
_imageElements.set(url, null); // placeholder to dedupe concurrent loads
|
|
76
|
+
if (isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Start loading image for tracking: ${url}`);
|
|
77
|
+
const imageElement = document.createElement("img") as HTMLImageElement;
|
|
78
|
+
imageElement.addEventListener("load", async () => {
|
|
79
|
+
try {
|
|
80
|
+
const img = await createImageBitmap(imageElement);
|
|
81
|
+
_imageElements.set(url, img);
|
|
82
|
+
if (isDevEnvironment() || debug) console.debug(`[WebXRImageTracking] Loaded image for tracking: ${url}`);
|
|
83
|
+
res(img);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(`[WebXRImageTracking] Failed to decode marker image for tracking: ${url}`, err);
|
|
87
|
+
_imageElements.delete(url); // allow a retry later (e.g. re-entering AR)
|
|
88
|
+
res(null);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
imageElement.addEventListener("error", (evt) => {
|
|
92
|
+
console.error(`[WebXRImageTracking] Failed to load marker image for tracking: ${url}`, evt);
|
|
93
|
+
_imageElements.delete(url); // allow a retry later (e.g. re-entering AR)
|
|
94
|
+
res(null);
|
|
95
|
+
});
|
|
96
|
+
imageElement.src = url;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
_imageLoadingPromises.set(url, promise);
|
|
100
|
+
promise.finally(() => {
|
|
101
|
+
_imageLoadingPromises.delete(url);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return promise;
|
|
105
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AxesHelper, Group, Material, Mesh, Object3D, XRHandSpace } from "three";
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { XRControllerModelFactory } from "three/examples/jsm/webxr/XRControllerModelFactory.js";
|
|
4
|
-
import { XRHandMeshModel } from "three/examples/jsm/webxr/XRHandMeshModel.js";
|
|
5
4
|
|
|
6
5
|
import { AssetReference } from "../../../engine/engine_addressables.js";
|
|
7
6
|
import { setDontDestroy } from "../../../engine/engine_gameobject.js";
|
|
@@ -15,6 +14,7 @@ import { NeedleXRController, type NeedleXRControllerEventArgs, type NeedleXREven
|
|
|
15
14
|
import { registerComponentExtension, registerExtensions } from "../../../engine/extensions/extensions.js";
|
|
16
15
|
import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
|
|
17
16
|
import { flipForwardMatrix } from "../../../engine/xr/internal.js";
|
|
17
|
+
import { XRHandMeshModel } from "../../../engine/xr/XRHandMeshModel.js";
|
|
18
18
|
import { Behaviour, Component, GameObject } from "../../Component.js"
|
|
19
19
|
|
|
20
20
|
const debug = getParam("debugwebxr");
|
|
@@ -288,8 +288,6 @@ export class XRControllerModel extends Behaviour {
|
|
|
288
288
|
|
|
289
289
|
const loader = new GLTFLoader();
|
|
290
290
|
addDracoAndKTX2Loaders(loader, context);
|
|
291
|
-
await registerExtensions(loader, context, this.sourceId ?? "", this.sourceId ?? "");
|
|
292
|
-
const componentsExtension = registerComponentExtension(loader);
|
|
293
291
|
|
|
294
292
|
let filename = "";
|
|
295
293
|
|
|
@@ -306,22 +304,30 @@ export class XRControllerModel extends Behaviour {
|
|
|
306
304
|
loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/');
|
|
307
305
|
}
|
|
308
306
|
|
|
307
|
+
// The hand is a separate glTF and needs its own source id: its components resolve their
|
|
308
|
+
// relative uris against it (resolveUrl) and its lightmaps/skybox are keyed by it. Using
|
|
309
|
+
// this component's id resolved the hand's uris next to the HOST glb and let the hand's
|
|
310
|
+
// skybox overwrite the scene's. customHand.url is already resolved, `filename` is not
|
|
311
|
+
// (extension stripped for XRHandMeshModel).
|
|
312
|
+
const handSourceId = customHand?.url ?? `${loader.path}${filename}.glb`;
|
|
313
|
+
await registerExtensions(loader, context, handSourceId, handSourceId);
|
|
314
|
+
const componentsExtension = registerComponentExtension(loader);
|
|
315
|
+
|
|
309
316
|
|
|
310
317
|
const handObject = new Object3D();
|
|
311
318
|
setDontDestroy(handObject);
|
|
312
|
-
// @ts-ignore
|
|
313
319
|
const handmesh = new XRHandMeshModel(handObject, hand, loader.path, filename, loader, (object: Object3D) => {
|
|
314
320
|
|
|
315
321
|
const gltf = componentsExtension?.gltf;
|
|
316
|
-
//
|
|
317
|
-
// we need
|
|
322
|
+
// XRHandMeshModel reparents the hand out of the gltf scene before calling this
|
|
323
|
+
// callback; we need it back in the scene so the builtin components are created on it
|
|
318
324
|
if (gltf?.scene.children?.length === 0) {
|
|
319
325
|
gltf.scene.children[0] = object;
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
// console.log(controller.side, componentsExtension.gltf, object, componentsExtension.gltf.scene?.children)
|
|
323
329
|
if (componentsExtension?.gltf)
|
|
324
|
-
getLoader().createBuiltinComponents(comp.context,
|
|
330
|
+
getLoader().createBuiltinComponents(comp.context, handSourceId, componentsExtension.gltf, null, componentsExtension);
|
|
325
331
|
|
|
326
332
|
// The hand mesh should not receive raycasts
|
|
327
333
|
object.traverse(child => {
|