@vincentt-xr/sdk 2.0.0-alpha.6 → 2.0.0-alpha.8
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/GROUNDING.md +1188 -14
- package/dist/{DebuggerUI-C_rbLdkh.js → DebuggerUI-C-ldiozL.js} +1 -1
- package/dist/{DebuggerUI-DYT_dk1E.cjs → DebuggerUI-CWX5EtQv.cjs} +1 -1
- package/dist/FaceMeshRenderer-BEEXZwS6.cjs +158 -0
- package/dist/FaceMeshRenderer-UuH5sTXG.js +1322 -0
- package/dist/FaceRetouch-C7h7x9kd.js +683 -0
- package/dist/FaceRetouch-DqftT26Y.cjs +134 -0
- package/dist/GLTFLoader-C5q63sKO.js +1633 -0
- package/dist/GLTFLoader-Cb_CRwmG.cjs +1 -0
- package/dist/SegmentationOverlay-DQuW3dsc.js +133 -0
- package/dist/SegmentationOverlay-aWVod8Kl.cjs +1 -0
- package/dist/assets/cache.d.ts +99 -0
- package/dist/assets/estimate.d.ts +11 -0
- package/dist/assets/index.d.ts +7 -0
- package/dist/assets/loaders.d.ts +28 -0
- package/dist/assets/react.d.ts +33 -0
- package/dist/calibration/calibration.d.ts +70 -0
- package/dist/calibration/index.d.ts +6 -0
- package/dist/calibration/storage.d.ts +13 -0
- package/dist/calibration/types.d.ts +53 -0
- package/dist/calibration/useXRCalibration.d.ts +46 -0
- package/dist/camera/CameraPermissionGate.d.ts +26 -0
- package/dist/camera/devices.d.ts +26 -0
- package/dist/camera/index.d.ts +8 -0
- package/dist/camera/permission.d.ts +86 -0
- package/dist/camera/useCameraPermission.d.ts +35 -0
- package/dist/capabilities/detect.d.ts +45 -0
- package/dist/capabilities/index.d.ts +5 -0
- package/dist/capabilities/types.d.ts +120 -0
- package/dist/capabilities/useXRCapabilities.d.ts +18 -0
- package/dist/compat/environment.d.ts +5 -0
- package/dist/compat/index.d.ts +4 -0
- package/dist/compat/run.d.ts +18 -0
- package/dist/compat/scenarios.d.ts +10 -0
- package/dist/compat/types.d.ts +101 -0
- package/dist/components/BodyTracker/index.d.ts +9 -0
- package/dist/components/FaceMesh/FaceMeshRenderer.d.ts +11 -3
- package/dist/components/FaceMesh/canonical/CanonicalFaceMeshRenderer.d.ts +41 -5
- package/dist/components/FaceMesh/canonical/parseCanonicalFaceModel.d.ts +12 -0
- package/dist/components/FaceMesh/canonical/shaders.d.ts +1 -1
- package/dist/components/FaceMesh/index.d.ts +2 -2
- package/dist/components/GestureListener/index.d.ts +11 -0
- package/dist/components/HandTracker/index.d.ts +10 -0
- package/dist/components/HolisticTracker/index.d.ts +69 -0
- package/dist/components/HolisticTracker/settings.d.ts +58 -0
- package/dist/components/HolisticTracker/signals.d.ts +49 -0
- package/dist/components/IrisEffect/IrisMaterial.d.ts +73 -0
- package/dist/components/IrisEffect/index.d.ts +51 -0
- package/dist/components/IrisEffect/settings.d.ts +117 -0
- package/dist/components/LipEffect/LipEffect.d.ts +7 -1
- package/dist/components/LipEffect/LipEffectMaterial.d.ts +43 -7
- package/dist/components/LipEffect/LipEffects.d.ts +9 -0
- package/dist/components/LipEffect/defaults.d.ts +27 -4
- package/dist/components/LipEffect/index.d.ts +11 -3
- package/dist/components/LipEffect/settings.d.ts +271 -0
- package/dist/components/LipEffect/types.d.ts +56 -9
- package/dist/components/LipEffect/useLipDesignTextures.d.ts +11 -0
- package/dist/components/LipEffect/uvGuide.d.ts +59 -0
- package/dist/components/PersonCutout/index.d.ts +50 -0
- package/dist/components/PersonCutout/personCutout.d.ts +16 -0
- package/dist/components/SceneObjectRenderer/index.d.ts +9 -2
- package/dist/components/ScreenImage/components/ScreenImageBody.d.ts +1 -1
- package/dist/components/ScreenImage/index.d.ts +3 -1
- package/dist/components/ScreenImage/materials/ScreenImageBlendMaterial.d.ts +1 -1
- package/dist/components/ScreenImage/materials/ScreenImageMaskMaterial.d.ts +11 -0
- package/dist/components/ScreenImage/source.d.ts +6 -0
- package/dist/components/ScreenImage/types.d.ts +18 -0
- package/dist/components/SegmentationOverlay/SegmentationOverlay.d.ts +37 -0
- package/dist/components/SegmentationOverlay/SegmentationOverlayMaterial.d.ts +31 -0
- package/dist/components/SegmentationOverlay/index.d.ts +9 -0
- package/dist/components/SegmentationOverlay/maskTexture.d.ts +23 -0
- package/dist/components/SegmentationOverlay/providers.d.ts +72 -0
- package/dist/components/SegmentationOverlay/settings.d.ts +65 -0
- package/dist/components/XRMediaSurface/index.d.ts +2 -1
- package/dist/components/XRProvider/index.d.ts +16 -2
- package/dist/components/XRViewport/hooks/useViewportExternalFaces.d.ts +1 -1
- package/dist/components/XRViewport/hooks/useViewportMediaSource.d.ts +1 -1
- package/dist/components/XRViewport/hooks/useViewportPipeline.d.ts +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/constants/index.d.ts +4 -1
- package/dist/contexts-CO0sd4JQ.cjs +1 -0
- package/dist/{contexts-DvQI4PqI.js → contexts-FzlnnMYD.js} +252 -246
- package/dist/coordinates/feed.d.ts +52 -0
- package/dist/coordinates/index.d.ts +5 -0
- package/dist/coordinates/spaces.d.ts +49 -0
- package/dist/coordinates/viewport.d.ts +22 -0
- package/dist/coordinates/world.d.ts +15 -0
- package/dist/debug-ui/gesture-settings/index.d.ts +14 -0
- package/dist/debug-ui/holistic-tracker-settings/index.d.ts +16 -0
- package/dist/debug-ui/index.d.ts +14 -0
- package/dist/debug-ui/iris-effect-settings/index.d.ts +12 -0
- package/dist/debug-ui/lip-effect-settings/index.d.ts +15 -0
- package/dist/debug-ui/media-source.cjs +1 -1
- package/dist/debug-ui/media-source.js +1 -1
- package/dist/debug-ui/product-hotspots/index.d.ts +24 -0
- package/dist/debug-ui/segmentation-overlay-settings/index.d.ts +12 -0
- package/dist/debug-ui/xr-diagnostics/index.d.ts +25 -0
- package/dist/debug-ui.cjs +3 -3
- package/dist/debug-ui.js +2057 -1687
- package/dist/detector-K8aej4MB.js +59 -0
- package/dist/detector-ijypGtse.cjs +1 -0
- package/dist/entries/core.d.ts +21 -3
- package/dist/entries/face-effects.d.ts +12 -4
- package/dist/entries/low-level.d.ts +22 -3
- package/dist/entries/product.d.ts +11 -0
- package/dist/entries/tracking.d.ts +26 -1
- package/dist/face-effects.cjs +155 -1
- package/dist/face-effects.js +1134 -2196
- package/dist/gestures/definitions.d.ts +85 -0
- package/dist/gestures/detector.d.ts +87 -0
- package/dist/gestures/index.d.ts +8 -0
- package/dist/gestures/measures.d.ts +101 -0
- package/dist/gestures/settings.d.ts +49 -0
- package/dist/{getFaceInfo-B43Cwq4n.js → getFaceInfo-BkZfaQs-.js} +5 -5
- package/dist/{getFaceInfo-DEzcL_Ub.cjs → getFaceInfo-ahPxQFGu.cjs} +1 -1
- package/dist/hooks/selectors.d.ts +68 -1
- package/dist/hooks/useCalibrationFaceTracker.d.ts +1 -1
- package/dist/hooks/useFaceInfo.d.ts +13 -1
- package/dist/hooks/useFaceInfos.d.ts +1 -1
- package/dist/hooks/useGesture.d.ts +32 -0
- package/dist/hooks/useIrisTracking.d.ts +45 -0
- package/dist/hooks/useXRDiagnostics.d.ts +81 -0
- package/dist/hooks/useXRModel.d.ts +7 -0
- package/dist/hooks/useXRSession/facade.d.ts +44 -2
- package/dist/hooks/useXRSession/index.d.ts +2 -2
- package/dist/hooks/useXRSession/reason.d.ts +3 -1
- package/dist/hooks/useXRSession/registry.d.ts +14 -0
- package/dist/hooks/useXRSession/store.d.ts +52 -3
- package/dist/index--RgGzZ6X.cjs +47 -0
- package/dist/{index-uE3sKczj.js → index-CHVbH0hz.js} +4 -4
- package/dist/index-CeVkgsag.cjs +1 -0
- package/dist/index-CnGox6Xr.js +3054 -0
- package/dist/{index-B-s4YbWu.js → index-CsUdQ-pn.js} +113 -111
- package/dist/{index-CY7Z8cHV.cjs → index-DKBAh6hV.cjs} +1 -1
- package/dist/index-DsZwTfG0.cjs +1 -0
- package/dist/index-Dw6tj2Mi.js +144 -0
- package/dist/index-OCPjOaeP.cjs +1 -0
- package/dist/{index-G_5h_FJQ.js → index-s9pF4zkG.js} +999 -1014
- package/dist/internal-BYza1h8_.cjs +1 -0
- package/dist/{internal-ldPYiUF0.js → internal-vsfNQycj.js} +15 -16
- package/dist/internal.cjs +1 -1
- package/dist/internal.js +2 -2
- package/dist/iris/compute.d.ts +25 -0
- package/dist/iris/index.d.ts +4 -0
- package/dist/iris/lead.d.ts +19 -0
- package/dist/iris/types.d.ts +87 -0
- package/dist/landmarkMask-CbTFzBYG.js +224 -0
- package/dist/landmarkMask-u7es2k4X.cjs +1 -0
- package/dist/low-level.cjs +1 -1
- package/dist/low-level.js +1104 -639
- package/dist/main.cjs +1 -1
- package/dist/main.js +1519 -462
- package/dist/measures-9dQPdw96.js +154 -0
- package/dist/measures-KfJzpvod.cjs +1 -0
- package/dist/models/Base/index.d.ts +49 -0
- package/dist/models/Body/index.d.ts +1 -1
- package/dist/models/Face/index.d.ts +1 -1
- package/dist/models/Hand/GestureTracking.d.ts +1 -1
- package/dist/models/Hand/HandTracking.d.ts +1 -1
- package/dist/models/Holistic/index.d.ts +49 -0
- package/dist/models/Holistic/result.d.ts +68 -0
- package/dist/models/SemanticSegmentation/index.d.ts +31 -0
- package/dist/models/SemanticSegmentation/presets.d.ts +28 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/product/ProductDimensions.d.ts +16 -0
- package/dist/product/ProductHotspot.d.ts +16 -0
- package/dist/product/ProductViewer.d.ts +9 -0
- package/dist/product/details.d.ts +53 -0
- package/dist/product/index.d.ts +14 -0
- package/dist/product/loadModelViewer.d.ts +4 -0
- package/dist/product/types.d.ts +71 -0
- package/dist/product/useProductAr.d.ts +37 -0
- package/dist/product/useProductDetails.d.ts +60 -0
- package/dist/product.cjs +1 -0
- package/dist/product.js +414 -0
- package/dist/providers-C0Dlhh9Z.js +365 -0
- package/dist/providers-YNwkmNeK.cjs +62 -0
- package/dist/quality/XRQualityController.d.ts +25 -0
- package/dist/quality/controller.d.ts +33 -0
- package/dist/quality/index.d.ts +6 -0
- package/dist/quality/types.d.ts +100 -0
- package/dist/quality/useXRQuality.d.ts +28 -0
- package/dist/{renderGroups-CFt1yYLB.js → renderGroups-CN1cGFUo.js} +1 -1
- package/dist/{renderGroups-BJsx32q5.cjs → renderGroups-O4KmaMl9.cjs} +1 -1
- package/dist/renderPasses-Cg-zqmEz.cjs +21 -0
- package/dist/renderPasses-NnKZVfBy.js +1460 -0
- package/dist/scene-object.cjs +1 -1
- package/dist/scene-object.js +2 -2
- package/dist/screen2d/coordinates.d.ts +20 -0
- package/dist/screenTransform-BenfCtZt.js +44 -0
- package/dist/screenTransform-BiXdpVf0.cjs +1 -0
- package/dist/segmentation-9PhSvc-R.cjs +1 -0
- package/dist/segmentation-CeOTKemr.js +5 -0
- package/dist/segmentation2d/faceMemory.d.ts +28 -0
- package/dist/segmentation2d/faceRegions.d.ts +24 -0
- package/dist/segmentation2d/index.d.ts +9 -0
- package/dist/segmentation2d/maskUv.d.ts +23 -0
- package/dist/segmentation2d/rasterize.d.ts +14 -0
- package/dist/segmentation2d/types.d.ts +46 -0
- package/dist/selectors-C5G1PMYL.js +195 -0
- package/dist/selectors-Du_PsVnW.cjs +1 -0
- package/dist/{session-CANvoLD6.cjs → session-BBr0yN8h.cjs} +7 -7
- package/dist/{session-Dhi0OqZo.js → session-Cy0m7l7-.js} +76 -76
- package/dist/settings-DRR7-snV.cjs +1 -0
- package/dist/settings-aQk845NJ.js +73 -0
- package/dist/subjects/index.d.ts +2 -0
- package/dist/subjects/select.d.ts +62 -0
- package/dist/subjects/types.d.ts +55 -0
- package/dist/tracking/coordinates.d.ts +5 -0
- package/dist/tracking-config/apply.d.ts +57 -0
- package/dist/tracking-config/index.d.ts +7 -0
- package/dist/tracking-config/resolve.d.ts +25 -0
- package/dist/tracking-config/storage.d.ts +8 -0
- package/dist/tracking-config/types.d.ts +106 -0
- package/dist/tracking-config/useXRTrackingConfig.d.ts +38 -0
- package/dist/tracking.cjs +1 -1
- package/dist/tracking.js +1056 -380
- package/dist/types/common.d.ts +11 -1
- package/dist/types/compositor.d.ts +21 -0
- package/dist/types/gesture-tracking.d.ts +2 -2
- package/dist/types/hand-tracking.d.ts +3 -3
- package/dist/types/holistic.d.ts +121 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model.d.ts +22 -2
- package/dist/types/nodes.d.ts +29 -4
- package/dist/types/segmentation.d.ts +19 -0
- package/dist/types/session.d.ts +48 -4
- package/dist/useFaceInfo-Blf7FCu3.js +303 -0
- package/dist/useFaceInfo-C5mO8NQY.cjs +1 -0
- package/dist/useXRCapabilities-B1R2ghy-.cjs +1 -0
- package/dist/useXRCapabilities-C68v__wB.js +30 -0
- package/dist/utils/fns.d.ts +6 -3
- package/dist/utils/integrity.d.ts +4 -1
- package/dist/utils/updateThrottle.d.ts +2 -0
- package/dist/utils/useStableUniforms.d.ts +18 -0
- package/docs/SDK_DOCUMENTATION.md +3 -0
- package/docs/api/adaptive-quality.mdx +104 -0
- package/docs/api/asset-lifecycle.mdx +78 -0
- package/docs/api/camera-permission.mdx +142 -0
- package/docs/api/capture-recording.mdx +9 -1
- package/docs/api/coordinate-spaces.mdx +165 -0
- package/docs/api/cross-camera.mdx +88 -0
- package/docs/api/diagnostics-calibration.mdx +97 -0
- package/docs/api/face-effects.mdx +6 -0
- package/docs/api/face-mesh.mdx +35 -0
- package/docs/api/gestures.mdx +173 -0
- package/docs/api/holistic-tracker.mdx +159 -0
- package/docs/api/iris-tracking.mdx +242 -0
- package/docs/api/multi-subject.mdx +95 -0
- package/docs/api/person-cutout.mdx +136 -0
- package/docs/api/product-ar.mdx +111 -0
- package/docs/api/product-details.mdx +78 -0
- package/docs/api/runtime-3d.mdx +5 -0
- package/docs/api/screen-image.mdx +3 -1
- package/docs/api/screen-space-coordinates.mdx +35 -1
- package/docs/api/segmentation-overlay.mdx +301 -0
- package/docs/api/session-lifecycle.mdx +136 -0
- package/docs/api/trackers.mdx +18 -0
- package/docs/api/tracking-config.mdx +108 -0
- package/docs/docs.json +23 -1
- package/docs/examples/lip-shaders.mdx +224 -0
- package/docs/guides/lip-effect-textures.mdx +292 -0
- package/docs/guides/lip-uv-template.png +0 -0
- package/docs/guides/lip-uv-template.svg +1 -0
- package/package.json +16 -1
- package/dist/FaceMeshRenderer-D1WsrTLA.cjs +0 -155
- package/dist/FaceMeshRenderer-DSCd1H55.js +0 -1264
- package/dist/FaceRetouch-CMJbGThL.js +0 -551
- package/dist/FaceRetouch-DsE0Dbw-.cjs +0 -134
- package/dist/contexts-h-9TRH0r.cjs +0 -1
- package/dist/index-C0-fg1Hk.cjs +0 -1
- package/dist/index-CxMEeCRP.cjs +0 -1
- package/dist/index-D67hHm44.cjs +0 -1
- package/dist/index-DPPpU5By.cjs +0 -17
- package/dist/index-DYfY9ljy.js +0 -111
- package/dist/index-I4eF5TKT.js +0 -1457
- package/dist/internal-MvKrz0Nt.cjs +0 -1
- package/dist/renderLayers-lDtL0_43.cjs +0 -1
- package/dist/renderLayers-zrK2pv5O.js +0 -13
- package/dist/renderPasses-C72MbZ-a.js +0 -1500
- package/dist/renderPasses-D-Krl6D0.cjs +0 -21
- package/dist/selectors-ByKNTXeA.js +0 -59
- package/dist/selectors-CAwmL9VW.cjs +0 -1
- package/dist/useFaceInfo-BSZq7AO1.cjs +0 -1
- package/dist/useFaceInfo-FVQBBNdY.js +0 -62
- package/dist/useFaceResults-C3oZkn2Z.cjs +0 -1
- package/dist/useFaceResults-D62STzT1.js +0 -19
package/GROUNDING.md
CHANGED
|
@@ -10,11 +10,12 @@ or spelunk `node_modules` for the API.
|
|
|
10
10
|
The API is split into task-domain entry points. Import each symbol from the door
|
|
11
11
|
this reference shows for it; the imports here are literal.
|
|
12
12
|
|
|
13
|
-
- `@vincentt-xr/sdk` — **core**: providers, scene, screen-space layout, runtime renderers (`Transform3D`, `MeshRenderer`, `SceneObjectRenderer`), `TextLabel`, `Panel`, `SpriteAnimation`, `VideoBackground`, `AspectRatioContainer`, capture/share (`useFrameCapture`, `useMediaRecorder`, `dataURLtoFile`), audio.
|
|
13
|
+
- `@vincentt-xr/sdk` — **core**: providers, scene, screen-space layout, runtime renderers (`Transform3D`, `MeshRenderer`, `SceneObjectRenderer`), `TextLabel`, `Panel`, `SpriteAnimation`, `VideoBackground`, `AspectRatioContainer`, capture/share (`useFrameCapture`, `useMediaRecorder`, `dataURLtoFile`), audio, capability detection (`useXRCapabilities`, `detectXRCapabilities`, `resolveTrackerSupport`), camera permission (`CameraPermissionGate`, `useCameraPermission`), adaptive quality (`useXRQuality`, `XRQualityController`), persistent tracking config (`useXRTrackingConfig`), asset lifecycle (`useXRAsset`, `XRAssetCache`, `defineTextureAsset` / `defineGltfAsset` / `defineVideoAsset`).
|
|
14
14
|
- `@vincentt-xr/sdk/scene-object` — **public authoring state**: scene-object/component types, factories, selectors, and immutable store operations.
|
|
15
|
-
- `@vincentt-xr/sdk/tracking` — **trackers**: face/hand/gesture/body/segmentation, `TrackingAnchor`, `GestureTrigger`, the bare `FaceMesh`, `useFaceResults
|
|
15
|
+
- `@vincentt-xr/sdk/tracking` — **trackers**: face/hand/gesture/body/holistic/segmentation, `TrackingAnchor`, `GestureTrigger`, the bare `FaceMesh`, `useFaceResults`, gestures, iris, device calibration (`useXRCalibration`), multi-subject policy (`selectSubject`, `useFaceInfo({ subject })`).
|
|
16
16
|
- `@vincentt-xr/sdk/face-effects` — **face deep**: FaceMesh material config, retouch, canonical mesh, head-binding.
|
|
17
|
-
- `@vincentt-xr/sdk/
|
|
17
|
+
- `@vincentt-xr/sdk/product` — **placement AR** for product pages: `ProductViewer` (Google `<model-viewer>`, optional peer, lazy-loaded), `useProductAr`. A separate mode from the session: placement AR is model-viewer, face/body AR is the session, never both at once.
|
|
18
|
+
- `@vincentt-xr/sdk/low-level` — **escape hatch**: raw model-node reads, selector hooks (`useXRLifecycle`, `useXRTrackerStatus`, …), tracking diagnostics (`useXRDiagnostics`), custom-tracker plumbing. Reach here only when core + tracking can't express it.
|
|
18
19
|
|
|
19
20
|
Trackers (`<FaceTracker>`, `<GestureTracker>`, `<HandTracker>`, `<BodyTracker>`)
|
|
20
21
|
**self-register** when mounted — no explicit `registerXRPipeline` call. Per-frame
|
|
@@ -94,6 +95,19 @@ import { ScreenSpaceUI, ScreenTransform } from "@vincentt-xr/sdk";
|
|
|
94
95
|
|
|
95
96
|
Coordinates are normalized: -1 to +1, with +y up. The center of the screen is `(0, 0)`. The anchors rect controls BOTH size and placement of the child — it stretches the child (plane, TextLabel, sprite) to fill the rect. Don't add `scale`/`position`/`planeGeometry args` to size or move a screen-anchored child; set the anchors.
|
|
96
97
|
|
|
98
|
+
Canvas screen pixels map across the live viewport on both axes, including
|
|
99
|
+
landscape and square displays. They are not automatically letterboxed. Preserve
|
|
100
|
+
an image or video's own aspect with its fit mode inside a screen rect, not with
|
|
101
|
+
manual point conversion. `ScreenTransform2DSettings`, used by serialized editor
|
|
102
|
+
layouts, is intentionally contain-fitted so text and its transform guide retain
|
|
103
|
+
their authored aspect ratio.
|
|
104
|
+
|
|
105
|
+
For a custom R3F mesh that needs a translated design point, use
|
|
106
|
+
`screenPixelsToScreenSpace({ point, parentSize })`; it applies the same separate
|
|
107
|
+
X/Y mapping as the screen primitives. `screenPixelsToUnits` is scalar conversion
|
|
108
|
+
for uniform distances and is not a substitute for point placement on a
|
|
109
|
+
non-design-aspect viewport.
|
|
110
|
+
|
|
97
111
|
```tsx
|
|
98
112
|
// Full-screen frame
|
|
99
113
|
<ScreenTransform anchors={{ left: -1, right: 1, top: 1, bottom: -1 }}>
|
|
@@ -528,10 +542,47 @@ projection correction. Author a Transform with `position.y = 0` to opt out.
|
|
|
528
542
|
Use it only when bridging custom runtime code; ordinary scene-object rendering
|
|
529
543
|
should use `SceneObjectRenderer`.
|
|
530
544
|
|
|
545
|
+
SDK material types and shader presets are convenience defaults, not a closed
|
|
546
|
+
material system. For an advanced effect, pass `materialOverride` to
|
|
547
|
+
`FaceMeshRenderer`. It accepts a material node, a three.js material instance
|
|
548
|
+
(set on the mesh directly, so one instance can serve many meshes), or a factory
|
|
549
|
+
returning either, invoked per visible face geometry with `{ cameraTexture, geometryKey, faceUv, lipEdge }`. `faceUv` is
|
|
550
|
+
the shared live camera-UV `BufferAttribute` and `lipEdge` the static lip band
|
|
551
|
+
position (both `null` until the canonical mesh has loaded), bound to every
|
|
552
|
+
group's geometry as `attribute vec2 faceUv` and `attribute float lipEdge`. The override takes
|
|
553
|
+
precedence over presets and stays runtime-only: do not place `THREE.Material`,
|
|
554
|
+
uniforms, or factories in `FaceMeshRendererSettings` or a persisted
|
|
555
|
+
`SceneObjectStore`.
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
<FaceMeshRenderer
|
|
559
|
+
value={faceMeshSettings}
|
|
560
|
+
materialOverride={({ cameraTexture, geometryKey }) => (
|
|
561
|
+
<shaderMaterial
|
|
562
|
+
key={geometryKey}
|
|
563
|
+
uniforms={{ cameraTexture: { value: cameraTexture } }}
|
|
564
|
+
vertexShader={vertexShader}
|
|
565
|
+
fragmentShader={fragmentShader}
|
|
566
|
+
transparent
|
|
567
|
+
/>
|
|
568
|
+
)}
|
|
569
|
+
/>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
For serialized scene objects, pass runtime overrides separately to
|
|
573
|
+
`SceneObjectRenderer`, keyed by the FaceMeshRenderer component ID. R3F owns a
|
|
574
|
+
material node's lifecycle; factories must return stable, ordinary React material
|
|
575
|
+
nodes and must not mutate private renderer internals.
|
|
576
|
+
|
|
531
577
|
`CanonicalFaceMeshRenderer` remains exported from `@vincentt-xr/sdk/face-effects`
|
|
532
578
|
for backwards compatibility and low-level experiments, but new editor and
|
|
533
579
|
scene-object work should use `FaceMeshRenderer`.
|
|
534
580
|
|
|
581
|
+
Every canonical face mesh (including `FaceMeshRenderer` and `LipEffect`) bridges
|
|
582
|
+
tracker dropouts with `holdMs` (default 250) and `predictMs` (default 0),
|
|
583
|
+
both `CanonicalFaceMeshRendererProps`. The contract, defaults, and limits are
|
|
584
|
+
in "Tracker dropouts — the one bridge to use"; do not add a second hold on top.
|
|
585
|
+
|
|
535
586
|
### `FaceRetouch` — legacy landmark skin retouch
|
|
536
587
|
|
|
537
588
|
Use `FaceRetouch` from `@vincentt-xr/sdk/face-effects` for the legacy
|
|
@@ -572,6 +623,350 @@ For multiple people, render one `FaceRetouch` per face:
|
|
|
572
623
|
optional horizontal UV clipping props in `0..1` space, useful for split-screen
|
|
573
624
|
comparison demos.
|
|
574
625
|
|
|
626
|
+
### Iris tracking — `useIrisTracking()` and `<IrisEffect>`
|
|
627
|
+
|
|
628
|
+
Iris data comes from the ten refined iris landmarks the shared face tracker
|
|
629
|
+
already emits: `useIrisTracking` (`@vincentt-xr/sdk/tracking`) acquires the
|
|
630
|
+
same `XRModel.FACE_TRACKER` as `FaceTracker` / `useFaceInfo`, so there is no
|
|
631
|
+
second detector. `<IrisEffect>` (`@vincentt-xr/sdk/face-effects`) draws a
|
|
632
|
+
material over each iris.
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
import { useIrisTracking, IRIS_LANDMARKS } from "@vincentt-xr/sdk/tracking";
|
|
636
|
+
import { IrisEffect } from "@vincentt-xr/sdk/face-effects";
|
|
637
|
+
|
|
638
|
+
<IrisEffect mode="shader" color="#4fa3ff" opacity={0.85} smoothing={15} />;
|
|
639
|
+
<IrisEffect mode="image" textureUrl="/iris.png" scale={1.1} mirrored />;
|
|
640
|
+
|
|
641
|
+
const { result, status } = useIrisTracking({ faceIndex: 0, smoothing: 15 });
|
|
642
|
+
useFrame(() => {
|
|
643
|
+
if (!result.detected) return;
|
|
644
|
+
const { center, radius, offset, openness, confidence } = result.left;
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
- **Result** (`IrisResult`, stable object mutated per frame): `version`,
|
|
649
|
+
`timestampMs`, `faceIndex`, `detected`, `held`, `confidence`, and `left` /
|
|
650
|
+
`right` (`IrisEye`): `center` + `ring` + `eyeCenter` in MediaPipe image
|
|
651
|
+
space (0..1, top-left, Y down, like `FaceInfo.landmarks`), `trackerCenter`
|
|
652
|
+
in tracker space, `radius` / `eyeWidth` in image-width units, `openness`
|
|
653
|
+
(lid gap / eye width), `offset` ([-1, 1] per axis, +x the person's left,
|
|
654
|
+
+y up, on every source: the horizontal sign is corrected on the mirrored
|
|
655
|
+
webcam feed) and `confidence`. Indices: `IRIS_LANDMARKS` (left center 473, ring
|
|
656
|
+
474–477; right center 468, ring 469–472; corners and lids per side). Sides
|
|
657
|
+
follow the mesh topology and are anatomical on every source, mirrored
|
|
658
|
+
webcam included; positions need no flip.
|
|
659
|
+
- **`offset` is NOT gaze.** It is where the iris sits inside the eyelid
|
|
660
|
+
opening and ignores head pose and eyeball geometry. The SDK provides no
|
|
661
|
+
gaze estimation; do not present `offset` as one. For blinks and
|
|
662
|
+
eye-look expressions use blendshapes (`useFaceInfo` / `useFaceAction`).
|
|
663
|
+
- **Confidence**: MediaPipe scores no iris point and emits them for closed
|
|
664
|
+
eyes, so `eye.confidence` = `openness / 0.2` clamped to 0..1, scaled by
|
|
665
|
+
the hold decay; `result.confidence` is 1 live, decaying to 0 over
|
|
666
|
+
`holdMs` while `held`. Gate effects on it; the built-in materials fade with
|
|
667
|
+
`irisConfidence`. `status` is `loading` → `ready` / `error` for the model.
|
|
668
|
+
- **Shared contracts**: dropouts use `FaceTrackMemory` (`holdMs` 250,
|
|
669
|
+
`predictMs` 0); `smoothing` is `TrackingAnchor`'s One Euro on centers and
|
|
670
|
+
radius (default 0 = raw; ~10 tames jitter, but it is a 1 Hz-class low-pass
|
|
671
|
+
that lags a 2–3 Hz head shake, so keep it low for fast motion), reset on
|
|
672
|
+
re-acquire; `leadMs` (default 80, 0 off)
|
|
673
|
+
advances the centers along their last sample-to-sample velocity between
|
|
674
|
+
inference samples, so a 24 fps face model reads continuous at 60+ fps
|
|
675
|
+
instead of hold-and-jump (raise the face `targetFps` first when the lag
|
|
676
|
+
itself is the problem); `faceIndex` / `numFaces`
|
|
677
|
+
select faces like `useFaceInfo` (array index, first consumer sets
|
|
678
|
+
`numFaces`). When the face stays lost beyond `holdMs` the effect hides at
|
|
679
|
+
once: no stale eye effects.
|
|
680
|
+
- **`<IrisEffect>`**: flat `IrisEffectSettings` props (`eye` both | left |
|
|
681
|
+
right, `mode` shader | image, `color`, `textureUrl`, `tintImage` (multiply
|
|
682
|
+
the image by `color`; off by default, images show as authored), `scale` × iris
|
|
683
|
+
radius, `opacity`, `targetFps` (face inference cap; 0 = session, first
|
|
684
|
+
face consumer wins, so request the same value everywhere; a later change
|
|
685
|
+
takes effect only once every holder has released the model — a lone
|
|
686
|
+
`<IrisEffect>` re-acquires and re-initializes, a `registerXRPipeline`
|
|
687
|
+
entry pins the first value for the session).
|
|
688
|
+
**_`targetFps: 60` gives the best iris following_** (fast head motion
|
|
689
|
+
reads continuous) **_but use it with caution_**: it runs face inference
|
|
690
|
+
at 2.5× the template's 24 fps on every frame, which heats and throttles
|
|
691
|
+
phones. Ship 60 only when the effect is the point of the experience and
|
|
692
|
+
the target devices have been checked; otherwise keep the session default
|
|
693
|
+
and rely on `leadMs`, or pick 30 as the middle ground. Then `smoothing`,
|
|
694
|
+
`leadMs`, `mirrored` flips the material only, `faceIndex`, `holdMs`) plus `numFaces`, `active`, `predictMs`, `texture`
|
|
695
|
+
(pre-loaded, beats `textureUrl`), `material` (a three material or a
|
|
696
|
+
factory `(IrisMaterialContext) => Material`, called once per eye for each
|
|
697
|
+
`material` identity; the effect disposes what a factory created when the
|
|
698
|
+
factory changes or on unmount, an instance stays yours; read the live
|
|
699
|
+
texture from `uniforms.map`, `context.texture` is only the texture known
|
|
700
|
+
when the factory ran), `renderOrder`, `onTracking(handle)`. Each eye is a quad spanning
|
|
701
|
+
the iris diameter on the z=0 plane; the built-in shader is a procedural
|
|
702
|
+
iris whose pupil follows `offset`; the image material covers the quad
|
|
703
|
+
tinted by `color`. Every material gets the same stable uniforms
|
|
704
|
+
(`IrisUniforms`: `irisCenter`, `irisRadius`, `irisOffset`,
|
|
705
|
+
`irisConfidence`, `irisOpenness`, `time`, `color`, `opacity`, `mirror`,
|
|
706
|
+
`tintImage`, `map`), refreshed in place by `updateIrisUniforms`; the fragment chunks
|
|
707
|
+
`IRIS_SHADER_FRAGMENT` / `IRIS_IMAGE_FRAGMENT` and `IRIS_VERTEX_SHADER`
|
|
708
|
+
are exported as starting points. Debug panel: `IrisEffectSettingsPanel`
|
|
709
|
+
(`@vincentt-xr/sdk/debug-ui`) with `resolveIrisEffectSettings` /
|
|
710
|
+
`patchIrisEffectSettings`.
|
|
711
|
+
- Use iris tracking for eye decorations, pupil overlays and shader inputs;
|
|
712
|
+
blendshapes for blink/wink/eye-look expressions; `PinToMesh` / FaceMesh
|
|
713
|
+
materials for content that must deform with the skin. To hide the iris
|
|
714
|
+
behind the eyelids, draw a `CanonicalFaceMeshRenderer` with
|
|
715
|
+
`showEyeGeometry={false}` at a higher `renderOrder` than the effect and a
|
|
716
|
+
`materialOverride` that samples the camera texture through the mesh's
|
|
717
|
+
live `faceUv` attribute with `transparent: true` and `depthTest: false`
|
|
718
|
+
(the example's "eyelid mask"); it repaints the feed over the face except
|
|
719
|
+
the eye openings. It must be transparent: three draws every opaque object
|
|
720
|
+
before any transparent one, and the iris quads are transparent. Full contract and the
|
|
721
|
+
manual test plan: `docs/api/iris-tracking.mdx`; example:
|
|
722
|
+
`/iris-tracking`.
|
|
723
|
+
- **Low-level iris seam** (`@vincentt-xr/sdk/low-level`): `createIrisResult`,
|
|
724
|
+
`updateIrisResult(result, landmarks, nowMs, { faceIndex, held,
|
|
725
|
+
faceConfidence, mirrored })`, `computeIrisEye`, `createIrisEye`,
|
|
726
|
+
`IRIS_FULL_OPENNESS` and `IrisLead` are the allocation-free helpers behind
|
|
727
|
+
`useIrisTracking`, for bring-your-own-landmarks pipelines (a recorded
|
|
728
|
+
clip, a custom tracker). App code reads `useIrisTracking`; only reach for
|
|
729
|
+
these when you already hold 478-point landmarks.
|
|
730
|
+
|
|
731
|
+
### `<LipEffect>` — lip tint / lipstick overlay
|
|
732
|
+
|
|
733
|
+
Use `LipEffect` from `@vincentt-xr/sdk/face-effects` to paint the lips of a
|
|
734
|
+
tracked face. It renders only the canonical lip-surface triangles (between the
|
|
735
|
+
MediaPipe outer and inner lip rings) and blends a design texture, or a solid
|
|
736
|
+
colour, over the live camera pixels. Mount a `<FaceTracker>` first.
|
|
737
|
+
|
|
738
|
+
```tsx
|
|
739
|
+
import { LipEffect } from "@vincentt-xr/sdk/face-effects";
|
|
740
|
+
|
|
741
|
+
<LipEffect preset="gloss" designTexture="/lips/gloss.png" color="#e85d75" />;
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Props are `Partial<LipEffectSettings>` plus `faceIndex`, `designTexture` (URL or
|
|
745
|
+
`THREE.Texture`, canonical face UV space; omit it for a solid `color` fill),
|
|
746
|
+
`lowerDesignTexture` (lower lip, used with `splitLips`), `material`,
|
|
747
|
+
`sharedMaterial` (a material instance to use instead of the built-in one),
|
|
748
|
+
`maxUpdateFps` (cap how often the lip mesh follows the landmarks), `holdMs`
|
|
749
|
+
(default 250: keep the lips on the last landmarks across tracker dropouts) and
|
|
750
|
+
`predictMs` (default 0: extrapolate the held landmarks along their last
|
|
751
|
+
velocity), and `renderOrder` (default `30`). `LipEffectSettings` is flat and
|
|
752
|
+
serializable:
|
|
753
|
+
|
|
754
|
+
| Field | Meaning |
|
|
755
|
+
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
756
|
+
| `preset` | `"custom"` or one of `lipstick`, `gloss`, `matte`, `tint`, `metallic`, `gradient` |
|
|
757
|
+
| `maskMode` | `"outer-and-inner-lips"` (default, mouth interior cut out) or `"outer-lips"` |
|
|
758
|
+
| `color`, `opacity`, `intensity` | Tint, coverage, and how much the design is multiplied by the tint (0 = as painted) |
|
|
759
|
+
| `shading` | How much the camera's own lip light/shadow modulates the paint (0 = flat) |
|
|
760
|
+
| `gloss`, `roughness`, `highlightColor` | Specular highlight amount, spread, and colour |
|
|
761
|
+
| `colorMode` | `"texture"` (sample the design when given) or `"solid"` (procedural: colour + gradient only) |
|
|
762
|
+
| `gradientColor`, `gradientAmount`, `gradientDirection` | Second colour blended `outward` from the mouth line, or `vertical` / `horizontal` across the lip box |
|
|
763
|
+
| `edgeSoftness` | Feather at the outer lip boundary as a fraction of the lip band (0 = hard edge, default 0.12) |
|
|
764
|
+
| `innerLipColor`, `innerLipAmount`, `innerLipWidth` | Separate tint for the inner lip near the mouth line (and the mouth interior in `outer-lips` mode) |
|
|
765
|
+
| `splitLips`, `lowerLipColor`, `lowerLipOpacity` | Give the lower lip its own colour and opacity; `lowerDesignTexture` prop gives it its own texture |
|
|
766
|
+
| `textureScaleX/Y`, `textureOffsetX/Y` | Design transform about the lip centre; offset is in lip widths/heights |
|
|
767
|
+
| `textureRotation`, `textureFlipX/Y` | Degrees counter-clockwise as seen on the face; mirror across the lip centre |
|
|
768
|
+
|
|
769
|
+
Resolution order is defaults, then the named preset, then every explicit prop.
|
|
770
|
+
Helpers: `resolveLipEffectSettings(partial)`, `patchLipEffectSettings(current, patch)`
|
|
771
|
+
(switching `preset` re-applies that preset), `LIP_EFFECT_PRESETS`,
|
|
772
|
+
`LIP_EFFECT_PRESET_KEYS`, `DEFAULT_LIP_EFFECT_SETTINGS`,
|
|
773
|
+
`LIP_EFFECT_SETTINGS_FIELDS` (typed `SettingsPanel` fields), and
|
|
774
|
+
`createLipTextureTransform(settings, bounds)` (the UV sampling `Matrix3`).
|
|
775
|
+
|
|
776
|
+
**Live controls.** `LipEffectSettingsPanel` from `@vincentt-xr/sdk/debug-ui` edits
|
|
777
|
+
the same settings object that drives rendering; spread what it emits onto the
|
|
778
|
+
component:
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
import { LipEffectSettingsPanel } from "@vincentt-xr/sdk/debug-ui";
|
|
782
|
+
|
|
783
|
+
const [settings, setSettings] = useState(() => resolveLipEffectSettings({ preset: "lipstick" }));
|
|
784
|
+
<LipEffect {...settings} designTexture={url} />;
|
|
785
|
+
<LipEffectSettingsPanel value={settings} onChange={setSettings} />;
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Design texture contract.** A square image in the canonical face UV atlas (same
|
|
789
|
+
layout as `Canonical_Face_Color.png`). Only pixels under the lip triangles are
|
|
790
|
+
sampled: roughly x 38–62 %, y 65–74 % of the image. Transparent pixels leave the
|
|
791
|
+
skin untouched (alpha drives the blend); RGB is multiplied by `color` at
|
|
792
|
+
`intensity`, so paint white/light for a pure tint. Image-left is the subject's
|
|
793
|
+
right lip corner: screen-right on the mirrored webcam view, screen-left for
|
|
794
|
+
image/video sources. Image-top is `v = 1`. Never remap UVs or assume a lips-only
|
|
795
|
+
canvas; use the texture transform settings to fit a design instead. Generate
|
|
796
|
+
the template instead of drawing it: `createLipUvGuideSvg(mesh)`,
|
|
797
|
+
`createLipUvGuideDataUrl(mesh, { region: "lips" })`, and
|
|
798
|
+
`createLipUvGuidePngBlob(mesh)` (browser) from the `useCanonicalFaceMesh()`
|
|
799
|
+
result; `getLipUvBounds(mesh)` for the numbers (`CANONICAL_LIP_UV_BOUNDS` is the
|
|
800
|
+
shipped mesh's fallback); `lipUvToPixel(u, v, size)` for the `flipY` pixel
|
|
801
|
+
mapping. The shipped `docs/guides/lip-uv-template.svg` / `.png` are regenerated
|
|
802
|
+
with `pnpm dlx tsx scripts/generate-lip-uv-guide.ts --png`; the `/lip-effect`
|
|
803
|
+
example page has a `Texture preview / UV guide` toggle and `Download SVG / PNG`
|
|
804
|
+
buttons. Full guide: `docs/guides/lip-effect-textures.mdx`.
|
|
805
|
+
|
|
806
|
+
**Several faces.** `LipEffects` renders one lip effect per face slot:
|
|
807
|
+
|
|
808
|
+
```tsx
|
|
809
|
+
import { LipEffects } from "@vincentt-xr/sdk/face-effects";
|
|
810
|
+
|
|
811
|
+
<FaceTracker numFaces={2} />;
|
|
812
|
+
<LipEffects maxFaces={2} faces={{ preset: "gloss", designTexture: url }} />;
|
|
813
|
+
<LipEffects
|
|
814
|
+
maxFaces={2}
|
|
815
|
+
faces={(faceIndex) =>
|
|
816
|
+
faceIndex === 0 ? { preset: "matte" } : { preset: "gloss", designTexture: url }
|
|
817
|
+
}
|
|
818
|
+
/>;
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
`faces` is one config for all faces or a picker per face (`null` skips a
|
|
822
|
+
face); each config takes everything `LipEffect` takes except `faceIndex`. With a
|
|
823
|
+
single config the faces share one built-in material (`shareMaterial`, default
|
|
824
|
+
on for a single config); `maxUpdateFps` and `renderOrder` apply to all faces.
|
|
825
|
+
Match `maxFaces` to the tracker's `numFaces`.
|
|
826
|
+
|
|
827
|
+
**Performance.** The built-in material is a plain three.js object:
|
|
828
|
+
`createLipEffectMaterial()`, `updateLipEffectMaterial(material, inputs)`, and the
|
|
829
|
+
owning hook `useLipEffectMaterial(inputs)` (inputs: `cameraTexture`,
|
|
830
|
+
`designTexture`, `lowerDesignTexture`, `settings`, `lipUvBounds`,
|
|
831
|
+
`subtractMouth`). Pass one instance as `sharedMaterial` to several `LipEffect`s,
|
|
832
|
+
or return a material instance from a `material` factory, and all of them draw
|
|
833
|
+
with the same program and uniforms. `maxUpdateFps` throttles the per-frame
|
|
834
|
+
landmark-to-vertex update (visibility still follows face presence every frame).
|
|
835
|
+
Tracking cost itself is set on the session: `<XRProvider perf={{ targetFps,
|
|
836
|
+
camRes }}>` lowers inference rate and the analysed resolution, and
|
|
837
|
+
`<FaceTracker targetFps>` overrides it per tracker. `useLipDesignTextures(upper,
|
|
838
|
+
lower)` resolves URL-or-texture props through R3F's loader cache, so faces that
|
|
839
|
+
share a URL share a texture.
|
|
840
|
+
|
|
841
|
+
**Custom material.** `material` accepts a React material node, a three.js
|
|
842
|
+
material instance, or a factory that receives a stable
|
|
843
|
+
`LipEffectMaterialContext` and returns either:
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
type LipEffectMaterialContext = {
|
|
847
|
+
cameraTexture: THREE.Texture | null;
|
|
848
|
+
designTexture: THREE.Texture | null;
|
|
849
|
+
faceUv: THREE.BufferAttribute; // live camera UVs, bound as `attribute vec2 faceUv`
|
|
850
|
+
lipEdge: THREE.BufferAttribute; // static band position, `attribute float lipEdge` (0 outer ring, 1 mouth line)
|
|
851
|
+
lowerDesignTexture: THREE.Texture | null;
|
|
852
|
+
faceIndex: number;
|
|
853
|
+
maskMode: LipMaskMode;
|
|
854
|
+
lipUvBounds: LipUvBounds; // lip region of the UV atlas
|
|
855
|
+
settings: LipEffectSettings; // the resolved settings
|
|
856
|
+
};
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
The factory runs once per lip geometry group (plus the mouth interior in
|
|
860
|
+
`outer-lips` mode) and only after the canonical mesh has loaded. Honour
|
|
861
|
+
`maskMode` with `stencilWrite stencilRef={1}
|
|
862
|
+
stencilFunc={THREE.NotEqualStencilFunc}` plus `KeepStencilOp` for
|
|
863
|
+
`stencilFail` / `stencilZFail` / `stencilZPass` (three.js only runs the
|
|
864
|
+
comparison while `stencilWrite` is true; the keep-ops stop the material from
|
|
865
|
+
altering the mouth stencil it tests). That is what the exported
|
|
866
|
+
`LipEffectMaterial`'s `subtractMouth` prop does; reuse that
|
|
867
|
+
component (`cameraTexture`, `designTexture`, `settings`, `lipUvBounds`,
|
|
868
|
+
`subtractMouth`, `lowerDesignTexture`) when only the settings need to differ.
|
|
869
|
+
`LIP_EFFECT_VERTEX_SHADER` / `LIP_EFFECT_FRAGMENT_SHADER` are the built-in GLSL
|
|
870
|
+
sources, and `docs/examples/lip-shaders.mdx` documents four factories (animated
|
|
871
|
+
gloss, noise texture, iridescence, time-based shimmer) built on the vertex
|
|
872
|
+
shader. The parsed mesh exposes the same band position as `lipEdgeDistance`, and
|
|
873
|
+
`getLipUvBounds(mesh).mouthV` is the mouth line in UV space.
|
|
874
|
+
|
|
875
|
+
### `<SegmentationOverlay>` — 2D semantic region layers
|
|
876
|
+
|
|
877
|
+
Use `SegmentationOverlay` from `@vincentt-xr/sdk/face-effects` to draw regions
|
|
878
|
+
of a semantic segmentation mask (`lips`, `eyes`, `brows`, `face_skin`, `mouth`,
|
|
879
|
+
`bg`, `person`; or any label an external model emits, such as `beard` / `ears`
|
|
880
|
+
from a face-parsing model, which the SDK itself does not provide)
|
|
881
|
+
as a tinted, screen-aligned 2D layer over the camera. It needs no canonical 3D
|
|
882
|
+
geometry.
|
|
883
|
+
|
|
884
|
+
```tsx
|
|
885
|
+
import {
|
|
886
|
+
SegmentationOverlay,
|
|
887
|
+
useFaceRegionMask,
|
|
888
|
+
usePersonMask,
|
|
889
|
+
} from "@vincentt-xr/sdk/face-effects";
|
|
890
|
+
|
|
891
|
+
const faces = useFaceRegionMask(); // lips, mouth, eyes, brows, face_skin — from face landmarks
|
|
892
|
+
const person = usePersonMask(); // bg, person — from portrait segmentation
|
|
893
|
+
<SegmentationOverlay mask={faces} label="lips" color="#c0304f" blendMode="multiply" />;
|
|
894
|
+
<SegmentationOverlay mask={person} label="bg" color="#000" opacity={0.6} blendMode="normal" />;
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**2D versus 3D.** Pick `SegmentationOverlay` for screen-space tints, cut-outs
|
|
898
|
+
and stamps, and for regions without landmarks (background, person, hair,
|
|
899
|
+
beard, ears, the latter three only with an external mask). Pick `LipEffect` /
|
|
900
|
+
`FaceMeshRenderer` when a design must follow
|
|
901
|
+
the lip or face surface under rotation with real shading. The 2D layer samples
|
|
902
|
+
`texture` in screen space and takes its lighting from the camera pixels via
|
|
903
|
+
`blendMode`; it is one full-screen quad and one small texture.
|
|
904
|
+
|
|
905
|
+
**Mask data.** `SemanticMask = { width, height, indices: Uint8Array (row 0 =
|
|
906
|
+
top), labels: string[], space?: "feed" | "source", version }`. Feed any
|
|
907
|
+
model's per-pixel class ids into it with `createSemanticMask` /
|
|
908
|
+
`clearSemanticMask` (face-effects, also tracking), `SEMANTIC_MASK_LABELS` and
|
|
909
|
+
`FACE_REGION_LABELS` name the built-in labels. Mutate in place and bump
|
|
910
|
+
`version`; the overlay uploads lazily per version, and draws nothing while
|
|
911
|
+
`version` is 0. The rasterizer primitives (`fillPolygon`,
|
|
912
|
+
`fillLandmarkPolygon`, `semanticMaskLabelIndex`, `rasterizeFaceRegions`,
|
|
913
|
+
`FACE_REGION_POLYGONS`) are `@vincentt-xr/sdk/low-level`.
|
|
914
|
+
|
|
915
|
+
**Coordinates.** `"feed"` (default) is the session camera texture — what
|
|
916
|
+
trackers see, cover-cropped and webcam-mirrored — and needs no setup; the
|
|
917
|
+
overlay quad matches `VideoBackground`. `"source"` is the raw media frame:
|
|
918
|
+
the overlay cover-fits it from the mask's aspect and the camera texture size
|
|
919
|
+
(`computeMaskUvTransform`); set `mirrored` when the feed is mirrored relative to
|
|
920
|
+
that source (webcam). Masks are nearest-filtered class indices at their own
|
|
921
|
+
resolution; `feather` (mask texels) softens edges.
|
|
922
|
+
|
|
923
|
+
**Props** are `Partial<SegmentationOverlaySettings>` (`label` — up to four,
|
|
924
|
+
comma-separated; `invert`; `color`; `opacity`; `blendMode` `normal` |
|
|
925
|
+
`multiply` | `screen` | `overlay`; `feather`; `mirrored`) plus `mask`,
|
|
926
|
+
`texture` (screen-space image), `material` (node, `THREE.Material`, or factory
|
|
927
|
+
receiving `{ cameraTexture, maskTexture, maskTransform, labelIndices, mask,
|
|
928
|
+
settings }`), `renderOrder`, `visible`. `SegmentationOverlaySettingsPanel`
|
|
929
|
+
(`@vincentt-xr/sdk/debug-ui`) edits the settings live. The internals are
|
|
930
|
+
`@vincentt-xr/sdk/low-level`: the material object API
|
|
931
|
+
(`createSegmentationOverlayMaterial` / `updateSegmentationOverlayMaterial` /
|
|
932
|
+
`useSegmentationOverlayMaterial`), the shaders
|
|
933
|
+
(`SEGMENTATION_OVERLAY_VERTEX_SHADER` / `_FRAGMENT_SHADER`), mask textures
|
|
934
|
+
(`createSemanticMaskTexture` / `uploadSemanticMask` / `useSemanticMaskTexture`)
|
|
935
|
+
and `computeMaskUvTransform`. `usePersonMask` and `SegmentationOverlay` are
|
|
936
|
+
also exported from `@vincentt-xr/sdk/tracking` for person / background work.
|
|
937
|
+
|
|
938
|
+
**Hair.** `useHairMask({ modelUrl?, quality?, active? })` runs MediaPipe's Hair
|
|
939
|
+
Segmenter through `XRModel.SEMANTIC_SEGMENTATION` and returns `{ mask, status,
|
|
940
|
+
error, labels }` with labels `bg` / `hair`; `status` is `loading` → `ready` |
|
|
941
|
+
`error`, and the mask stays at `version` 0 until the first inference. By
|
|
942
|
+
default the model loads from MediaPipe's published file, integrity-pinned in
|
|
943
|
+
the SDK manifest (verified; a republish fails loudly); the same hash is pinned
|
|
944
|
+
under the SDK CDN path `/assets/xr/models/hair_segmenter.tflite` for when the
|
|
945
|
+
file is uploaded there. `useHairMask()` with no options works. An inference
|
|
946
|
+
error is transient and clears on the next good frame.
|
|
947
|
+
`useSemanticSegmentationMask({ preset: "hair" | "multiclass", … })` is the
|
|
948
|
+
generic form (`multiclass` adds `body_skin`, `face_skin`, `clothes`,
|
|
949
|
+
`others`); one preset per session, first acquirer wins. Hair is neither
|
|
950
|
+
landmark geometry (no hair points) nor the portrait segmenter (person as one
|
|
951
|
+
class): use it for hair tints, recolours, highlights and cut-outs as 2D
|
|
952
|
+
effects. Full contract: `docs/api/segmentation-overlay.mdx` → "Hair
|
|
953
|
+
segmentation".
|
|
954
|
+
|
|
955
|
+
**Faces and performance.** `useFaceRegionMask({ faceIndex: "all" | n,
|
|
956
|
+
regions, resolution = 256, maxUpdateFps, holdMs = 250, predictMs = 0 })`
|
|
957
|
+
merges all faces or isolates one; `holdMs` keeps a dropped face's mask for
|
|
958
|
+
that long (small or distant faces make the tracker blink) and `predictMs`
|
|
959
|
+
extrapolates the held landmarks along their last velocity (`FaceTrackMemory`
|
|
960
|
+
is the reusable bridge);
|
|
961
|
+
make one provider and overlay per face for per-face looks. `usePersonMask({
|
|
962
|
+
maxUpdateFps = 15 })` acquires the segmentation model and reads the mask back
|
|
963
|
+
from the GPU each update, so keep its rate low and share it; it covers every
|
|
964
|
+
person in frame, and its mask stays at `version` 0 (overlay draws nothing)
|
|
965
|
+
until the model has loaded. Use `blendMode="normal"` for `bg` / `person`;
|
|
966
|
+
`multiply` is for tinting skin and is nearly invisible on dark backgrounds. Combine labels in
|
|
967
|
+
one overlay instead of stacking quads. Full reference, including the manual
|
|
968
|
+
visual test plan: `docs/api/segmentation-overlay.mdx`.
|
|
969
|
+
|
|
575
970
|
### `<FaceTracker>` — Face tracking (468 landmarks)
|
|
576
971
|
|
|
577
972
|
Detects faces and provides face transform to children via context. Supports
|
|
@@ -871,6 +1266,13 @@ import { FaceTracker, FaceMesh } from "@vincentt-xr/sdk/tracking";
|
|
|
871
1266
|
</FaceTracker>;
|
|
872
1267
|
```
|
|
873
1268
|
|
|
1269
|
+
The composite face shader (camera + canonical texture with blend modes) and the
|
|
1270
|
+
`FaceWarp3DHead` shader end with `#include <colorspace_fragment>`, so the face
|
|
1271
|
+
renders at the same brightness as the camera feed and built-in materials;
|
|
1272
|
+
`FaceWarp3DHead` tags its UV reference texture sRGB for the same reason. Any
|
|
1273
|
+
custom `ShaderMaterial` layered over the feed must include that chunk too (see
|
|
1274
|
+
"Custom shaders" under lip effects).
|
|
1275
|
+
|
|
874
1276
|
### `<HandTracker>` — Hand skeleton (21 joints)
|
|
875
1277
|
|
|
876
1278
|
The hand-skeleton tracker. Use it for 21-joint hand landmarks — anchoring to `hand.indexTip`, `hand.wrist`, etc. It also exposes a crude open-vs-closed grip on `node.gesture` (only `closed_fist` / `open_palm`), but that is a side effect of the skeleton, not a gesture recognizer. **For any named gesture, use `<GestureTracker>`** (next section) — it is the gesture door; `<HandTracker>` is the skeleton door.
|
|
@@ -885,6 +1287,13 @@ Supports multiple detected hands: `hand` selects `"left"` or `"right"` when that
|
|
|
885
1287
|
hand is available; `handIndex` falls back to positional selection. Existing
|
|
886
1288
|
one-hand scenes keep working via the default first-hand selection.
|
|
887
1289
|
|
|
1290
|
+
Low-level `node.coordinates` are `TrackerPoint` values: center-origin, X right,
|
|
1291
|
+
Y up, and positive Z toward the viewer. X/Y are in `[-1, 1]`; Z is
|
|
1292
|
+
tracker-relative depth, not a normalized screen axis. For a bespoke cursor or
|
|
1293
|
+
overlay, call `trackerPointToScreenPixels({ point, viewportSize })` from
|
|
1294
|
+
`@vincentt-xr/sdk`. The SDK already owns the mirror and origin conversion; do
|
|
1295
|
+
not repeat it in app code.
|
|
1296
|
+
|
|
888
1297
|
### `<GestureTracker>` — Named gestures
|
|
889
1298
|
|
|
890
1299
|
The gesture door — the component that recognizes named gestures. Its `node.gesture` is one of these exact strings (copy verbatim — these are the only valid values):
|
|
@@ -920,12 +1329,183 @@ import { BodyTracker, TrackingAnchor } from "@vincentt-xr/sdk/tracking";
|
|
|
920
1329
|
</BodyTracker>;
|
|
921
1330
|
```
|
|
922
1331
|
|
|
1332
|
+
Low-level body landmarks use the same `TrackerPoint` contract as hand and
|
|
1333
|
+
gesture points. `TrackingAnchor` is preferred for attachment; use
|
|
1334
|
+
`trackerPointToScreenPixels` only when rendering a bespoke 2D projection.
|
|
1335
|
+
|
|
1336
|
+
### `<HolisticTracker>` — Face + hands + pose from one inference
|
|
1337
|
+
|
|
1338
|
+
Opt-in, additive: one MediaPipe Holistic inference per frame for a **single
|
|
1339
|
+
person**, shared by every consumer. Use it when a scene needs face, hands and
|
|
1340
|
+
body together; keep the individual trackers when it needs one stream (the
|
|
1341
|
+
holistic model is heavier than any single one, 13.7 MB). Mounting
|
|
1342
|
+
`<FaceTracker>` beside `<HolisticTracker>` runs two models — pick one per scene.
|
|
1343
|
+
|
|
1344
|
+
```tsx
|
|
1345
|
+
import { HolisticTracker, TrackingAnchor } from "@vincentt-xr/sdk/tracking";
|
|
1346
|
+
|
|
1347
|
+
<HolisticTracker holdMs={250} hands="both">
|
|
1348
|
+
<HolisticTracker.Face>
|
|
1349
|
+
<TrackingAnchor target="face.noseTip" smoothing={15}>
|
|
1350
|
+
{/* … */}
|
|
1351
|
+
</TrackingAnchor>
|
|
1352
|
+
</HolisticTracker.Face>
|
|
1353
|
+
<HolisticTracker.Hand hand="right">
|
|
1354
|
+
<TrackingAnchor target="hand.indexTip">{/* … */}</TrackingAnchor>
|
|
1355
|
+
</HolisticTracker.Hand>
|
|
1356
|
+
<HolisticTracker.Body>
|
|
1357
|
+
<TrackingAnchor target="body.leftShoulder">{/* … */}</TrackingAnchor>
|
|
1358
|
+
</HolisticTracker.Body>
|
|
1359
|
+
</HolisticTracker>;
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
- **Sub-trackers** `HolisticTracker.Face` / `.Hand hand="left"|"right"` /
|
|
1363
|
+
`.Body` provide the same tracker context as `FaceTracker` / `HandTracker` /
|
|
1364
|
+
`BodyTracker`: every `face.*`, `hand.*`, `body.*` anchor target (including
|
|
1365
|
+
`body.neck` and `face.custom`) and `smoothing` work unchanged. The group is
|
|
1366
|
+
hidden while its stream is absent; origins are the nose tip, the wrist, the
|
|
1367
|
+
head. The holistic graph has no face transformation matrix, so there is
|
|
1368
|
+
**no head rotation**; use `<FaceTracker>` when children must roll with the
|
|
1369
|
+
head. `FaceMesh`, the face readers, face effects and head binding read
|
|
1370
|
+
`XRModel.FACE_TRACKER`, not holistic.
|
|
1371
|
+
- **Unified result**: `useHolisticTracking()` → `{ node, ready, status,
|
|
1372
|
+
error }`; read `node.getResult()` inside `useFrame` (stable object, mutated
|
|
1373
|
+
in place, `version` bumps every frame). `HolisticResult` = `face`
|
|
1374
|
+
(478 `NormalizedLandmark`s in MediaPipe image space like `FaceTracker`'s
|
|
1375
|
+
`detectionResults`, plus `blendshapes` name → score), `leftHand` /
|
|
1376
|
+
`rightHand` (21 `TrackerPoint`s like `HandTracker`, plus `handedness` and
|
|
1377
|
+
metric `worldLandmarks`), `pose` (33 `TrackerPoint`s like `BodyTracker`,
|
|
1378
|
+
plus `visibility[]` and `worldLandmarks`). Each stream is `null` when
|
|
1379
|
+
absent. Coordinates get no source-dependent mirroring, matching Hand/Body.
|
|
1380
|
+
Hand **labels** are anatomical on every source: holistic assigns hands from
|
|
1381
|
+
the pose, whose left/right follow the image, so the SDK swaps them on the
|
|
1382
|
+
mirrored webcam feed (`leftHand` = the person's left hand, which appears on
|
|
1383
|
+
the viewer's left in the selfie view). `hands="left"` therefore keeps the
|
|
1384
|
+
person's left hand, the same hand `<HandTracker hand="left">` selects. The
|
|
1385
|
+
pose's left/right landmarks (shoulders 11/12, wrists 15/16, …) are swapped
|
|
1386
|
+
the same way, so `pose` and the hand streams always describe the same
|
|
1387
|
+
physical hand and `getHolisticSignals` can pair them; this differs from
|
|
1388
|
+
`BodyTracker`, whose indices follow the image.
|
|
1389
|
+
- **Pre-release asset host**: the default model is MediaPipe's published
|
|
1390
|
+
`holistic_landmarker.task` on `storage.googleapis.com` (SHA-384 pinned), not
|
|
1391
|
+
the SDK CDN, until the CDN copy is published. Pass `modelUrl` to self-host
|
|
1392
|
+
it; the hash for the CDN path is already pinned.
|
|
1393
|
+
- **Confidence**: face/hands `1` while detected, pose = mean visibility.
|
|
1394
|
+
Each stream runs through `FaceTrackMemory` (the one dropout bridge):
|
|
1395
|
+
`holdMs` 250 / `predictMs` 0 by default; while held, `held` is true and
|
|
1396
|
+
`confidence` decays linearly to 0 at `holdMs`. `holdMs={0}` drops at once.
|
|
1397
|
+
- **Delegate**: `delegate` is `"auto"` (default) | `"GPU"` | `"CPU"`.
|
|
1398
|
+
MediaPipe's holistic face-blendshape stage has no WebGL kernel for one of
|
|
1399
|
+
its ops, so with `outputFaceBlendshapes` the GPU graph fails on its first
|
|
1400
|
+
frame; `"auto"` rebuilds the graph on the CPU once (one `console.warn`,
|
|
1401
|
+
status dips to `loading` for the rebuild, no refetch) and
|
|
1402
|
+
`node.getDelegate()` then reports `"CPU"`. CPU inference is several times
|
|
1403
|
+
slower (tens to hundreds of ms per frame on a laptop); when blendshapes
|
|
1404
|
+
are not needed, `outputFaceBlendshapes={false}` keeps the whole graph on
|
|
1405
|
+
the GPU. A fixed `"GPU"` policy reports the failure as `error` instead.
|
|
1406
|
+
- **Options**: model-creation (`modelUrl`, the five `min*Confidence`
|
|
1407
|
+
thresholds, `outputFaceBlendshapes`, `delegate`) come from the first
|
|
1408
|
+
mounted consumer and re-acquire when changed; `targetFps` overrides `perf.targetFps`;
|
|
1409
|
+
`hands` (`both` | `left` | `right` | `none`), `analysisMaxDimension` (768),
|
|
1410
|
+
`holdMs`, `predictMs` apply live (last writer wins); `active` pauses;
|
|
1411
|
+
`onStatusChange(status, error)` reports `loading` → `ready` / `error` (a
|
|
1412
|
+
transient inference error recovers on the next good frame). The default
|
|
1413
|
+
model is MediaPipe's published `holistic_landmarker.task`, SHA-384 pinned.
|
|
1414
|
+
- **Combined signals**: `getHolisticSignals(result)` →
|
|
1415
|
+
`HolisticSignals` with presence per stream, `smile` / `mouthOpen`
|
|
1416
|
+
blendshape scores, `leftHandRaised` / `rightHandRaised` (pose wrist above
|
|
1417
|
+
its shoulder), `leftPalmOpen` / `rightPalmOpen` (three of four fingers
|
|
1418
|
+
extended) and `greeting` (raised open hand while smiling; right wins).
|
|
1419
|
+
`isHandRaised` / `isPalmOpen` are exported alone. Pass an `out` object to
|
|
1420
|
+
avoid per-frame allocation.
|
|
1421
|
+
- **Low-level**: `XRModelNodeHolisticTracking` (`@vincentt-xr/sdk/low-level`)
|
|
1422
|
+
= `{ getResult, getRawResult, getStatus, getError, getDelegate, setOptions }`. Debug
|
|
1423
|
+
panel: `HolisticTrackerSettingsPanel` (`@vincentt-xr/sdk/debug-ui`) with
|
|
1424
|
+
`resolveHolisticTrackerSettings` / `toHolisticTrackerProps`. Full contract
|
|
1425
|
+
and manual test plan: `docs/api/holistic-tracker.mdx`; example:
|
|
1426
|
+
`/holistic-tracking`.
|
|
1427
|
+
|
|
923
1428
|
### `<Segmentation type="portrait">` — Background segmentation (declarative)
|
|
924
1429
|
|
|
925
1430
|
Registers the portrait segmentation pipeline. The resulting mask feeds into `<VideoBackground segmentationMask={...} />` for background replacement. Mount it alongside a ref or state that holds the mask texture.
|
|
926
1431
|
|
|
1432
|
+
For a **tinted or textured person / background layer** rather than a background
|
|
1433
|
+
swap, use `usePersonMask()` with `<SegmentationOverlay mask={…} label="bg" />`
|
|
1434
|
+
(or `label="person"`). For **hair**, use `useHairMask()` with `label="hair"`
|
|
1435
|
+
(MediaPipe Hair Segmenter via `XRModel.SEMANTIC_SEGMENTATION`). For **background
|
|
1436
|
+
replacement** (person kept, background swapped for any media), use
|
|
1437
|
+
`<PersonCutout>` over a full-screen `ScreenImage`; see "Camera background". Both are exported from this door as well as from
|
|
1438
|
+
`@vincentt-xr/sdk/face-effects`; `usePersonMask` acquires the segmentation model
|
|
1439
|
+
itself, so no `<Segmentation>` is needed for it. See `<SegmentationOverlay>` in
|
|
1440
|
+
the face-effects section for the full contract.
|
|
1441
|
+
|
|
927
1442
|
---
|
|
928
1443
|
|
|
1444
|
+
## Multi-subject policy — indices, counts, selection, association
|
|
1445
|
+
|
|
1446
|
+
**An index is a result-array position, not an identity**: `faceIndex` /
|
|
1447
|
+
`handIndex` / `getFace(i)` read position `i` of the current frame; the order
|
|
1448
|
+
can change between frames and nothing promises the same person at the same
|
|
1449
|
+
index later. The SDK never recognises people; the only continuity is spatial
|
|
1450
|
+
(`sticky`). Single-subject defaults (index 0) are unchanged.
|
|
1451
|
+
|
|
1452
|
+
Max subjects (`XR_MAX_SUBJECTS`): face 10 (`numFaces`, default 1, first
|
|
1453
|
+
acquirer of the shared model wins), hand 2 (fixed, `handedness` labels them),
|
|
1454
|
+
gesture 1, body 1, holistic 1 (single person).
|
|
1455
|
+
|
|
1456
|
+
```tsx
|
|
1457
|
+
import {
|
|
1458
|
+
useFaceInfo,
|
|
1459
|
+
selectSubject,
|
|
1460
|
+
faceCandidates,
|
|
1461
|
+
jointCandidates,
|
|
1462
|
+
associateSubjects,
|
|
1463
|
+
} from "@vincentt-xr/sdk/tracking";
|
|
1464
|
+
|
|
1465
|
+
const face = useFaceInfo({ numFaces: 4, subject: { selection: "largest" } }); // or "nearest-center" | "leftmost" | "rightmost" | "index"
|
|
1466
|
+
const same = useFaceInfo({ numFaces: 4, subject: { selection: "sticky", stickyRadius: 0.2 } }); // follows the previous pick by proximity; fallback "largest"
|
|
1467
|
+
const result = selectSubject(faceCandidates(results.faceLandmarks), policy, previousMemory); // pure: { index | null, reason, memory }; carry result.memory to the next frame
|
|
1468
|
+
const pairs = associateSubjects(faceCandidates(faces), jointCandidates(bodies), 0.35); // nearest-centre pairs, each subject once
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
Loss: the tracker hold (`holdMs` 250) replays the last result, then drops it;
|
|
1472
|
+
`sticky` keeps its memory through a miss and re-selects a subject that
|
|
1473
|
+
reappears nearby, otherwise the fallback decides and the memory moves with it.
|
|
1474
|
+
Reappearance never restores an index. Full contract: `docs/api/multi-subject.mdx`;
|
|
1475
|
+
example: `/multi-subject`.
|
|
1476
|
+
|
|
1477
|
+
## Tracker dropouts — the one bridge to use
|
|
1478
|
+
|
|
1479
|
+
The face landmarker loses and re-finds a face for a frame or two at a time
|
|
1480
|
+
when the face is small, distant, fast, or turned hard. Raw detection results
|
|
1481
|
+
show those gaps as `faceLandmarks` momentarily empty. **Do not write your own
|
|
1482
|
+
hold, smoothing, or interpolation for this in app code.** The SDK has one
|
|
1483
|
+
mechanism, used by every face surface, and it is the source of truth:
|
|
1484
|
+
|
|
1485
|
+
- `FaceTrackMemory` (`@vincentt-xr/sdk/face-effects`) remembers each face's
|
|
1486
|
+
last landmarks and a smoothed velocity. A missing face is replayed for
|
|
1487
|
+
`holdMs`, extrapolated along that velocity for the first `predictMs`, then
|
|
1488
|
+
held still, then dropped. Depth (`z`) rides along when present.
|
|
1489
|
+
- Rendered surfaces use it with `holdMs: 250` and `predictMs: 0` by default:
|
|
1490
|
+
`CanonicalFaceMeshRenderer` (so `FaceMeshRenderer` and `LipEffect` /
|
|
1491
|
+
`LipEffects`) and `useFaceRegionMask` for 2D overlays. The face readers
|
|
1492
|
+
(`useFaceInfo` / `useFaceInfos` / `useFaceDetection`) apply the same
|
|
1493
|
+
250 ms hold to presence and lifecycle events through their own `holdMs`;
|
|
1494
|
+
they report, they do not extrapolate. Set `holdMs: 0` only when immediate
|
|
1495
|
+
disappearance is the point. Keep `predictMs` at or below 300: it
|
|
1496
|
+
extrapolates linearly and will overshoot a sharp reversal for up to that
|
|
1497
|
+
window.
|
|
1498
|
+
- Measured on the SDK's "Person Fullbody" clip, hold 250 with predict 150 cut
|
|
1499
|
+
empty frames from 23 % to 2 %; the remaining gaps are longer than the hold
|
|
1500
|
+
and are correctly dropped rather than shown stale.
|
|
1501
|
+
- The dropouts themselves are a tracking-input problem. When they are
|
|
1502
|
+
frequent, raise the analysed resolution with `perf.camRes` before reaching
|
|
1503
|
+
for a longer hold.
|
|
1504
|
+
|
|
1505
|
+
Custom consumers of raw landmarks (a bespoke mesh, a custom material factory
|
|
1506
|
+
that reads `detectionResults`) should run them through `FaceTrackMemory` with
|
|
1507
|
+
the same defaults so they stay in step with the SDK's own surfaces.
|
|
1508
|
+
|
|
929
1509
|
## Tracker performance & lifecycle
|
|
930
1510
|
|
|
931
1511
|
ML tracker models are the heaviest thing in an XR app — each one runs a MediaPipe pipeline on the camera feed. Two rules keep apps from overheating the device and from a scene-transition gesture bug.
|
|
@@ -948,7 +1528,7 @@ Scope a tracker to a single scene only when just that scene uses it (e.g. a `<Fa
|
|
|
948
1528
|
|
|
949
1529
|
### First load: each tracker you mount is a download
|
|
950
1530
|
|
|
951
|
-
Before a tracker reports anything, the viewer's phone downloads its model and the shared MediaPipe engine (`vision_wasm_internal.wasm`, served gzip-compressed, ~2.9 MB). The SDK fetches the two in parallel, and trackers mounted together share one engine download. Model sizes: face 3.8 MB, hand 7.8 MB, gesture 8.4 MB, body 5.8 MB. All are cached by the browser for a year, so a returning viewer downloads nothing.
|
|
1531
|
+
Before a tracker reports anything, the viewer's phone downloads its model and the shared MediaPipe engine (`vision_wasm_internal.wasm`, served gzip-compressed, ~2.9 MB). The SDK fetches the two in parallel, and trackers mounted together share one engine download. Model sizes: face 3.8 MB, hand 7.8 MB, gesture 8.4 MB, body 5.8 MB, holistic 13.7 MB (face + hands + pose in one). All are cached by the browser for a year, so a returning viewer downloads nothing.
|
|
952
1532
|
|
|
953
1533
|
So every extra tracker costs a first-time viewer its full model on mobile data. Mount only the trackers the experience actually reads; a face-only effect should not also mount `<HandTracker>`.
|
|
954
1534
|
|
|
@@ -973,7 +1553,45 @@ Session lifecycle preserves that ownership state: `session.stop()` pauses every
|
|
|
973
1553
|
tracker without releasing its MediaPipe resources, and the next `session.start()`
|
|
974
1554
|
resumes only models that still have at least one active owner. Static-image
|
|
975
1555
|
detection pulses use the same suspension path, so source switches do not
|
|
976
|
-
accidentally reactivate an `active={false}` tracker.
|
|
1556
|
+
accidentally reactivate an `active={false}` tracker. A tracker whose last
|
|
1557
|
+
owner unmounts while the session runs is disposed, and the instance a later
|
|
1558
|
+
mount acquires starts its own inference loop (the run guard is per instance),
|
|
1559
|
+
so remounting a sole-owner tracker mid-session is safe.
|
|
1560
|
+
|
|
1561
|
+
A failed `session.setMediaSource()` (webcam denied, no device, a stream that
|
|
1562
|
+
never produces frames) rejects and leaves the session where it was: an idle
|
|
1563
|
+
session stays idle, a running session clears its swap gate and keeps rendering,
|
|
1564
|
+
and the next `setMediaSource()` binds normally with no stop/start in between.
|
|
1565
|
+
Each bind owns a generation; a webcam denial that lands after a later bind
|
|
1566
|
+
already succeeded is dropped instead of overwriting the healthy session's
|
|
1567
|
+
`error`. Callers therefore recover from a blocked camera by selecting another
|
|
1568
|
+
source once.
|
|
1569
|
+
|
|
1570
|
+
### Adaptive quality — budget + tiers (on by default)
|
|
1571
|
+
|
|
1572
|
+
`<XRProvider>` mounts an `XRQualityController` unless `quality={false}`. It
|
|
1573
|
+
samples frame time, tracker inference, memory and device signals every
|
|
1574
|
+
second and steps between declared tiers: `targetFps` and `analysisScale`
|
|
1575
|
+
through the session perf config, `renderScale` through `<XRScene>`'s pixel
|
|
1576
|
+
ratio, `camRes` only when a tier sets `rebindCamera: true`.
|
|
1577
|
+
|
|
1578
|
+
```tsx
|
|
1579
|
+
import { XRProvider, useXRQuality, DEFAULT_QUALITY_TIERS, DEFAULT_PERFORMANCE_BUDGET } from "@vincentt-xr/sdk";
|
|
1580
|
+
|
|
1581
|
+
<XRProvider quality={{ tiers: DEFAULT_QUALITY_TIERS, budget: { maxFrameMs: 33 }, minTier: "medium" }}>…</XRProvider>
|
|
1582
|
+
<XRProvider quality={{ adaptive: false, initialTier: "medium" }}>…</XRProvider> // pinned, still observable
|
|
1583
|
+
<XRProvider quality={false}>…</XRProvider> // no controller
|
|
1584
|
+
|
|
1585
|
+
const { tier, index, tiers, decision, history, signals, locked, setTier, lock, unlock, reset } = useXRQuality();
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
- Defaults: tiers high / medium / low (`targetFps` 30 / 24 / 15, `renderScale` 1 / 0.8 / 0.6, `analysisScale` 1 / 0.85 / 0.7); budget `maxFrameMs` 1000/24, `maxInferenceMs` 24, `sampleMs` 1000, `downgradeAfter` 2, `upgradeAfter` 6, `upgradeHeadroom` 0.75, `cooldownMs` 4000. All plain JSON; `resolveQualityConfig` validates partials.
|
|
1589
|
+
- Bounded (`minTier` / `maxTier`), hysteretic (streaks), rate-limited (cooldown), reversible (steps back up with headroom). Judges thermal → memory → inference → frame time. At the floor and still over budget it sets `signals.thermal = "suspected"` and changes nothing. Hidden tabs are never judged. A per-tracker `targetFps` prop still beats the tier; the app's `perf` is the base and is restored when the controller unmounts.
|
|
1590
|
+
- Every change is a decision `{ index, tier, reason, at }` (`init`, `frame-time`, `inference`, `memory`, `thermal`, `recovered`, `manual`, `locked`, `reset`) kept in `history`; the state is in the session store (`useXRQualityState()`, low-level) and in `useXRDiagnostics().quality` / the diagnostics panel.
|
|
1591
|
+
- `perf.analysisScale` (0.25..1) is the new knob the tiers drive; set it by hand only with `quality={false}`.
|
|
1592
|
+
- Pure seam (low-level): `stepQualityController`, `judgeSample`, `createQualityState`, `setQualityTier`, `lockQuality`, `resetQuality`, `resolveInitialTierIndex`, `readDeviceSignals`.
|
|
1593
|
+
|
|
1594
|
+
Full contract: `docs/api/adaptive-quality.mdx`; example: `/adaptive-quality`.
|
|
977
1595
|
|
|
978
1596
|
### `perf` — cap inference rate + camera resolution
|
|
979
1597
|
|
|
@@ -1005,6 +1623,320 @@ against hashes pinned in the package. A missing hash for
|
|
|
1005
1623
|
`https://cdn.vincentt.studio` fails closed; custom consumer-owned asset URLs
|
|
1006
1624
|
remain best-effort and warn when no pinned hash exists.
|
|
1007
1625
|
|
|
1626
|
+
### Capability detection — decide before the session starts
|
|
1627
|
+
|
|
1628
|
+
Run the check on the landing screen (no `<XRProvider>` needed). It never opens
|
|
1629
|
+
the camera and never downloads a model body: camera = secure context +
|
|
1630
|
+
`getUserMedia` + video-input count + Permissions API state; rendering = WebGL
|
|
1631
|
+
version, OffscreenCanvas, WebAssembly (WebGPU is reported, not required);
|
|
1632
|
+
models = one `HEAD` request per tracker you list.
|
|
1633
|
+
|
|
1634
|
+
```tsx
|
|
1635
|
+
import { useXRCapabilities, resolveTrackerSupport, XRModel } from "@vincentt-xr/sdk";
|
|
1636
|
+
import type { XRCapabilities, XRFallbackReason } from "@vincentt-xr/sdk";
|
|
1637
|
+
|
|
1638
|
+
const { capabilities, status, refresh } = useXRCapabilities({
|
|
1639
|
+
models: [XRModel.FACE_TRACKER], // optional asset probes
|
|
1640
|
+
});
|
|
1641
|
+
if (status === "ready" && capabilities) {
|
|
1642
|
+
const face = resolveTrackerSupport(capabilities, XRModel.FACE_TRACKER);
|
|
1643
|
+
if (!face.supported) renderFallback(face.reason); // { code, message, recoverable }
|
|
1644
|
+
if (face.needsPermission) renderCameraPrimer(); // first getUserMedia will prompt
|
|
1645
|
+
}
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
- `capabilities.camera.status`: `"supported" | "permission-prompt" | "permission-denied" | "unsupported" | "unknown"`. Denied and unsupported are distinct; `reason.recoverable` says whether the viewer can fix it (grant permission, close the other tab).
|
|
1649
|
+
- `capabilities.rendering.status` + `webglVersion` (0/1/2), `offscreenCanvas`, `wasm`, `webgpu`.
|
|
1650
|
+
- `capabilities.models[model]`: `status`, `reason`, `url`, `bytes` (`unknown` for models not listed). The `SEMANTIC_SEGMENTATION` probe covers the default `selfie_multiclass_256x256` preset only; the hair preset loads a different file.
|
|
1651
|
+
- `capabilities.canRunTracking`: rendering supported and camera not unsupported.
|
|
1652
|
+
- `detectXRCapabilities(options)` is the same check as a promise. `refresh()` re-runs it (after the viewer granted permission).
|
|
1653
|
+
|
|
1654
|
+
**Fallback vocabulary.** Every degraded state is an `XRFallbackReason =
|
|
1655
|
+
{ code, message, recoverable }`. Branch on `code`:
|
|
1656
|
+
`camera-permission-denied`, `camera-in-use`, `no-camera`,
|
|
1657
|
+
`camera-insecure-context`, `camera-unsupported`, `camera-lost`,
|
|
1658
|
+
`webgl-unavailable`, `offscreen-canvas-unavailable`, `wasm-unavailable`,
|
|
1659
|
+
`model-unreachable`, `model-integrity`, `model-init-failed`, `unknown`.
|
|
1660
|
+
`message` is display prose and may be reworded. `resolveTrackerSupport`
|
|
1661
|
+
returns the most fundamental blocker first (rendering → camera → model).
|
|
1662
|
+
|
|
1663
|
+
Inside a session, a component that does not own a tracker reads an actionable
|
|
1664
|
+
status without touching the model:
|
|
1665
|
+
|
|
1666
|
+
```tsx
|
|
1667
|
+
import { useXRTrackerStatus } from "@vincentt-xr/sdk/low-level";
|
|
1668
|
+
|
|
1669
|
+
const { status, fallback } = useXRTrackerStatus(XRModel.FACE_TRACKER);
|
|
1670
|
+
// status: "unavailable" (nobody acquired it) | "loading" | "ready" |
|
|
1671
|
+
// "blocked" (camera failed; fallback = the session error) |
|
|
1672
|
+
// "error" (the model's initialize failed; fallback = its cause)
|
|
1673
|
+
```
|
|
1674
|
+
|
|
1675
|
+
`useXRModel(...)` (the owner) also returns `fallback` next to `error`;
|
|
1676
|
+
`useXRModelErrors()` lists every model whose initialize failed.
|
|
1677
|
+
|
|
1678
|
+
### Camera permission & privacy — gate, request, retry, recover
|
|
1679
|
+
|
|
1680
|
+
Six creator-facing states: `unknown` (never asked; the next request prompts),
|
|
1681
|
+
`requesting`, `granted`, `denied` (declined or dismissed; asking again
|
|
1682
|
+
prompts), `blocked` (persistently denied in the site settings; asking again
|
|
1683
|
+
fails silently), `unavailable` (no camera / insecure context / unsupported /
|
|
1684
|
+
held by another app, see `reason`). `denied` vs `blocked` is decided by
|
|
1685
|
+
reading the Permissions API after the failed request.
|
|
1686
|
+
|
|
1687
|
+
```tsx
|
|
1688
|
+
import { CameraPermissionGate, useCameraPermission } from "@vincentt-xr/sdk";
|
|
1689
|
+
|
|
1690
|
+
// Gate: children mount only while access is granted; no provider needed.
|
|
1691
|
+
<CameraPermissionGate
|
|
1692
|
+
fallback={({ state, reason, canPrompt, request }) => (
|
|
1693
|
+
<Primer state={state} reason={reason} onAllow={canPrompt ? () => request() : undefined} />
|
|
1694
|
+
)}
|
|
1695
|
+
>
|
|
1696
|
+
<XRProvider>…</XRProvider>
|
|
1697
|
+
</CameraPermissionGate>;
|
|
1698
|
+
|
|
1699
|
+
// Hook: query on mount (no prompt), prompt from a button.
|
|
1700
|
+
const { state, reason, canPrompt, usable, request, refresh } = useCameraPermission();
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
- `request()` must run from a user gesture; the probe stream is stopped at once (indicator off, nothing captured). `auto` prompts on mount. Both follow site-setting changes live.
|
|
1704
|
+
- The built-in fallback is accessible (`aria-live` status, a real button, disabled + explained when `blocked`); keep those in a custom one.
|
|
1705
|
+
- Inside a session, `useXRSessionCameraPermission()` (`@vincentt-xr/sdk/low-level`) reports the session's own state (`requesting` during its webcam bind, `granted`, the classified failure, `blocked` / `unavailable` after a revoke). Stream/canvas sources leave it `unknown`.
|
|
1706
|
+
- Retry / recover without remounting: `session.setMediaSource({ source: XRMediaSource.WEBCAM })` then `session.start()`; a failed bind leaves the session where it was.
|
|
1707
|
+
- Privacy: `session.stop()` stops the tracks (indicator off); a revoke or lost camera does so automatically; no model runs before `start()`, so no frame is processed before explicit access; frames never leave the device.
|
|
1708
|
+
- Plain helpers (core): `queryCameraPermission`, `requestCameraAccess({ keepStream, constraints })`, `subscribeCameraPermission`, `canPromptForCamera`, `CAMERA_PERMISSION_MESSAGES`. `classifyCameraFailure(reason, permissionState)` + `mapDomExceptionToReason(err)` are `@vincentt-xr/sdk/low-level` (they take an `XRErrorReason`).
|
|
1709
|
+
- Camera choice: see "Cross-camera & orientation" below.
|
|
1710
|
+
|
|
1711
|
+
### Cross-camera & orientation — front / rear, devices, mirror policy
|
|
1712
|
+
|
|
1713
|
+
```tsx
|
|
1714
|
+
import { XRMediaSource, listCameras, findCameraByFacing } from "@vincentt-xr/sdk";
|
|
1715
|
+
import { useXRCameraFacing, useXRMirrored, useXROrientation } from "@vincentt-xr/sdk/low-level";
|
|
1716
|
+
|
|
1717
|
+
await session.setMediaSource({ source: XRMediaSource.WEBCAM, facingMode: "environment" }); // or { deviceId }
|
|
1718
|
+
const facing = await session.switchCamera({ facingMode: "environment" }); // live switch; starts a bound/stopped session
|
|
1719
|
+
const cameras = await listCameras(); // [{ deviceId, label, facing }] — labels need permission
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
- `facingMode` (`"user"` default, `"environment"`) is a soft hint; `deviceId` is exact and wins. `cameraFacing` / `useXRCameraFacing()` report what the track says (a one-camera laptop answers `environment` with `user`). `useXRCameraDeviceId()` reports the device.
|
|
1723
|
+
- Same request = no-op; a different facing / device / mirror policy releases the old tracks once and rebinds under the same source canvas (models keep their input). `running` / `paused` are preserved.
|
|
1724
|
+
- **Mirror policy is keyed on the source kind**, and the kind follows the _effective_ facing the track reports: front camera `"webcam"` mirrored (even when `environment` was requested on a one-camera device); rear camera `"rear-camera"`, images, videos straight; `mirrored: boolean` on the bind pins it. Preview and tracking share the feed, so positions never need flipping; `useXRMirrored()` (= `isMirroredSourceKind(sourceKind)`) is for direction semantics only.
|
|
1725
|
+
- Orientation: `useXROrientation()` → `{ type: "portrait" | "landscape", angle }`; on change the session re-reads the video size so `useXRFeedCrop()` and landmark-anchored content re-align at once. `calibration.orientation` is only for raw sideways captures the browser does not rotate.
|
|
1726
|
+
- Camera-switch lifecycle tests: `lib/hooks/useXRSession/lifecycle.test.ts`. Full contract and phone checklist: `docs/api/cross-camera.mdx`; example: `/camera-switching`.
|
|
1727
|
+
|
|
1728
|
+
Full contract: `docs/api/camera-permission.mdx`; example: `/camera-permission`.
|
|
1729
|
+
|
|
1730
|
+
### Persistent tracking configuration — `useXRTrackingConfig`
|
|
1731
|
+
|
|
1732
|
+
One serializable, validated, versioned document (`XRTrackingConfig`,
|
|
1733
|
+
`version: 1`) for tracker selection and tuning, projected onto the provider,
|
|
1734
|
+
the tracker components, the camera request and model options. Runtime
|
|
1735
|
+
resources (streams, textures, models, session / permission / quality state,
|
|
1736
|
+
calibration measurements) are excluded by construction.
|
|
1737
|
+
|
|
1738
|
+
```tsx
|
|
1739
|
+
import {
|
|
1740
|
+
XRProvider,
|
|
1741
|
+
useXRTrackingConfig,
|
|
1742
|
+
resolveTrackingConfig,
|
|
1743
|
+
parseTrackingConfig,
|
|
1744
|
+
} from "@vincentt-xr/sdk";
|
|
1745
|
+
import { FaceTracker } from "@vincentt-xr/sdk/tracking";
|
|
1746
|
+
|
|
1747
|
+
const tracking = useXRTrackingConfig({
|
|
1748
|
+
storageKey: "myapp.tracking",
|
|
1749
|
+
initial: { trackers: { hand: { enabled: true } } },
|
|
1750
|
+
}); // ABOVE the provider
|
|
1751
|
+
if (!tracking.hydrated) return null; // wait for the persisted document: a provider re-init would restart the camera
|
|
1752
|
+
<XRProvider {...tracking.providerProps}>…</XRProvider>; // perf, lifecycle, quality
|
|
1753
|
+
const face = tracking.trackerProps("face"); // { numFaces, targetFps?, holdMs, predictMs, active }
|
|
1754
|
+
{
|
|
1755
|
+
face.active && <FaceTracker numFaces={face.numFaces} targetFps={face.targetFps} />;
|
|
1756
|
+
}
|
|
1757
|
+
await session.switchCamera(tracking.cameraRequest); // { facingMode, deviceId?, mirrored? }
|
|
1758
|
+
tracking.update({ perf: { targetFps: 20 } });
|
|
1759
|
+
tracking.reset();
|
|
1760
|
+
tracking.importJson(text);
|
|
1761
|
+
tracking.json;
|
|
1762
|
+
tracking.warnings;
|
|
1763
|
+
```
|
|
1764
|
+
|
|
1765
|
+
- Sections: `trackers.{face,hand,gesture,body,holistic,segmentation,semanticSegmentation}` (`enabled`, `targetFps | null` = inherit, face `numFaces 1..10 / holdMs / predictMs / smoothing`, holistic `hands / holdMs / predictMs / analysisMaxDimension / outputFaceBlendshapes / confidence / delegate`, segmentation `quality`, semantic `preset / quality`), `perf { targetFps, camRes, cameraZoom, analysisScale }`, `lifecycle { pauseWhenHidden }`, `quality` (adaptive partial config or `false`), `camera { facingMode, deviceId, mirrored | "auto" }`, `fallbacks { onUnsupported: "block" | "degrade", delegate }`, `assets { holisticModelUrl, semanticSegmentationModelUrl }`, `updatedAt`.
|
|
1766
|
+
- `resolveTrackingConfig(input, base?)` fills from `DEFAULT_TRACKING_CONFIG` (face only, perf 24 fps / 720 / 1.2, adaptive quality on, front camera, degrade), clamps and type-checks every leaf, returns `warnings` instead of throwing; `mergeTrackingConfig` patches; `serializeTrackingConfig` / `parseTrackingConfig` round-trip, drop unknown fields and migrate older `version`s onto the defaults (`migrated: true` + warning; the hook re-saves). `loadTrackingConfig` / `saveTrackingConfig` / `clearTrackingConfig` (`localStorage`, key `vincentt.xr.tracking-config`, injectable storage).
|
|
1767
|
+
- Projections: `toProviderProps`, `toTrackerProps(config, kind)`, `toCameraRequest`, `toModelOptions(config, model)`, `enabledModels(config)` (probe these with `useXRCapabilities`).
|
|
1768
|
+
|
|
1769
|
+
Full contract: `docs/api/tracking-config.mdx`; example: `/tracking-config`.
|
|
1770
|
+
|
|
1771
|
+
### Product placement AR — `<ProductViewer>` + `useProductAr`
|
|
1772
|
+
|
|
1773
|
+
"View in your space" goes through the device's native path (WebXR on Android
|
|
1774
|
+
Chrome, Scene Viewer on Android, Quick Look on iOS from a USDZ); the SDK's
|
|
1775
|
+
camera session has no world tracking. The SDK wraps Google's
|
|
1776
|
+
`<model-viewer>` (`@google/model-viewer`, optional peer dependency, lazy-loaded
|
|
1777
|
+
on first render, bundles its own three.js ≈ 1 MB — keep it on product routes).
|
|
1778
|
+
|
|
1779
|
+
```tsx
|
|
1780
|
+
import { ProductViewer, useProductAr } from "@vincentt-xr/sdk/product";
|
|
1781
|
+
import { useXRCapabilities } from "@vincentt-xr/sdk";
|
|
1782
|
+
|
|
1783
|
+
const ar = useProductAr(); // { status, supportedModes, canActivate, reason, loading, error, bind, activate }
|
|
1784
|
+
<ProductViewer
|
|
1785
|
+
src="/hat.glb"
|
|
1786
|
+
iosSrc="/hat.usdz"
|
|
1787
|
+
poster="/hat.webp"
|
|
1788
|
+
alt="Hat"
|
|
1789
|
+
placement="floor"
|
|
1790
|
+
scale="auto"
|
|
1791
|
+
onReady={ar.bind}
|
|
1792
|
+
/>;
|
|
1793
|
+
{
|
|
1794
|
+
ar.supportedModes.length > 0 && (
|
|
1795
|
+
<button onClick={() => ar.activate()} disabled={!ar.canActivate}>
|
|
1796
|
+
View in your space
|
|
1797
|
+
</button>
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
useXRCapabilities().capabilities?.placement; // { webxr, sceneViewer, quickLook, any, reason } — offer AR only when `any`
|
|
1801
|
+
```
|
|
1802
|
+
|
|
1803
|
+
- Props map onto model-viewer attributes: `src`, `iosSrc` (USDZ, iOS), `poster`, `alt`, `ar`, `arModes` (preference order, default webxr → scene-viewer → quick-look), `placement` (`floor` | `wall`), `scale` (`auto` | `fixed` = true size), `cameraControls`, `autoRotate`, `loading`, `environmentImage`, `shadowIntensity`, `exposure`, `attributes` (pass-through); events `onLoad`, `onError`, `onArStatus`, `onReady(element)`; children are slotted (`slot="ar-button"`, hotspots).
|
|
1804
|
+
- `activate()` must run from a user gesture. `status`: `not-presenting` → `session-started` → `object-placed` (model-viewer's `ar-status`), `failed`, or `unavailable` (no supported mode; `reason.code` `placement-unsupported`).
|
|
1805
|
+
- **Camera handoff (WebXR only)**: WebXR runs in the page and needs the camera the tracking session holds; Scene Viewer and Quick Look open a native activity and never touch it. Under an `<XRProvider>` with a running / paused webcam session, `activate()` stops the session first when the mode about to be used is `webxr` and restores the same facing and lifecycle (a paused session comes back paused) when AR ends (`handoffCamera: false` opts out; `releaseSessionForAr(session, mode)` is the pure form). Stream / canvas sources are untouched. Keep `arModes` on the viewer and `modes` on the hook identical (both default to `DEFAULT_PRODUCT_AR_MODES`).
|
|
1806
|
+
- Assets: GLB for all platforms (small, Draco / KTX2), USDZ for Quick Look produced at publishing time (never at runtime), both on the CDN.
|
|
1807
|
+
|
|
1808
|
+
Full contract: `docs/api/product-ar.mdx`; example: `/product-ar`.
|
|
1809
|
+
|
|
1810
|
+
**Variants, hotspots, dimensions** (same door):
|
|
1811
|
+
|
|
1812
|
+
```tsx
|
|
1813
|
+
import { ProductHotspot, ProductDimensions, useProductVariants, useProductHotspots, useProductDimensions } from "@vincentt-xr/sdk/product";
|
|
1814
|
+
import type { ProductHotspotData, ProductViewerElement } from "@vincentt-xr/sdk/product";
|
|
1815
|
+
|
|
1816
|
+
const variants = useProductVariants(el); // KHR_materials_variants: { variants, variant, setVariant } — materials only, carry into WebXR / Scene Viewer; Quick Look shows the USDZ as exported
|
|
1817
|
+
const dims = useProductDimensions(el); // { size: [w, h, d], center } metres, null until load
|
|
1818
|
+
const hotspots = useProductHotspots(el, { initial, onChange }); // { hotspots, placeFromPointer(clientX, clientY, label), add, update, remove, clear, json, importJson }
|
|
1819
|
+
<ProductViewer scale="fixed" onReady={(e) => setEl(e)}>
|
|
1820
|
+
{hotspots.hotspots.map((h) => <ProductHotspot key={h.id} hotspot={h} />)} // { id, position: [x,y,z], normal: [x,y,z], label?, description? } in model metres
|
|
1821
|
+
<ProductDimensions dimensions={dims} unit="metric" /> // W / H / D labels on the bounding box; pair with scale="fixed"
|
|
1822
|
+
</ProductViewer>
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
Pure helpers: `parseHotspots` / `serializeHotspots` (validated authoring data), `hotspotAttributes`, `nextHotspotId`, `dimensionLines`, `formatLength`. Debug UI: `ProductHotspotPanel` (`@vincentt-xr/sdk/debug-ui`) for click-to-place authoring and JSON export. Full contract: `docs/api/product-details.mdx`; example: `/product-details`.
|
|
1826
|
+
|
|
1827
|
+
### Device compatibility — matrix, smoke, evidence
|
|
1828
|
+
|
|
1829
|
+
Supported: iOS WebKit 16.4+, Android Chrome 110+, desktop Chrome / Edge /
|
|
1830
|
+
Safari; Firefox best effort; insecure contexts and no-WebGL unsupported.
|
|
1831
|
+
Platform differences the SDK already absorbs: no `video.captureStream` on
|
|
1832
|
+
iOS (feed the camera, not a hosted clip), Permissions API camera descriptor
|
|
1833
|
+
missing on Firefox (`unknown` until the first prompt), `deviceMemory` only on
|
|
1834
|
+
Chromium. Full table: `docs/compatibility.mdx`.
|
|
1835
|
+
|
|
1836
|
+
```tsx
|
|
1837
|
+
import { XRModel } from "@vincentt-xr/sdk";
|
|
1838
|
+
import {
|
|
1839
|
+
runCompatChecks,
|
|
1840
|
+
useXRContext,
|
|
1841
|
+
COMPAT_SCENARIOS,
|
|
1842
|
+
MANUAL_COMPAT_SCENARIOS,
|
|
1843
|
+
} from "@vincentt-xr/sdk/low-level";
|
|
1844
|
+
|
|
1845
|
+
const { session } = useXRContext();
|
|
1846
|
+
const report = await runCompatChecks(session, {
|
|
1847
|
+
models: [XRModel.FACE_TRACKER],
|
|
1848
|
+
label: "iPhone 13",
|
|
1849
|
+
}); // stops the session afterwards unless keepSessionRunning
|
|
1850
|
+
report.results; // [{ id, status: "pass" | "fail" | "skip", cause: "capability" | "model" | "lifecycle" | "performance" | null, code, detail, durationMs }]
|
|
1851
|
+
report.summary; // { pass, fail, skip, causes }
|
|
1852
|
+
```
|
|
1853
|
+
|
|
1854
|
+
- Automated scenarios: `capabilities`, `session-start`, `model-ready`, `model-detects`, `pause-resume`, `stop-start`, `performance`. It opens the webcam (or `stream`), acquires and releases the models itself, never throws.
|
|
1855
|
+
- Manual scenarios (`MANUAL_COMPAT_SCENARIOS`): permission prompt / blocked recovery, rear camera, rotation, background-return, camera interrupted, sustained use, low-end device. `/compat` runs both and exports the JSON evidence (`XRCompatReport`, schema `version: 1`).
|
|
1856
|
+
- Failure attribution: `classifyCompatCause(code, layer)` maps SDK codes (`camera-*`, `no-camera`, `webgl-*` → capability; `model-*` → model; `timeout:lifecycle`, `feed-stalled`, `lifecycle:*` → lifecycle; `fps-low`, `inference-high` → performance).
|
|
1857
|
+
- CI: `pnpm smoke` (Playwright, headless Chromium, fake camera) runs `/compat?auto=1` and uploads the evidence; `.github/workflows/compat-smoke.yml`, advisory.
|
|
1858
|
+
|
|
1859
|
+
### Session lifecycle — start / pause / resume / stop / reset
|
|
1860
|
+
|
|
1861
|
+
```
|
|
1862
|
+
idle ──setMediaSource──> media-bound ──start──> running ──stop──> stopped
|
|
1863
|
+
↑ │ ↑ │
|
|
1864
|
+
│ pause │ │ resume │
|
|
1865
|
+
│ ▼ │ │
|
|
1866
|
+
│ paused ─┘ │
|
|
1867
|
+
└────────────reset──────────────────────┘
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
- `session.pause()` — `running → paused`: feed loop stops, every model pauses, the video element pauses; the camera stream stays open. No-op in any other state. `session.resume()` (or `start()`) continues with no rebind. `setMediaSource()` while paused binds the new media and stays paused.
|
|
1871
|
+
- `session.stop()` releases the stream/tracks and keeps the models initialized; `start()` afterwards needs `setMediaSource()` first. Binding the _same_ source again (the webcam after a privacy-mode stop) re-opens it; the "unchanged source" short-circuit only applies while the media is live. `session.reset()` also releases every model and clears errors.
|
|
1872
|
+
- **Provider unmount disposes the session** (route change away from the AR page): tracks and models are released exactly once and later calls throw `"… called after the provider unmounted"`. Keep one `<XRProvider>` above the pages that share a session.
|
|
1873
|
+
- **Camera loss.** A live video track that ends (unplugged, revoked, taken by another app) or a camera permission that flips to denied releases the media once, suspends the models, sets `useXRError()` to `reason: "camera-lost"` (or `"permission-denied"`) and parks the session in `stopped`. Recover with `setMediaSource()` + `start()`. Tracks the session replaced itself during a source swap never trigger this.
|
|
1874
|
+
- **Camera constraints.** The webcam bind asks for 24–60 fps and `camRes` width; a camera that cannot promise the minimum (cheap, virtual or fake devices) is retried once with a soft frame-rate request, so a slow camera binds instead of failing with `OverconstrainedError` (which now reads as `unsupported` when even the relaxed request fails).
|
|
1875
|
+
- **Visibility.** The session always retries `video.play()` when a hidden tab returns (iOS Safari pauses the camera track). `<XRProvider lifecycle={{ pauseWhenHidden: true }}>` additionally pauses the whole session while hidden and resumes on return; a session the app paused itself stays paused.
|
|
1876
|
+
- `useXRLifecycle()` (`@vincentt-xr/sdk/low-level`) subscribes to the state: `"idle" | "media-bound" | "running" | "paused" | "stopped"`.
|
|
1877
|
+
|
|
1878
|
+
Full contract and manual steps: `docs/api/session-lifecycle.mdx`; example: `/session-health`.
|
|
1879
|
+
|
|
1880
|
+
### Tracking diagnostics — `useXRDiagnostics()` and `<XRDiagnosticsPanel>`
|
|
1881
|
+
|
|
1882
|
+
Opt-in: sampled on an interval (default 250 ms, never per frame) only while a
|
|
1883
|
+
consumer is mounted, so it costs nothing in production until rendered. Gate it
|
|
1884
|
+
behind a debug flag.
|
|
1885
|
+
|
|
1886
|
+
```tsx
|
|
1887
|
+
import { useXRDiagnostics } from "@vincentt-xr/sdk/low-level";
|
|
1888
|
+
import { XRDiagnosticsPanel } from "@vincentt-xr/sdk/debug-ui";
|
|
1889
|
+
|
|
1890
|
+
const d = useXRDiagnostics({ intervalMs: 250, models: [XRModel.FACE_TRACKER] });
|
|
1891
|
+
d.session; // lifecycle, ready, error, source/sourceKind, sourceWidth/Height, renderWidth/Height, feedFps, feedFrames
|
|
1892
|
+
d.models[0]; // model, label, state, inferenceMs, inferenceFps, inferences, skippedFrames,
|
|
1893
|
+
// detections, confidence, status, error, paused, targetFps, owners
|
|
1894
|
+
d.inferenceMsTotal; // ms of ML per frame across running models
|
|
1895
|
+
|
|
1896
|
+
<XRDiagnosticsPanel models={[XRModel.FACE_TRACKER, XRModel.HAND_TRACKER]} />; // DOM panel, outside the canvas
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
`state` per model: `"unavailable"` (nobody acquired it), `"loading"`,
|
|
1900
|
+
`"ready"`, `"paused"` (no active owner, or the session is not running),
|
|
1901
|
+
`"error"`. `models` defaults to every `XRModel`, so unacquired models show as
|
|
1902
|
+
unavailable. `detections`/`confidence`: face = count / none; hand, gesture =
|
|
1903
|
+
count / score; body, holistic = count / mean landmark visibility; segmenters
|
|
1904
|
+
= 1 when a mask was produced / none. `skippedFrames` counts RAF ticks the
|
|
1905
|
+
model did not analyse (its `targetFps` cap, or busy). The legacy
|
|
1906
|
+
`DebuggerProvider` / `DebuggerOverlay` (camera fps, render time, memory)
|
|
1907
|
+
still exists; prefer this hook for tracker health.
|
|
1908
|
+
|
|
1909
|
+
### Device calibration — `useXRCalibration()`
|
|
1910
|
+
|
|
1911
|
+
Orientation, mirror mode, scale/offset, face distance and body proportions,
|
|
1912
|
+
measured once, persisted (`localStorage`, key `vincentt.xr.calibration`),
|
|
1913
|
+
reset, and applied **only where you call it**. Raw tracker results are never
|
|
1914
|
+
altered.
|
|
1915
|
+
|
|
1916
|
+
```tsx
|
|
1917
|
+
import { useXRCalibration } from "@vincentt-xr/sdk/tracking";
|
|
1918
|
+
|
|
1919
|
+
const cal = useXRCalibration({ storageKey: "myapp.calibration" }); // persist: false keeps it in memory
|
|
1920
|
+
cal.calibrateFace(face.landmarks); // reference face (feed-space landmarks); false without a face
|
|
1921
|
+
cal.calibrateBody(poseFeedLandmarks); // reference body (33 pose landmarks in feed space)
|
|
1922
|
+
cal.update({ orientation: 90, mirrored: "auto", scale: 1.1, offset: { x: 0, y: 0 } });
|
|
1923
|
+
cal.applyToFeedPoint(face.landmarks[4]); // new point; input untouched
|
|
1924
|
+
cal.faceDistance(face.landmarks); // 1 at the reference distance, > 1 closer, null without a face/reference
|
|
1925
|
+
cal.bodyScale(poseFeedLandmarks); // 1 at the reference, null without a body/reference
|
|
1926
|
+
cal.reset(); // defaults + clears storage
|
|
1927
|
+
cal.calibration; // { version, orientation, mirrored, scale, offset, face, body, updatedAt }
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
`mirrored: "auto"` follows the session (only the webcam is mirrored), so it
|
|
1931
|
+
never flips a point; `true`/`false` flip only when they disagree with the
|
|
1932
|
+
session. Pure helpers for non-React code: `applyCalibrationToFeedPoint`,
|
|
1933
|
+
`measureFaceCalibration`, `measureBodyCalibration`, `estimateFaceDistance`,
|
|
1934
|
+
`estimateBodyScale`, `updateCalibration`, `resetCalibration`,
|
|
1935
|
+
`loadCalibration`, `saveCalibration`, `clearCalibration` (all
|
|
1936
|
+
`@vincentt-xr/sdk/tracking`). Body landmarks from `getLandmarks2D()` are
|
|
1937
|
+
tracker space; map to feed with `trackerToFeed` before measuring. Full
|
|
1938
|
+
contract: `docs/api/diagnostics-calibration.mdx`; example: `/session-health`.
|
|
1939
|
+
|
|
1008
1940
|
---
|
|
1009
1941
|
|
|
1010
1942
|
## Reading tracker state
|
|
@@ -1092,6 +2024,75 @@ useFrame(() => {
|
|
|
1092
2024
|
> above; reach for the raw read only when you need the session handle for something the
|
|
1093
2025
|
> selector can't express.
|
|
1094
2026
|
|
|
2027
|
+
### `useGesture` / `<GestureListener>` — reusable gesture detection (face, hand, body)
|
|
2028
|
+
|
|
2029
|
+
One engine for pinch, grab, open palm, wave, hand raise, nod, head shake,
|
|
2030
|
+
mouth open, smile, blink, brow raise, arm raise and T-pose, plus custom
|
|
2031
|
+
gestures. Import from `@vincentt-xr/sdk/tracking`. Each gesture acquires the
|
|
2032
|
+
shared face, hand or body model it needs (no second detector); mount hooks
|
|
2033
|
+
once above scene switches like any tracker.
|
|
2034
|
+
|
|
2035
|
+
```tsx
|
|
2036
|
+
import { GestureListener, useGesture } from "@vincentt-xr/sdk/tracking";
|
|
2037
|
+
|
|
2038
|
+
const pinch = useGesture("pinch", { hand: "right", onBegin: () => grab(), onEnd: () => drop() });
|
|
2039
|
+
<GestureListener gesture="headNod" onBegin={() => confirm()} />;
|
|
2040
|
+
```
|
|
2041
|
+
|
|
2042
|
+
- **Built-ins** (`GESTURE_NAMES`, `GESTURES[name]` with `kind`, `description`,
|
|
2043
|
+
`defaults`): face `mouthOpen`, `smile`, `blink`, `browRaise`, `headNod`,
|
|
2044
|
+
`headShake`; hand `pinch`, `grab`, `openPalm`, `handRaised`, `wave`, and
|
|
2045
|
+
the finger-count signs `oneFinger`, `peaceSign`, `threeFingers`,
|
|
2046
|
+
`fourFingers`, `fist`, `palm`, `thumbUp`, `thumbDown` (the weakest required
|
|
2047
|
+
finger scores, so 1 only when every finger agrees; `grab` / `openPalm` are
|
|
2048
|
+
the thumb-agnostic mean-curl versions of `fist` / `palm`); body
|
|
2049
|
+
`armRaisedLeft`, `armRaisedRight`, `armRaised`, `bothArmsRaised`, `tPose`.
|
|
2050
|
+
Hand measures are scale-free (÷ palm size), body measures are in shoulder
|
|
2051
|
+
widths, body sides are the person's. Nod / shake / wave are oscillations
|
|
2052
|
+
(three turning points above an amplitude within a window) and ship with a
|
|
2053
|
+
600 ms cooldown so one nod counts once.
|
|
2054
|
+
- **Engine contract** (`GestureDetectorOptions`): per frame the definition
|
|
2055
|
+
yields `{ value, confidence }` or `null` (no subject). Confidence below
|
|
2056
|
+
`minConfidence` (0.5) or no sample = loss; an active gesture survives loss
|
|
2057
|
+
for `lostGraceMs` (250) with its value frozen, then ends with
|
|
2058
|
+
`endReason: "lost"`. Inactive → `begin` at `value >= enterThreshold`
|
|
2059
|
+
(0.5) after `holdMs` (0) of staying above it, never during `cooldownMs`
|
|
2060
|
+
(0) after the last end; active → `end` ("released") at
|
|
2061
|
+
`value <= exitThreshold` (enter − 0.15). A `begin` always gets one `stay`
|
|
2062
|
+
frame. `GestureState`: `active`, `value`, `confidence`, `tracked`, `phase`
|
|
2063
|
+
begin | stay | end | none, `justBegan`, `justEnded`, `endReason`,
|
|
2064
|
+
`startedAtMs`, `durationMs` (as of the last published change, not a
|
|
2065
|
+
per-frame clock; derive a live duration from `startedAtMs`), `count`.
|
|
2066
|
+
Face/hand/body confidence is
|
|
2067
|
+
presence-based (1 while tracked); custom definitions may supply a real
|
|
2068
|
+
score.
|
|
2069
|
+
- **Options** (`UseGestureOptions`): the detector options above, `active`,
|
|
2070
|
+
`faceIndex` / `numFaces`, `hand` / `handIndex`, `bodyIndex`, `targetFps`,
|
|
2071
|
+
`onBegin` / `onEnd` / `onChange` (frame-loop callbacks with that frame's
|
|
2072
|
+
state). The hook returns React state that changes only when the gesture's
|
|
2073
|
+
state changes. `resolveGestureDetectorOptions(overrides, defaults)` is the
|
|
2074
|
+
merge the hook applies.
|
|
2075
|
+
- **Custom**: `defineGesture({ name, kind, description, defaults?,
|
|
2076
|
+
createMemory?, measure })`; `measure(input, memory, nowMs)` gets a
|
|
2077
|
+
`FaceInfo | null` (face) or `TrackerPoint[] | undefined` (hand: the 21
|
|
2078
|
+
joints; body: the 33 pose points). Low-level seam
|
|
2079
|
+
(`@vincentt-xr/sdk/low-level`): `stepGestureDetector`,
|
|
2080
|
+
`createGestureDetectorState`, `snapshotGestureState`,
|
|
2081
|
+
`gestureStatesEqual`, the measures (`measurePinch`, `measureGrab`,
|
|
2082
|
+
`measureOpenPalm`, `measureHandHeight`, `measureArmRaise`,
|
|
2083
|
+
`measureTPose`, `measureFingerPattern({ fingers, thumb })` with `null`
|
|
2084
|
+
for "either", `measureThumb`, `fingerExtensions`, `thumbExtension`,
|
|
2085
|
+
`thumbDirection`), `HAND_JOINTS`, `POSE_JOINTS`, `OscillationTracker`.
|
|
2086
|
+
- **Versus the others**: `<GestureTrigger>` fires once on a named static
|
|
2087
|
+
hand pose from the gesture-recognizer model; `useGesture` measures
|
|
2088
|
+
continuous and temporal gestures from the landmark trackers with tunable
|
|
2089
|
+
thresholds. `useFaceAction` is the face-only predecessor; the face gestures
|
|
2090
|
+
reuse its measurements and add cooldown, confidence, loss grace and the
|
|
2091
|
+
temporal nod / shake. Debug panel: `GestureSettingsPanel`
|
|
2092
|
+
(`@vincentt-xr/sdk/debug-ui`) with `resolveGestureSettings` /
|
|
2093
|
+
`patchGestureSettings` / `GESTURE_SETTINGS_FIELDS`. Full contract and the
|
|
2094
|
+
manual test plan: `docs/api/gestures.mdx`; example: `/gestures`.
|
|
2095
|
+
|
|
1095
2096
|
### `<GestureTrigger gestures={["victory"]} onTrigger={(g) => ...}>` — Declarative one-shot gesture handler
|
|
1096
2097
|
|
|
1097
2098
|
Fires once per gesture recognition event. The simplest way to act on a gesture — prefer it over a manual `useFrame` poll. Two modes:
|
|
@@ -1131,6 +2132,62 @@ In a v2-template project this is already mounted in `src/App.tsx` — do NOT add
|
|
|
1131
2132
|
treat the camera feed as a real scene plane should render a separate editor
|
|
1132
2133
|
video plane instead of changing `VideoBackground` semantics.
|
|
1133
2134
|
|
|
2135
|
+
### `<PersonCutout>` — background replacement as two screen layers
|
|
2136
|
+
|
|
2137
|
+
The easy path for virtual backgrounds and person cut-outs: any full-screen
|
|
2138
|
+
background surface below, the person-masked live camera above.
|
|
2139
|
+
|
|
2140
|
+
```tsx
|
|
2141
|
+
import { FULL_SCREEN_ANCHORS, PersonCutout, ScreenImage, ScreenSpaceUI } from "@vincentt-xr/sdk";
|
|
2142
|
+
|
|
2143
|
+
<ScreenSpaceUI>
|
|
2144
|
+
<ScreenImage
|
|
2145
|
+
src="/backgrounds/beach.jpg"
|
|
2146
|
+
fit="cover"
|
|
2147
|
+
anchors={FULL_SCREEN_ANCHORS}
|
|
2148
|
+
renderOrder={0}
|
|
2149
|
+
/>
|
|
2150
|
+
<PersonCutout renderOrder={100} />
|
|
2151
|
+
</ScreenSpaceUI>;
|
|
2152
|
+
```
|
|
2153
|
+
|
|
2154
|
+
- **Background**: a `ScreenImage` with an image, `{ kind: "gif" }`, or
|
|
2155
|
+
`{ kind: "texture", texture: videoTexture }`, or a custom shader plane inside
|
|
2156
|
+
`<ScreenTransform anchors={FULL_SCREEN_ANCHORS}>`; give it a lower
|
|
2157
|
+
`renderOrder` than the cut-out (default `100`). Do not mirror it unless the
|
|
2158
|
+
content must match a mirrored webcam (`flipX`).
|
|
2159
|
+
- **Foreground**: `PersonCutout` is a `ScreenImage` whose source is the session
|
|
2160
|
+
camera texture and whose `maskTexture` is the portrait segmentation mask. The
|
|
2161
|
+
camera texture is already cover-cropped and webcam-mirrored, and the mask is
|
|
2162
|
+
in that same feed space; the mask is sampled through the image's own fit
|
|
2163
|
+
transform, so alignment holds under any aspect ratio, crop or flip. Props:
|
|
2164
|
+
the `ScreenImage` props except `source` / mask props, plus `invert` (show the
|
|
2165
|
+
background only), `fallback` (`"camera"` default: unmasked feed while the
|
|
2166
|
+
model loads or fails; `"hidden"`: nothing), `active`, `onStatusChange(status,
|
|
2167
|
+
error)` with `loading` | `ready` | `error`. It acquires `<Segmentation
|
|
2168
|
+
type="portrait">`'s model itself and shares it with `VideoBackground` and
|
|
2169
|
+
`usePersonMask`.
|
|
2170
|
+
- `usePersonMaskTexture()` returns `{ texture, status, error }` for custom
|
|
2171
|
+
compositions; the mask is white (RGB 255) on the person and black elsewhere
|
|
2172
|
+
with alpha 255 everywhere, so read it as `maskChannel="green"` (what three's
|
|
2173
|
+
`alphaMap` reads too). `ScreenImage` gained `maskTexture`, `maskChannel`
|
|
2174
|
+
(`alpha` | `red` | `green` | `luminance`), `maskInvert`, `maskFeather` and
|
|
2175
|
+
`maskErode` for any image; the mask always samples through the image's fit
|
|
2176
|
+
transform and takes precedence over advanced blend modes.
|
|
2177
|
+
- **Edge quality**: `maskFeather` is a 3×3 blur radius in mask texels (`0` =
|
|
2178
|
+
hard edge); `maskErode` (0..1, needs feather) shrinks the kept region into
|
|
2179
|
+
that soft edge so the source's background fringe (green-screen spill, halo)
|
|
2180
|
+
is trimmed. `PersonCutout` defaults to `maskFeather={4}` / `maskErode={0.7}`,
|
|
2181
|
+
measured on the 720-px segmenter mask; raise erode for spill, lower it when
|
|
2182
|
+
thin features (fingers, hair tips) vanish. Neither fixes mask lag on fast
|
|
2183
|
+
motion, which is the segmenter's temporal error, not an edge problem.
|
|
2184
|
+
- **2D versus 3D**: use this for background replacement and person-in-scene
|
|
2185
|
+
effects (one quad, one mask, covers body, hair and clothes). Use the face
|
|
2186
|
+
mesh when content must stick to the face surface with perspective and
|
|
2187
|
+
shading; use the head occluder when 3D objects must hide behind the head in
|
|
2188
|
+
depth, which a 2D mask cannot do. Full contract and the manual test plan:
|
|
2189
|
+
`docs/api/person-cutout.mdx`; example: `/person-cutout`.
|
|
2190
|
+
|
|
1134
2191
|
---
|
|
1135
2192
|
|
|
1136
2193
|
## Mesh primitives and textures
|
|
@@ -1379,6 +2436,56 @@ authoring model into center, size, rotation, pivot offset, and aspect. Do not co
|
|
|
1379
2436
|
the default `720x1280` root or `32` reference-pixel constant into another renderer
|
|
1380
2437
|
or guide.
|
|
1381
2438
|
|
|
2439
|
+
### Coordinate spaces — one toolkit
|
|
2440
|
+
|
|
2441
|
+
Six spaces, named once; every conversion lives in `@vincentt-xr/sdk` and does
|
|
2442
|
+
its origin, axis, crop and mirror change exactly once. Never chain a manual
|
|
2443
|
+
flip, crop or design-canvas conversion after a helper.
|
|
2444
|
+
|
|
2445
|
+
- **source**: raw media pixels (top-left, Y down), never mirrored.
|
|
2446
|
+
- **feed**: the camera texture, 0..1 top-left Y down — the source
|
|
2447
|
+
cover-cropped to the render size, enlarged by `perf.cameraZoom` (1.2)
|
|
2448
|
+
about the centre, mirrored for the **webcam only**. Every landmark and SDK
|
|
2449
|
+
mask lives here. The feed fills the viewport, so feed and viewport agree
|
|
2450
|
+
per axis on every aspect ratio.
|
|
2451
|
+
- **tracker**: feed remapped to center origin, X right, Y up, Z toward the
|
|
2452
|
+
viewer (`TrackerPoint`: hand, body, holistic nodes).
|
|
2453
|
+
- **viewport**: the rendered canvas in CSS pixels; **screen**: 720×1280
|
|
2454
|
+
design pixels, center origin, Y up; **world**: three.js, tracked content on
|
|
2455
|
+
the z = 0 plane.
|
|
2456
|
+
|
|
2457
|
+
`FeedCrop` (`computeFeedCrop({ source, feed, zoom, mirrored })`) is the one
|
|
2458
|
+
description of source → feed: the session's blit loop draws with it,
|
|
2459
|
+
`computeMaskUvTransform` (now with `zoom`) fits source-space masks with it,
|
|
2460
|
+
and the conversions use it. `useXRFeedCrop()` (`/low-level`) returns the live
|
|
2461
|
+
one (`null` until media is bound).
|
|
2462
|
+
|
|
2463
|
+
Conversions: `sourceToFeed` / `feedToSource` (+ `isFeedPointInSource`),
|
|
2464
|
+
`feedToTracker` / `trackerToFeed`, `feedToViewport` / `viewportToFeed`,
|
|
2465
|
+
`feedToScreenPixels` / `screenPixelsToFeed`, `feedToWorld` / `trackerToWorld`
|
|
2466
|
+
(camera, optional `planeZ`, depth-independent), `worldToFeed` /
|
|
2467
|
+
`worldToTracker` (`z` = NDC depth), `computeFitLayout(source, destination,
|
|
2468
|
+
"cover" | "contain", zoom)`. Mirroring: positions never need a flip; direction
|
|
2469
|
+
signs do — use `isMirroredSourceKind(sourceKind)` (webcam or unknown → true),
|
|
2470
|
+
never `sourceKind === "webcam"` inline; `mirrorFeedX` flips a feed x.
|
|
2471
|
+
Orientation: `orientSourcePoint` / `orientedSize` rotate a sideways capture
|
|
2472
|
+
(0 / 90 / 180 / 270, clockwise) before `sourceToFeed`; the browser already
|
|
2473
|
+
orients webcam frames. The trackers (`HandTracker`, `GestureTracker`,
|
|
2474
|
+
`BodyTracker`, `HolisticTracker`, `FaceTracker`, `IrisEffect`), the face
|
|
2475
|
+
model's mirror handling, the viewport layout and the mask fit all run on
|
|
2476
|
+
these helpers; `unprojectToZZero` remains as a thin alias of `feedToWorld`.
|
|
2477
|
+
Diagrams and worked examples: `docs/api/coordinate-spaces.mdx`.
|
|
2478
|
+
|
|
2479
|
+
```ts
|
|
2480
|
+
import { feedToSource, feedToWorld, trackerToFeed, feedToViewport } from "@vincentt-xr/sdk";
|
|
2481
|
+
import { useXRFeedCrop } from "@vincentt-xr/sdk/low-level";
|
|
2482
|
+
|
|
2483
|
+
const crop = useXRFeedCrop(); // source → feed, live
|
|
2484
|
+
const sourcePx = crop ? feedToSource(face.landmarks[1], crop) : null; // back onto the raw frame
|
|
2485
|
+
const css = feedToViewport(trackerToFeed(hand.coordinates[8]), viewportSize); // DOM overlay
|
|
2486
|
+
feedToWorld(face.landmarks[1], camera, mesh.position); // onto the z = 0 plane
|
|
2487
|
+
```
|
|
2488
|
+
|
|
1382
2489
|
### Screen/world coordinate utilities
|
|
1383
2490
|
|
|
1384
2491
|
Import the `*ScreenPixels` coordinate helpers, `screenPointToRay`, and
|
|
@@ -1388,6 +2495,11 @@ design pixels are Three.js world units.
|
|
|
1388
2495
|
SDK screen pixels use the Effect House-style `720x1280` design canvas, center
|
|
1389
2496
|
origin, and +Y up.
|
|
1390
2497
|
|
|
2498
|
+
`docs/api/screen-space-coordinates.mdx` is the API source of truth for which
|
|
2499
|
+
helper owns each conversion. Keep this grounding summary and that page aligned;
|
|
2500
|
+
raw MediaPipe face landmarks are normalized image points, while SDK low-level
|
|
2501
|
+
hand, gesture, and body reads are center-origin tracker points.
|
|
2502
|
+
|
|
1391
2503
|
```ts
|
|
1392
2504
|
const units = screenPixelsToUnits(120, { parentSize: screenDimensions });
|
|
1393
2505
|
const pixels = screenUnitsToPixels(units, { parentSize: screenDimensions });
|
|
@@ -1416,8 +2528,8 @@ const placementRay = screenPointToRay({
|
|
|
1416
2528
|
- Use `clientToScreenPixels` / `screenPixelsToClient` for pointer events and DOM
|
|
1417
2529
|
overlays; pass the viewport element's `getBoundingClientRect()` result.
|
|
1418
2530
|
- Use `normalizedToScreenPixels` / `screenPixelsToNormalized` for normalized
|
|
1419
|
-
viewport coordinates with top-left origin.
|
|
1420
|
-
camera
|
|
2531
|
+
viewport coordinates with top-left origin — a feed point is one. They do not
|
|
2532
|
+
undo the camera crop; `feedToSource` with the session's `FeedCrop` does.
|
|
1421
2533
|
- Use `mediaToScreenPixels` / `screenPixelsToMedia` with the shared
|
|
1422
2534
|
`XRMediaLayout` for camera/image pixels. They apply cover/contain offsets,
|
|
1423
2535
|
scaling, and optional display mirroring; do not manually repeat that math.
|
|
@@ -1677,8 +2789,12 @@ For debug pages that need a camera/image/video picker, import
|
|
|
1677
2789
|
applies the selected debug media preset to the current XR session and starts
|
|
1678
2790
|
the face-tracking media pipeline used by the debug face examples. The older
|
|
1679
2791
|
`useMediaSourceSession(activeMediaSource, { pipeline })` name remains available
|
|
1680
|
-
for custom debug pipelines.
|
|
1681
|
-
|
|
2792
|
+
for custom debug pipelines. `defaultMediaSources` lists the hosted clips first
|
|
2793
|
+
(so `defaultMediaSources[0]` is a video and a page opens without a camera
|
|
2794
|
+
prompt or a camera at all), then `webcam`, then the stills; pick the webcam
|
|
2795
|
+
entry explicitly when a page must start on the live camera. Use these only in
|
|
2796
|
+
debug/example UI; production runtime should set the session media source
|
|
2797
|
+
directly.
|
|
1682
2798
|
|
|
1683
2799
|
### Debug UI usage in Harness client apps
|
|
1684
2800
|
|
|
@@ -2105,8 +3221,10 @@ if (error?.reason === "camera-in-use") {
|
|
|
2105
3221
|
The shape is `{ error, message, reason }`:
|
|
2106
3222
|
|
|
2107
3223
|
- **`reason`** — the stable, machine-readable identity. Branch on this. One of
|
|
2108
|
-
`"permission-denied"`, `"camera-in-use"`, `"no-camera"`, `"unsupported"`,
|
|
2109
|
-
`"
|
|
3224
|
+
`"permission-denied"`, `"camera-in-use"`, `"no-camera"`, `"unsupported"`,
|
|
3225
|
+
`"camera-lost"` (the camera stopped mid-session; see "Session lifecycle"), or
|
|
3226
|
+
`"unknown"`. `sessionReasonToFallback(reason)` (`@vincentt-xr/sdk/low-level`)
|
|
3227
|
+
maps it onto the `XRFallbackReason` vocabulary.
|
|
2110
3228
|
- **`message`** — human prose for display. It may be reworded in any release, so
|
|
2111
3229
|
never match on its text.
|
|
2112
3230
|
- **`error`** — a legacy constant kept for backwards compatibility. Carries no
|
|
@@ -2120,10 +3238,61 @@ request; `"unknown"` is anything unrecognized.
|
|
|
2120
3238
|
|
|
2121
3239
|
---
|
|
2122
3240
|
|
|
3241
|
+
## Asset lifecycle & GPU memory — `useXRAsset`, `XRAssetCache`
|
|
3242
|
+
|
|
3243
|
+
One identity, one load, one owner list, one disposal per asset. Own three.js
|
|
3244
|
+
loaders over the SDK's verifying fetch; no drei.
|
|
3245
|
+
|
|
3246
|
+
```tsx
|
|
3247
|
+
import {
|
|
3248
|
+
XRAssetCacheProvider,
|
|
3249
|
+
createAssetCache,
|
|
3250
|
+
defineGltfAsset,
|
|
3251
|
+
defineTextureAsset,
|
|
3252
|
+
defineVideoAsset,
|
|
3253
|
+
useXRAsset,
|
|
3254
|
+
useXRAssetStats,
|
|
3255
|
+
useXRAssetCache,
|
|
3256
|
+
} from "@vincentt-xr/sdk";
|
|
3257
|
+
|
|
3258
|
+
const HAT = defineGltfAsset(url); // { key, load(signal), dispose, bytes }
|
|
3259
|
+
const { value, status, error } = useXRAsset(HAT); // shared value; reference released on unmount
|
|
3260
|
+
<XRAssetCacheProvider cache={createAssetCache({ budgetBytes: 64 * 1048576 })}>
|
|
3261
|
+
…
|
|
3262
|
+
</XRAssetCacheProvider>;
|
|
3263
|
+
const stats = useXRAssetStats(); // assets, referencedAssets, usageBytes, reclaimableBytes, budgetBytes, overBudget, evictions, hits, misses
|
|
3264
|
+
```
|
|
3265
|
+
|
|
3266
|
+
- `cache.acquire(asset, ownerId)` / `release(key, ownerId)`: in-flight loads are shared; owners are ids (re-renders and StrictMode never double-count); releasing during a load aborts it.
|
|
3267
|
+
- Unreferenced values stay cached until the budget (default 256 MiB) needs room — LRU, unreferenced only, `dispose` exactly once — or `disposeUnreferenced()` / `disposeAll()`. `setBudget`, `evict(bytes)`, `preload(asset)` (warm without a reference), `get(key)`, `list()`, `subscribe()`.
|
|
3268
|
+
- Bytes are estimates: `estimateTextureBytes`, `estimateGeometryBytes`, `estimateObjectBytes`; `disposeObject3D` releases everything under a scene node once.
|
|
3269
|
+
- Route change: owners release on unmount; `<XRAssetCacheProvider>` disposes unreferenced values when it unmounts (`disposeUnreferencedOnUnmount`, default true). Tracker-owned resources are the session registry's, released once on provider unmount.
|
|
3270
|
+
- Custom assets: write your own `XRAssetDefinition` (`key` + `load` honouring `signal` + `dispose` + `bytes`).
|
|
3271
|
+
|
|
3272
|
+
Full contract: `docs/api/asset-lifecycle.mdx`; example: `/asset-lifecycle`.
|
|
3273
|
+
|
|
3274
|
+
---
|
|
3275
|
+
|
|
2123
3276
|
## Capture & share
|
|
2124
3277
|
|
|
2125
3278
|
Grab a photo of the live render or record the canvas, then save or share it.
|
|
2126
3279
|
|
|
3280
|
+
### Camera, layout, mirror, and capture parity
|
|
3281
|
+
|
|
3282
|
+
One owner composes media pixels before they are displayed, tracked, or captured.
|
|
3283
|
+
For direct session media, the session owns the webcam source and mirrors it once
|
|
3284
|
+
before its cover-cropped camera texture reaches trackers and R3F content. For
|
|
3285
|
+
`XRViewport mediaStrategy="internal"`, the viewport owns source selection,
|
|
3286
|
+
fit, and mirror state; it sends that already composed canvas to the session, so
|
|
3287
|
+
the tracker, visible media surface, and capture target share the same pixels.
|
|
3288
|
+
|
|
3289
|
+
Resize and source changes recompute this composition before the next tracker
|
|
3290
|
+
frame. Do not add a second CSS, canvas, or shader mirror to a session camera
|
|
3291
|
+
texture or viewport media canvas. `captureFrame()` and `captureStream()` capture
|
|
3292
|
+
the active XRScene canvas when present, otherwise the DOM viewport media canvas.
|
|
3293
|
+
They capture canvas composition only; arbitrary DOM HUD elements are not part of
|
|
3294
|
+
the image or recording.
|
|
3295
|
+
|
|
2127
3296
|
### `useFrameCapture()` + `dataURLtoFile()` — Single-shot photo
|
|
2128
3297
|
|
|
2129
3298
|
Use `useFrameCapture` for UI/effect logic. It exposes `status`, `capturePhoto`,
|
|
@@ -2144,7 +3313,13 @@ await photo.capturePhoto();
|
|
|
2144
3313
|
```
|
|
2145
3314
|
|
|
2146
3315
|
For synchronous low-level code, `session.captureFrame()` remains available via
|
|
2147
|
-
`useXRContext` from `@vincentt-xr/sdk/low-level`.
|
|
3316
|
+
`useXRContext` from `@vincentt-xr/sdk/low-level`. It also exposes
|
|
3317
|
+
`session.captureStream(fps?)`, which records the same active canvas:
|
|
3318
|
+
|
|
3319
|
+
```tsx
|
|
3320
|
+
const rec = useMediaRecorder();
|
|
3321
|
+
await rec.start(session.captureStream(30));
|
|
3322
|
+
```
|
|
2148
3323
|
|
|
2149
3324
|
### `useMediaRecorder()` — Record the canvas to a video Blob
|
|
2150
3325
|
|
|
@@ -2156,8 +3331,7 @@ the first supported codec (mp4 → webm). Pass `audio: true` to mix in the mic,
|
|
|
2156
3331
|
import { useMediaRecorder } from "@vincentt-xr/sdk";
|
|
2157
3332
|
|
|
2158
3333
|
const rec = useMediaRecorder();
|
|
2159
|
-
|
|
2160
|
-
await rec.start(canvas.captureStream(30));
|
|
3334
|
+
await rec.start(session.captureStream(30));
|
|
2161
3335
|
// ...later
|
|
2162
3336
|
rec.stop(); // -> rec.recordedBlob is the video Blob
|
|
2163
3337
|
```
|