@vincentt-xr/sdk 2.0.0-alpha.7 → 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 +1109 -13
- package/dist/{DebuggerUI-BjOMr8q_.js → DebuggerUI-C-ldiozL.js} +1 -1
- package/dist/{DebuggerUI-B6bP6P9r.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/canonical/CanonicalFaceMeshRenderer.d.ts +33 -3
- 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 +1 -1
- 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/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/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-ByhzppYl.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 +18 -1
- package/dist/entries/face-effects.d.ts +11 -3
- package/dist/entries/low-level.d.ts +22 -3
- package/dist/entries/product.d.ts +11 -0
- package/dist/entries/tracking.d.ts +24 -1
- package/dist/face-effects.cjs +155 -1
- package/dist/face-effects.js +1134 -2197
- 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-C4QpCQIY.js → getFaceInfo-BkZfaQs-.js} +5 -5
- package/dist/{getFaceInfo-fA1bFBub.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 +42 -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-VJ0SJwmG.js → index-CHVbH0hz.js} +4 -4
- package/dist/index-CeVkgsag.cjs +1 -0
- package/dist/index-CnGox6Xr.js +3054 -0
- package/dist/{index-ByoNj0QP.js → index-CsUdQ-pn.js} +113 -111
- package/dist/{index-BjmTe9eM.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-DXT3WccE.js → index-s9pF4zkG.js} +1065 -1101
- package/dist/internal-BYza1h8_.cjs +1 -0
- package/dist/{internal-Dmk1cTXq.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 +1085 -634
- package/dist/main.cjs +1 -1
- package/dist/main.js +1519 -465
- 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-KZk6eRiY.js → renderGroups-CN1cGFUo.js} +1 -1
- package/dist/{renderGroups-80H3DpTl.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/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-CpkiK5YA.cjs → session-BBr0yN8h.cjs} +7 -7
- package/dist/{session-DLFN_VII.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-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 -383
- package/dist/types/compositor.d.ts +21 -0
- 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 +27 -2
- package/dist/types/segmentation.d.ts +19 -0
- package/dist/types/session.d.ts +41 -3
- 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/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/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 +4 -2
- 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/screen-space-coordinates.mdx +4 -1
- package/docs/api/segmentation-overlay.mdx +301 -0
- package/docs/api/session-lifecycle.mdx +136 -0
- package/docs/api/trackers.mdx +5 -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 +13 -1
- package/dist/FaceMeshRenderer-C5l6VLdO.cjs +0 -155
- package/dist/FaceMeshRenderer-wjAHrJzP.js +0 -1270
- package/dist/FaceRetouch-BkJzoA79.cjs +0 -134
- package/dist/FaceRetouch-DwbKQJAN.js +0 -551
- package/dist/contexts-Dr62It_K.cjs +0 -1
- package/dist/index-9dlAQAhy.js +0 -1462
- package/dist/index-BpEf2BBb.cjs +0 -1
- package/dist/index-CyUUurQT.cjs +0 -1
- package/dist/index-DCiMbxC3.cjs +0 -1
- package/dist/index-DpI-qo5X.cjs +0 -17
- package/dist/index-DyVF4vEW.js +0 -111
- package/dist/internal-Cq-TEQ5Y.cjs +0 -1
- package/dist/renderLayers-lDtL0_43.cjs +0 -1
- package/dist/renderLayers-zrK2pv5O.js +0 -13
- package/dist/renderPasses-BOoBSr8z.cjs +0 -21
- package/dist/renderPasses-BcquyVtu.js +0 -1500
- package/dist/selectors-B8H97CKA.js +0 -59
- package/dist/selectors-CZLKT2Iv.cjs +0 -1
- package/dist/useFaceInfo-BJ8-_MOs.cjs +0 -1
- package/dist/useFaceInfo-Dk426LTu.js +0 -62
- package/dist/useFaceResults-CtetsO9M.js +0 -19
- package/dist/useFaceResults-wBJF3zyp.cjs +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Iris Tracking"
|
|
3
|
+
description: "Stable left/right iris data from the shared face tracker, and an iris effect that draws a shader or image material over each iris."
|
|
4
|
+
sidebarTitle: "Iris Tracking"
|
|
5
|
+
icon: "eye"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Iris tracking reads the ten refined iris landmarks the face tracker already
|
|
9
|
+
produces. It starts no second detector: `useIrisTracking` acquires the same
|
|
10
|
+
shared `XRModel.FACE_TRACKER` as `FaceTracker` and `useFaceInfo`, and
|
|
11
|
+
`<IrisEffect>` renders over it.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useIrisTracking } from "@vincentt-xr/sdk/tracking";
|
|
15
|
+
import { IrisEffect } from "@vincentt-xr/sdk/face-effects";
|
|
16
|
+
|
|
17
|
+
// Rendering: built-in procedural iris, or an image, or your own material.
|
|
18
|
+
<IrisEffect mode="shader" color="#4fa3ff" opacity={0.85} smoothing={15} />;
|
|
19
|
+
<IrisEffect mode="image" textureUrl="/iris.png" scale={1.1} />;
|
|
20
|
+
|
|
21
|
+
// Data: read inside useFrame.
|
|
22
|
+
const { result, status } = useIrisTracking({ faceIndex: 0, smoothing: 15 });
|
|
23
|
+
useFrame(() => {
|
|
24
|
+
if (!result.detected) return;
|
|
25
|
+
const { center, radius, offset, confidence } = result.left;
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Landmarks and sides
|
|
30
|
+
|
|
31
|
+
| Side (person's) | Center | Ring (outer, top, inner, bottom) | Inner / outer corner | Upper / lower lid |
|
|
32
|
+
| --------------- | ------ | -------------------------------- | -------------------- | ----------------- |
|
|
33
|
+
| `left` | 473 | 474, 475, 476, 477 | 362 / 263 | 386 / 374 |
|
|
34
|
+
| `right` | 468 | 469, 470, 471, 472 | 133 / 33 | 159 / 145 |
|
|
35
|
+
|
|
36
|
+
Sides follow the face-mesh topology, so they are the person's anatomical
|
|
37
|
+
sides on every source: mirrored webcam, straight video or image. The data
|
|
38
|
+
needs the refined mesh (478 points); with fewer landmarks each eye reports
|
|
39
|
+
`detected: false`. These indices are exported as `IRIS_LANDMARKS`. The
|
|
40
|
+
allocation-free helpers behind the hook (`createIrisResult`,
|
|
41
|
+
`updateIrisResult`, `computeIrisEye`, `IrisLead`) live on
|
|
42
|
+
`@vincentt-xr/sdk/low-level` for pipelines that already hold their own
|
|
43
|
+
landmarks.
|
|
44
|
+
|
|
45
|
+
## Coordinate spaces
|
|
46
|
+
|
|
47
|
+
- `center`, `ring`, `eyeCenter`: MediaPipe image space, 0..1 with a top-left
|
|
48
|
+
origin and Y down, the same space as `FaceInfo.landmarks` and
|
|
49
|
+
`detectionResults`. `z` is tracker-relative depth.
|
|
50
|
+
- `trackerCenter`: SDK tracker space (center origin, X right, Y up, Z toward
|
|
51
|
+
the viewer), the same as hand and body points.
|
|
52
|
+
- `radius`, `eyeWidth`: image-width units (half the horizontal ring diameter
|
|
53
|
+
and the corner-to-corner distance). To draw in world space, unproject the
|
|
54
|
+
center and a point `radius` to its right and take their distance, which is
|
|
55
|
+
what `<IrisEffect>` does.
|
|
56
|
+
- `offset`: the iris center relative to the eye center, each axis divided by
|
|
57
|
+
half the eye width and clamped to [-1, 1]; +x toward the person's left, +y
|
|
58
|
+
up, on every source (the horizontal sign is corrected on the mirrored
|
|
59
|
+
webcam feed, so it never flips with the camera). **This is the iris position inside the eyelid opening, not a gaze
|
|
60
|
+
direction**: it ignores head pose and eyeball geometry. Use it for
|
|
61
|
+
pupil-follow and shader inputs, not for "where is the person looking".
|
|
62
|
+
- `openness`: eyelid gap divided by eye width (about 0.25–0.35 open, 0 closed).
|
|
63
|
+
|
|
64
|
+
Mirroring: the session mirrors the webcam feed and no other source, and the
|
|
65
|
+
landmarks are already in that feed space, so positions need no flip. The
|
|
66
|
+
`mirrored` prop on `<IrisEffect>` flips the material horizontally (image
|
|
67
|
+
orientation, shader +x), nothing else.
|
|
68
|
+
|
|
69
|
+
## Confidence and lifecycle
|
|
70
|
+
|
|
71
|
+
MediaPipe reports no score for iris points and emits them even when the eye
|
|
72
|
+
is closed, so confidence is derived: `eye.confidence` is `openness / 0.2`
|
|
73
|
+
clamped to 0..1, scaled by the hold decay. `result.confidence` is 1 while the
|
|
74
|
+
face is live and decays linearly to 0 over `holdMs` while `held`. Gate
|
|
75
|
+
effects on these; the built-in materials fade with `irisConfidence`.
|
|
76
|
+
|
|
77
|
+
`useIrisTracking` returns `status` (`loading` → `ready` / `error`) for the
|
|
78
|
+
shared face model plus `result.detected` / `held` / `version` /
|
|
79
|
+
`timestampMs` / `faceIndex`. The `result` object is stable and mutated in
|
|
80
|
+
place; read it inside `useFrame`.
|
|
81
|
+
|
|
82
|
+
## Smoothing, hold and face selection
|
|
83
|
+
|
|
84
|
+
- Dropouts go through `FaceTrackMemory`, the SDK's one bridge: `holdMs` 250
|
|
85
|
+
and `predictMs` 0 by default, like every other face surface. A face that
|
|
86
|
+
stays missing longer than `holdMs` is dropped and `<IrisEffect>` hides at
|
|
87
|
+
once, so no stale eye effect lingers.
|
|
88
|
+
- `smoothing` runs the same One Euro filter as `TrackingAnchor` on the
|
|
89
|
+
centers and radius (default 0 = raw; ~10 tames jitter at rest). It is a
|
|
90
|
+
low-pass whose floor cutoff is `smoothing × 0.1` Hz, so 15 attenuates and
|
|
91
|
+
lags a 2–3 Hz head shake: keep it low when exact following matters. It
|
|
92
|
+
resets on re-acquire, so the iris never slides in from an old position.
|
|
93
|
+
- `targetFps` requests an inference cap for the shared face model (0 keeps
|
|
94
|
+
the session's `perf.targetFps`, 24 in the template). The first mounted face
|
|
95
|
+
consumer's value wins, so pass the same number everywhere the face model
|
|
96
|
+
is acquired, and note that a later change only takes effect once every
|
|
97
|
+
holder has released the model (a lone `<IrisEffect>` re-acquires and
|
|
98
|
+
re-initializes it; a session pipeline entry pins the first value for the
|
|
99
|
+
session). The example makes the effect the only holder so its panel value
|
|
100
|
+
is the real rate. **60 gives the best following, but use it with caution**: it runs
|
|
101
|
+
face inference 2.5× as often as the template default and will heat and
|
|
102
|
+
throttle phones. Ship it only when the eye effect is the point of the
|
|
103
|
+
experience and you have checked the target devices; otherwise keep the
|
|
104
|
+
session default (or 30) and let `leadMs` bridge the gap.
|
|
105
|
+
- `leadMs` (default 80, 0 disables) bridges throttled inference to the render
|
|
106
|
+
rate: between samples the centers advance along their last
|
|
107
|
+
sample-to-sample velocity for at most `leadMs`, so a 24 fps face model
|
|
108
|
+
reads continuous at 60+ fps instead of hold-and-jump. When the lag itself
|
|
109
|
+
is the problem, raise the face model's `targetFps` first (the example
|
|
110
|
+
requests 60).
|
|
111
|
+
- `faceIndex` / `numFaces` select the face exactly like `useFaceInfo`: the
|
|
112
|
+
index is the result-array position, not a persistent identity, and the
|
|
113
|
+
first mounted face consumer decides `numFaces`.
|
|
114
|
+
|
|
115
|
+
## `<IrisEffect>` props and materials
|
|
116
|
+
|
|
117
|
+
`<IrisEffect>` takes the flat `IrisEffectSettings` (`eye`: both | left |
|
|
118
|
+
right, `mode`: shader | image, `color`, `textureUrl`, `tintImage` (multiply the
|
|
119
|
+
image by `color`; off by default so images show as authored), `scale`, `opacity`,
|
|
120
|
+
`targetFps`, `smoothing`, `leadMs`, `mirrored`, `faceIndex`, `holdMs`) plus `numFaces`, `active`,
|
|
121
|
+
`predictMs`, `texture` (a pre-loaded three texture, beats `textureUrl`),
|
|
122
|
+
`material`, `renderOrder` and `onTracking(handle)`.
|
|
123
|
+
|
|
124
|
+
Each eye is a quad spanning the iris diameter, positioned on the camera's
|
|
125
|
+
z=0 plane. Every material receives the same uniforms (`IrisUniforms`):
|
|
126
|
+
|
|
127
|
+
| Uniform | Type | Meaning |
|
|
128
|
+
| ---------------- | ----------- | ---------------------------------- |
|
|
129
|
+
| `irisCenter` | `vec2` | Image-space center |
|
|
130
|
+
| `irisRadius` | `float` | Image-width radius |
|
|
131
|
+
| `irisOffset` | `vec2` | Position inside the eye, [-1, 1] |
|
|
132
|
+
| `irisConfidence` | `float` | 0..1, fade with it |
|
|
133
|
+
| `irisOpenness` | `float` | Eyelid gap / eye width |
|
|
134
|
+
| `time` | `float` | Seconds since mount |
|
|
135
|
+
| `color` | `vec3` | Tint |
|
|
136
|
+
| `opacity` | `float` | |
|
|
137
|
+
| `mirror` | `float` | 1 when mirrored |
|
|
138
|
+
| `tintImage` | `float` | 1 to multiply the image by `color` |
|
|
139
|
+
| `map` | `sampler2D` | The image in `image` mode |
|
|
140
|
+
|
|
141
|
+
A minimal custom material:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { IrisEffect, type IrisMaterialContext } from "@vincentt-xr/sdk/face-effects";
|
|
145
|
+
|
|
146
|
+
const ring = ({ uniforms }: IrisMaterialContext) =>
|
|
147
|
+
new THREE.ShaderMaterial({
|
|
148
|
+
uniforms, // the shared, live uniforms
|
|
149
|
+
vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
|
|
150
|
+
fragmentShader: `
|
|
151
|
+
uniform float irisConfidence; uniform vec3 color; uniform float opacity; varying vec2 vUv;
|
|
152
|
+
void main(){
|
|
153
|
+
float r = length(vUv - 0.5) * 2.0;
|
|
154
|
+
float ring = smoothstep(0.6, 0.7, r) * (1.0 - smoothstep(0.9, 1.0, r));
|
|
155
|
+
gl_FragColor = vec4(color, ring * opacity * irisConfidence);
|
|
156
|
+
#include <colorspace_fragment>
|
|
157
|
+
}`,
|
|
158
|
+
transparent: true,
|
|
159
|
+
depthTest: false,
|
|
160
|
+
depthWrite: false,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
<IrisEffect material={ring} color="#ffcc00" />;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The factory runs once per eye for each `material` identity; the effect
|
|
167
|
+
disposes what a factory created when the factory changes or on unmount,
|
|
168
|
+
while an instance you pass stays yours. Read the live texture from
|
|
169
|
+
`uniforms.map`: `context.texture` is only the texture known when the factory
|
|
170
|
+
ran, often null. The built-in
|
|
171
|
+
image shader shows it as authored, multiplies it by `color` only when
|
|
172
|
+
`tintImage` is on, and fades with confidence.
|
|
173
|
+
|
|
174
|
+
## When to use what
|
|
175
|
+
|
|
176
|
+
- **Iris tracking**: eye decorations, pupil overlays, iris recolouring,
|
|
177
|
+
shader inputs tied to where the iris sits in the eye.
|
|
178
|
+
- **Blendshapes** (`useFaceInfo` / `useFaceAction`): blinks, winks, squints,
|
|
179
|
+
eye-look-left/right/up/down categories as discrete expressions.
|
|
180
|
+
- **Gaze estimation**: not provided. `offset` is not gaze; a real estimate
|
|
181
|
+
needs head pose and eye geometry.
|
|
182
|
+
- **PinToMesh / FaceMesh materials**: content that must deform with the skin
|
|
183
|
+
around the eye rather than sit on the iris.
|
|
184
|
+
|
|
185
|
+
## Hiding the iris behind the eyelids
|
|
186
|
+
|
|
187
|
+
The quads are flat and draw over the lids. To clip them to the eye openings
|
|
188
|
+
without a dedicated mask, draw the face mesh over them with the camera feed
|
|
189
|
+
and no eye-opening triangles (the example's "eyelid mask" toggle). The
|
|
190
|
+
material samples the feed through the mesh's live `faceUv` attribute with
|
|
191
|
+
depth testing off, so it always lands on top of the quads:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { CanonicalFaceMeshRenderer, IrisEffect } from "@vincentt-xr/sdk/face-effects";
|
|
195
|
+
import { useXRCameraTexture } from "@vincentt-xr/sdk/low-level";
|
|
196
|
+
|
|
197
|
+
const feed = new THREE.ShaderMaterial({
|
|
198
|
+
uniforms: { cameraTexture: { value: useXRCameraTexture() } },
|
|
199
|
+
vertexShader: `attribute vec2 faceUv; varying vec2 vUv;
|
|
200
|
+
void main(){ vUv = faceUv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
|
|
201
|
+
fragmentShader: `uniform sampler2D cameraTexture; varying vec2 vUv;
|
|
202
|
+
void main(){ gl_FragColor = texture2D(cameraTexture, vUv); #include <colorspace_fragment> }`,
|
|
203
|
+
transparent: true, // the quads are transparent; an opaque mask would draw before them
|
|
204
|
+
depthTest: false,
|
|
205
|
+
depthWrite: false,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// IrisEffect renders at order 100; the mask draws after it.
|
|
209
|
+
<IrisEffect />;
|
|
210
|
+
<CanonicalFaceMeshRenderer
|
|
211
|
+
showEyeGeometry={false}
|
|
212
|
+
showEyeMask={false}
|
|
213
|
+
renderOrder={101}
|
|
214
|
+
materialOverride={feed}
|
|
215
|
+
/>;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Memoize the material on the camera texture and dispose it on unmount, as
|
|
219
|
+
the example does.
|
|
220
|
+
|
|
221
|
+
## Known limitations
|
|
222
|
+
|
|
223
|
+
- Single-person per `faceIndex`; multi-face scenes mount one effect per face.
|
|
224
|
+
- Iris points are present even for closed eyes; `confidence` is the gate.
|
|
225
|
+
- The quads sit on the z=0 plane without eyeball curvature or head pose.
|
|
226
|
+
- Hold replays the last position; with `predictMs` > 0 a sharp reversal can
|
|
227
|
+
overshoot for up to that window.
|
|
228
|
+
|
|
229
|
+
## Manual validation
|
|
230
|
+
|
|
231
|
+
Run `/iris-tracking`:
|
|
232
|
+
|
|
233
|
+
| Case | Steps | Expect |
|
|
234
|
+
| -------------------- | --------------------------------------- | ------------------------------------------------------------------ |
|
|
235
|
+
| Shader material | default, change color / scale / opacity | Both irises recoloured live; pupil follows the iris offset |
|
|
236
|
+
| Image material | mode `image`, presets or a URL | Image covers each iris, tinted by color; mirror flips it |
|
|
237
|
+
| Custom material | "custom shader (ring)" | Pulsing ring from the shared uniforms |
|
|
238
|
+
| Tracking loss | cover the face / "Person Shaking Head" | Effect holds ~250 ms then disappears; no stale eyes |
|
|
239
|
+
| Multi-face | "Two People Smiling", face index 0 / 1 | Effect moves to the selected face |
|
|
240
|
+
| Mirrored camera | webcam | Left/right stay anatomical; the eye on the viewer's left is `left` |
|
|
241
|
+
| Portrait / landscape | resize the pane | Quads stay centred on the irises at the right size |
|
|
242
|
+
| Closed eyes | blink | Effect fades with confidence |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Multi-Subject Tracking Policy"
|
|
3
|
+
description: "The identity and association contract for faces, hands, bodies and holistic results: what an index means, how many subjects each tracker reports, how to select one, what happens on loss and reappearance, and how subjects are matched across trackers."
|
|
4
|
+
sidebarTitle: "Multi-Subject Policy"
|
|
5
|
+
icon: "people-group"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Identity contract
|
|
9
|
+
|
|
10
|
+
- **An index is a result-array position, not an identity.** `faceIndex`,
|
|
11
|
+
`handIndex`, `getFace(i)`, `getLandmarks2D(i)` read position `i` of the
|
|
12
|
+
current frame's result. The order is whatever MediaPipe emits; it can
|
|
13
|
+
change between frames when subjects move, overlap, or one is lost. Nothing
|
|
14
|
+
in the SDK promises that `faceIndex: 1` is the same person two frames
|
|
15
|
+
later.
|
|
16
|
+
- **No learned identity.** The SDK never recognises people. The only
|
|
17
|
+
continuity it offers is spatial: the `sticky` selection below follows the
|
|
18
|
+
subject nearest to the previously selected one.
|
|
19
|
+
- **Single-subject APIs are unchanged.** `useFaceInfo()`, `<FaceTracker>`,
|
|
20
|
+
`<HandTracker>`, `<TrackingAnchor>` keep reading index 0 by default; the
|
|
21
|
+
policy is an opt-in option on top.
|
|
22
|
+
|
|
23
|
+
## Maximum subjects
|
|
24
|
+
|
|
25
|
+
| Tracker | Max | Default | Option |
|
|
26
|
+
| -------- | --- | ------- | --------------------------------------------------------------------------------------------- |
|
|
27
|
+
| face | 10 | 1 | `numFaces` on `<FaceTracker>` / `useFaceInfo(s)`; the first acquirer of the shared model wins |
|
|
28
|
+
| hand | 2 | 2 | none (two hands, `handedness` labels them) |
|
|
29
|
+
| gesture | 1 | 1 | none |
|
|
30
|
+
| body | 1 | 1 | none |
|
|
31
|
+
| holistic | 1 | 1 | none (single person by design) |
|
|
32
|
+
|
|
33
|
+
`XR_MAX_SUBJECTS` carries this table. Every consumer that needs more than one
|
|
34
|
+
face must ask for it; a consumer that asks for fewer than the first acquirer
|
|
35
|
+
gets the larger count.
|
|
36
|
+
|
|
37
|
+
## Selecting one subject
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { useFaceInfo, selectSubject, faceCandidates } from "@vincentt-xr/sdk/tracking";
|
|
41
|
+
|
|
42
|
+
// Hook form (faces): pick by policy instead of a fixed index.
|
|
43
|
+
const face = useFaceInfo({ numFaces: 4, subject: { selection: "largest" } });
|
|
44
|
+
const follow = useFaceInfo({ numFaces: 4, subject: { selection: "sticky", stickyRadius: 0.2 } });
|
|
45
|
+
|
|
46
|
+
// Pure form (any tracker): build candidates, select, carry the memory.
|
|
47
|
+
const result = selectSubject(faceCandidates(results.faceLandmarks), policy, previousMemory);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| `selection` | Picks |
|
|
51
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `index` | position `policy.index` (default 0) — the historical behaviour |
|
|
53
|
+
| `largest` | the biggest subject (closest to the camera) |
|
|
54
|
+
| `nearest-center` | closest to the feed centre |
|
|
55
|
+
| `leftmost` / `rightmost` | by feed x, as shown in the preview |
|
|
56
|
+
| `sticky` | nearest to the previously selected subject within `stickyRadius` (feed units, default 0.2); same `label` (handedness) preferred; otherwise `fallback` (default `largest`) |
|
|
57
|
+
|
|
58
|
+
`selectSubject` returns `{ index, reason, memory }`; `index` is `null` when
|
|
59
|
+
there is no candidate. `faceCandidates(landmarkArrays)` and
|
|
60
|
+
`jointCandidates(handsOrBodies)` build candidates (feed-space centre and
|
|
61
|
+
size) from face landmarks or tracker-space joints.
|
|
62
|
+
|
|
63
|
+
## Loss and reappearance
|
|
64
|
+
|
|
65
|
+
- A subject that disappears for a few frames is bridged by the tracker's
|
|
66
|
+
hold (`holdMs`, 250 ms by default, through `FaceTrackMemory` / the readers'
|
|
67
|
+
hold): the last result is replayed, then dropped.
|
|
68
|
+
- `sticky` keeps its memory through a miss, so a subject that reappears
|
|
69
|
+
near where it was is re-selected; one that reappears elsewhere is a new
|
|
70
|
+
subject and the policy's `fallback` decides. When the selection switches
|
|
71
|
+
to the fallback, the memory moves with it (a deliberate switch, not a jump
|
|
72
|
+
back later).
|
|
73
|
+
- Reappearance never restores a previous index: read the policy's pick, not
|
|
74
|
+
a remembered position.
|
|
75
|
+
|
|
76
|
+
## Association across trackers
|
|
77
|
+
|
|
78
|
+
`associateSubjects(primary, secondary, maxDistance = 0.35)` greedily matches
|
|
79
|
+
the nearest centres between two candidate lists (face ↔ body, hand ↔ body),
|
|
80
|
+
each subject at most once, ignoring pairs farther than `maxDistance` (feed
|
|
81
|
+
units). That proximity-in-the-same-frame rule is the whole association
|
|
82
|
+
contract; with the single-person body / holistic models it reduces to
|
|
83
|
+
"the body belongs to the nearest face".
|
|
84
|
+
|
|
85
|
+
## Manual validation
|
|
86
|
+
|
|
87
|
+
Run `/multi-subject` (starts on the two-people image, `numFaces` 4):
|
|
88
|
+
|
|
89
|
+
| Case | Steps | Expect |
|
|
90
|
+
| ---------- | ----------------------------------------------- | --------------------------------------------------- |
|
|
91
|
+
| Candidates | two / three people images | one grey dot per face, count in the status line |
|
|
92
|
+
| Rules | largest / nearest-center / leftmost / rightmost | the cyan ring moves to the matching face |
|
|
93
|
+
| Index | `index` with 0..3 | ring on that array position, none when out of range |
|
|
94
|
+
| Sticky | `sticky` on "Two People Smiling" video | the ring stays on the same person as they move |
|
|
95
|
+
| Loss | cover a face (webcam) | ring holds ~250 ms, then follows the fallback |
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Person Cutout"
|
|
3
|
+
description: "Background replacement as a two-layer ScreenImage composition: any background media below, the person-masked live camera above."
|
|
4
|
+
sidebarTitle: "Person Cutout"
|
|
5
|
+
icon: "user"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`PersonCutout` is the live camera, masked to the person by portrait
|
|
9
|
+
segmentation, rendered as a full-screen `ScreenImage`. Put any background
|
|
10
|
+
surface behind it with a lower `renderOrder` and you have background
|
|
11
|
+
replacement without assembling masks, materials or coordinates yourself.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { FULL_SCREEN_ANCHORS, PersonCutout, ScreenImage, ScreenSpaceUI } from "@vincentt-xr/sdk";
|
|
15
|
+
|
|
16
|
+
<ScreenSpaceUI>
|
|
17
|
+
{/* 1. Background: image, video texture or GIF — anything ScreenImage shows. */}
|
|
18
|
+
<ScreenImage
|
|
19
|
+
src="/backgrounds/beach.jpg"
|
|
20
|
+
fit="cover"
|
|
21
|
+
anchors={FULL_SCREEN_ANCHORS}
|
|
22
|
+
renderOrder={0}
|
|
23
|
+
/>
|
|
24
|
+
{/* 2. Foreground: the camera, person only. */}
|
|
25
|
+
<PersonCutout renderOrder={100} />
|
|
26
|
+
</ScreenSpaceUI>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## The composition contract
|
|
30
|
+
|
|
31
|
+
- **Background** is any full-screen surface drawn first: a `ScreenImage`
|
|
32
|
+
(`{ kind: "image" }`, `{ kind: "gif" }`, or `{ kind: "texture" }` with a
|
|
33
|
+
`THREE.VideoTexture`), or a custom shader plane inside
|
|
34
|
+
`<ScreenTransform anchors={FULL_SCREEN_ANCHORS}>`. Give it a `renderOrder`
|
|
35
|
+
below the cut-out's (default 100).
|
|
36
|
+
- **Foreground** is `PersonCutout`: a `ScreenImage` whose `source` is the
|
|
37
|
+
session camera texture and whose `maskTexture` is the portrait segmentation
|
|
38
|
+
mask. It accepts the usual `ScreenImage` props (`opacity`, `fit`, `anchors`,
|
|
39
|
+
`blendMode`, `flipX`, `renderOrder`, `visible`) except `source` and the mask
|
|
40
|
+
props, which it owns.
|
|
41
|
+
- **Ordering** is `renderOrder`; both layers are overlay-style screen planes
|
|
42
|
+
with depth testing off, so only the order matters.
|
|
43
|
+
|
|
44
|
+
## Alignment: aspect ratio, mirroring, resolution
|
|
45
|
+
|
|
46
|
+
- The camera texture is already cover-cropped to the viewport and mirrored
|
|
47
|
+
for webcams by the session, and the segmentation mask is produced in that
|
|
48
|
+
same feed space. `PersonCutout` samples the mask through the image's own
|
|
49
|
+
fit transform (`fit="cover"` by default), so mask and camera stay aligned
|
|
50
|
+
under any aspect ratio and any further crop or flip you apply.
|
|
51
|
+
- Background media is not mirrored. If a background has to match a mirrored
|
|
52
|
+
webcam (a sign you are sitting in front of), set `flipX` on that
|
|
53
|
+
`ScreenImage`.
|
|
54
|
+
- The mask is a render-size canvas texture with a half-pixel blur, so edges
|
|
55
|
+
are soft at the display resolution. The segmentation `quality` option on
|
|
56
|
+
the model trades edge detail for cost.
|
|
57
|
+
|
|
58
|
+
## Mask, opacity, invert
|
|
59
|
+
|
|
60
|
+
The SDK mask carries its signal in RGB: white (255) on the person, black on
|
|
61
|
+
the background, with alpha 255 everywhere (three's `alphaMap` reads the green
|
|
62
|
+
channel, which is how `VideoBackground` consumes the same texture). The
|
|
63
|
+
cut-out reads `maskChannel="green"`, keeping the person and dropping the
|
|
64
|
+
background. `invert` flips that to show only the background with the person
|
|
65
|
+
cut out.
|
|
66
|
+
`opacity` fades the whole person layer. `usePersonMaskTexture()` returns the
|
|
67
|
+
raw mask texture with its `status` for custom compositions, and `ScreenImage`
|
|
68
|
+
itself now accepts `maskTexture`, `maskChannel` (`alpha` | `red` | `green` |
|
|
69
|
+
`luminance`), `maskInvert`, `maskFeather` and `maskErode` for any image.
|
|
70
|
+
|
|
71
|
+
## Edge quality: feather and erode
|
|
72
|
+
|
|
73
|
+
The segmenter's mask is hard-edged and sits a texel or two outside the person,
|
|
74
|
+
so the raw cut-out carries a fringe of the source background (green-screen
|
|
75
|
+
spill, a light halo on dark backgrounds). Two props clean it up:
|
|
76
|
+
|
|
77
|
+
- `maskFeather` — radius in mask texels of a 3×3 blur over the mask. `0` keeps
|
|
78
|
+
the hard edge. Texel units keep the softness the same on screen whatever
|
|
79
|
+
resolution the mask arrives at.
|
|
80
|
+
- `maskErode` — `0..1` of the feather width to shrink the kept region by. The
|
|
81
|
+
blurred edge is re-thresholded, so the silhouette moves inward and the fringe
|
|
82
|
+
falls outside it. It does nothing without feather.
|
|
83
|
+
|
|
84
|
+
`PersonCutout` defaults to `maskFeather={4}` and `maskErode={0.7}`, chosen on
|
|
85
|
+
the SDK's green-screen clips with the 720-px portrait mask. Raise erode when
|
|
86
|
+
spill remains, lower it when fingers or hair tips disappear. Neither prop fixes
|
|
87
|
+
the mask trailing a fast-moving arm; that is the segmenter's temporal lag.
|
|
88
|
+
|
|
89
|
+
## Loading and fallback
|
|
90
|
+
|
|
91
|
+
The segmentation model downloads on first use. `PersonCutout` reports
|
|
92
|
+
`loading` → `ready` | `error` through `onStatusChange`. While not ready it
|
|
93
|
+
follows `fallback`: `"camera"` (default) draws the unmasked camera so the
|
|
94
|
+
experience degrades to a plain feed, `"hidden"` draws nothing so only the
|
|
95
|
+
background shows. On error the same fallback applies and `error` carries
|
|
96
|
+
the reason.
|
|
97
|
+
|
|
98
|
+
## When to use this instead of 3D
|
|
99
|
+
|
|
100
|
+
- **Use this 2D path** for background replacement, virtual backgrounds,
|
|
101
|
+
person-in-a-scene stickers, and any effect that only needs "the person
|
|
102
|
+
versus the rest of the frame". It is one quad, one mask, no geometry.
|
|
103
|
+
- **Use the face mesh** when something must stick to the face surface with
|
|
104
|
+
correct perspective and shading (lipstick, face paint, UV-mapped textures).
|
|
105
|
+
- **Use the head occluder** when 3D objects must disappear behind the head
|
|
106
|
+
(glasses arms, hats seen from the side). A 2D mask cannot occlude in depth.
|
|
107
|
+
- The 2D mask also covers the body, hair and clothes, which neither the face
|
|
108
|
+
mesh nor the head occluder does.
|
|
109
|
+
|
|
110
|
+
## Performance
|
|
111
|
+
|
|
112
|
+
One extra full-screen quad with a masked shader; the segmentation model
|
|
113
|
+
runs at its own `targetFps` (24 by default). Share the model: `VideoBackground`
|
|
114
|
+
with `segmentation="portrait"`, `usePersonMask` overlays and `PersonCutout`
|
|
115
|
+
all consume the same instance.
|
|
116
|
+
|
|
117
|
+
## Manual visual test plan
|
|
118
|
+
|
|
119
|
+
Run `/person-cutout` in the playground:
|
|
120
|
+
|
|
121
|
+
| Case | Source / background | Expect |
|
|
122
|
+
| ------------------------ | --------------------------------------- | ------------------------------------------------------------------------------- |
|
|
123
|
+
| Image background | video source, Image | Person on the photo; no camera background visible around the edges |
|
|
124
|
+
| Video background | video source, Video | Background plays behind the person; both layers keep playing |
|
|
125
|
+
| GIF background | video source, GIF | GIF animates behind the person |
|
|
126
|
+
| Shader background | video source, Shader | Animated plane behind the person; person edges unaffected |
|
|
127
|
+
| Opacity | any, opacity 0.5 | Person fades over the background; background unchanged |
|
|
128
|
+
| Show background only | any, invert | Person region shows the background through the camera layer |
|
|
129
|
+
| Edge feather / erode | video source, feather 0 vs 4, erode 0.7 | Feather 0 shows a green fringe on sleeves; the defaults remove it |
|
|
130
|
+
| Aspect / orientation | resize the pane, rotate device | Person stays aligned with the camera; background re-covers the screen |
|
|
131
|
+
| Mirrored webcam | webcam | Person mirrored like the feed; text on a background not mirrored unless `flipX` |
|
|
132
|
+
| Movement | "Person Shaking Head", "Person ThumbUp" | Mask follows head and hands; thin fingers may soften |
|
|
133
|
+
| Lighting | webcam, bright vs dim | Mask holds under both; dark clothes on dark background is the hard case |
|
|
134
|
+
| Loading, fallback camera | reload | Plain camera until `ready`, then the background appears |
|
|
135
|
+
| Loading, fallback hidden | reload | Only the background until `ready` |
|
|
136
|
+
| Multi-person | "Two People Smiling" | Both people kept; the segmenter is person-class |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Product Placement AR"
|
|
3
|
+
description: "View-in-your-space AR for product pages on top of Google's model-viewer: capability detection, the ProductViewer wrapper, the useProductAr hook, and the camera handoff with the tracking session."
|
|
4
|
+
sidebarTitle: "Product AR"
|
|
5
|
+
icon: "cube"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Placement AR puts a product on the floor or a wall through the device's
|
|
9
|
+
native path: WebXR (Android Chrome), Scene Viewer (Android) or Quick Look
|
|
10
|
+
(iOS, from a USDZ). It is a different mode from the SDK's camera session,
|
|
11
|
+
which has no world tracking. The SDK wraps Google's `<model-viewer>` for it
|
|
12
|
+
rather than rebuilding it; the rule for every page: **placement AR is
|
|
13
|
+
model-viewer, face/body AR is the session, never both at once.**
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { ProductViewer, useProductAr } from "@vincentt-xr/sdk/product";
|
|
17
|
+
|
|
18
|
+
const Product = () => {
|
|
19
|
+
const ar = useProductAr();
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<ProductViewer
|
|
23
|
+
src="/hat.glb"
|
|
24
|
+
iosSrc="/hat.usdz"
|
|
25
|
+
poster="/hat.webp"
|
|
26
|
+
alt="Hat"
|
|
27
|
+
onReady={ar.bind}
|
|
28
|
+
/>
|
|
29
|
+
{ar.supportedModes.length > 0 && (
|
|
30
|
+
<button onClick={() => ar.activate()} disabled={!ar.canActivate}>
|
|
31
|
+
View in your space
|
|
32
|
+
</button>
|
|
33
|
+
)}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
`@google/model-viewer` is an optional peer dependency: add it to the app
|
|
42
|
+
that renders products. `<ProductViewer>` imports it lazily on first render,
|
|
43
|
+
so other pages never pay for it. It bundles its own three.js (it cannot share
|
|
44
|
+
the SDK's), roughly 1 MB; keep it on product routes only.
|
|
45
|
+
|
|
46
|
+
## Capability
|
|
47
|
+
|
|
48
|
+
`detectXRCapabilities` / `useXRCapabilities` report `placement`:
|
|
49
|
+
|
|
50
|
+
| Field | How it is detected |
|
|
51
|
+
| ------------- | ----------------------------------------------------------------------------------- |
|
|
52
|
+
| `webxr` | `navigator.xr.isSessionSupported("immersive-ar")` |
|
|
53
|
+
| `sceneViewer` | Android Chrome / Samsung Internet user agent |
|
|
54
|
+
| `quickLook` | `<a rel="ar">` support (iOS Safari and WebKit browsers) |
|
|
55
|
+
| `any` | one of the above; otherwise `reason.code` `placement-unsupported` (not recoverable) |
|
|
56
|
+
|
|
57
|
+
Offer "View in AR" only when `any` is true; desktop shows the viewer alone.
|
|
58
|
+
|
|
59
|
+
## `<ProductViewer>`
|
|
60
|
+
|
|
61
|
+
| Prop | model-viewer attribute |
|
|
62
|
+
| -------------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
63
|
+
| `src`, `iosSrc`, `poster`, `alt` | `src`, `ios-src`, `poster`, `alt` |
|
|
64
|
+
| `ar` (default true), `arModes` | `ar`, `ar-modes` (preference order) |
|
|
65
|
+
| `placement: "floor" \| "wall"` | `ar-placement` |
|
|
66
|
+
| `scale: "auto" \| "fixed"` | `ar-scale` (`fixed` = true-to-size, no pinch) |
|
|
67
|
+
| `cameraControls`, `autoRotate`, `loading`, `environmentImage`, `shadowIntensity`, `exposure` | same names, kebab-cased |
|
|
68
|
+
| `attributes` | anything else, passed straight through |
|
|
69
|
+
| `onLoad`, `onError`, `onArStatus`, `onReady(element)` | `load`, `error`, `ar-status` events; the element |
|
|
70
|
+
|
|
71
|
+
Children are slotted into model-viewer (`slot="ar-button"`, `slot="poster"`,
|
|
72
|
+
hotspots).
|
|
73
|
+
|
|
74
|
+
## `useProductAr()`
|
|
75
|
+
|
|
76
|
+
Returns `{ status, supportedModes, canActivate, reason, loading, error, bind, activate, handleStatus }`.
|
|
77
|
+
`status` is model-viewer's `ar-status` (`not-presenting`, `session-started`,
|
|
78
|
+
`object-placed`, `failed`) or `unavailable` when no mode is supported.
|
|
79
|
+
`activate()` must run from a user gesture.
|
|
80
|
+
|
|
81
|
+
### Camera handoff
|
|
82
|
+
|
|
83
|
+
Only WebXR runs in the page and needs the camera the tracking session
|
|
84
|
+
holds; Scene Viewer and Quick Look open a native activity and never touch
|
|
85
|
+
it. When the hook runs under an `<XRProvider>` whose session is `running` or
|
|
86
|
+
`paused` on the webcam and the mode about to be used is `webxr`,
|
|
87
|
+
`activate()` stops the session first and restores the same facing and
|
|
88
|
+
lifecycle (a paused session comes back paused) when `ar-status` returns to
|
|
89
|
+
`not-presenting` (or `failed`). `handoffCamera: false` opts out. Stream /
|
|
90
|
+
canvas sources are left alone. The pure form is
|
|
91
|
+
`releaseSessionForAr(session, mode)`. Keep `arModes` on the viewer and
|
|
92
|
+
`modes` on the hook identical; both default to `DEFAULT_PRODUCT_AR_MODES`.
|
|
93
|
+
|
|
94
|
+
## Assets
|
|
95
|
+
|
|
96
|
+
- GLB for every platform; keep it under a few MB, Draco / KTX2 compressed.
|
|
97
|
+
- USDZ for iOS Quick Look via `iosSrc`; without it model-viewer exports one
|
|
98
|
+
on the fly (slower, lower fidelity). The GLB → USDZ step is a publishing
|
|
99
|
+
step (see the asset pipeline issue), never runtime code.
|
|
100
|
+
- Host both on the CDN with long cache headers.
|
|
101
|
+
|
|
102
|
+
## Manual validation
|
|
103
|
+
|
|
104
|
+
Run `/product-ar`:
|
|
105
|
+
|
|
106
|
+
| Device | Expect |
|
|
107
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
108
|
+
| Desktop | viewer with orbit; `supported modes: none`, button disabled |
|
|
109
|
+
| Android Chrome | `scene-viewer` (and `webxr` on ARCore devices); tap → Scene Viewer / WebXR places the astronaut; status log `session-started` → `object-placed` → `not-presenting` |
|
|
110
|
+
| iPhone | `quick-look`; tap → Quick Look opens the USDZ |
|
|
111
|
+
| With a tracking session running | the session reads `stopped` during AR and `running` after |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Product Variants, Hotspots & Dimensions"
|
|
3
|
+
description: "Material variants (colour / size), surface-anchored hotspots with serializable authoring data, and true-size dimension labels on a ProductViewer."
|
|
4
|
+
sidebarTitle: "Product Details"
|
|
5
|
+
icon: "tags"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Builds on `<ProductViewer>` (Product Placement AR). Three details a product
|
|
9
|
+
page needs on the same model: switching looks, pointing at parts, and
|
|
10
|
+
telling the real size.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import {
|
|
14
|
+
ProductViewer,
|
|
15
|
+
ProductHotspot,
|
|
16
|
+
ProductDimensions,
|
|
17
|
+
useProductVariants,
|
|
18
|
+
useProductHotspots,
|
|
19
|
+
useProductDimensions,
|
|
20
|
+
type ProductViewerElement,
|
|
21
|
+
} from "@vincentt-xr/sdk/product";
|
|
22
|
+
|
|
23
|
+
const [el, setEl] = useState<ProductViewerElement | null>(null);
|
|
24
|
+
const variants = useProductVariants(el); // { variants, variant, setVariant }
|
|
25
|
+
const dimensions = useProductDimensions(el); // { size: [w, h, d], center } in metres, null until loaded
|
|
26
|
+
const hotspots = useProductHotspots(el, { initial: fromCms, onChange: save });
|
|
27
|
+
|
|
28
|
+
<ProductViewer src="/shoe.glb" scale="fixed" onReady={(e) => setEl(e)}>
|
|
29
|
+
{hotspots.hotspots.map((h) => (
|
|
30
|
+
<ProductHotspot key={h.id} hotspot={h} onClick={open} />
|
|
31
|
+
))}
|
|
32
|
+
<ProductDimensions dimensions={dimensions} unit="metric" />
|
|
33
|
+
</ProductViewer>;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Variants
|
|
37
|
+
|
|
38
|
+
`useProductVariants(element)` reads the model's `KHR_materials_variants`
|
|
39
|
+
after load and sets `variantName` on the element. Variants change materials
|
|
40
|
+
only, so they carry into AR wherever the platform renders the glTF (WebXR,
|
|
41
|
+
Scene Viewer). Quick Look renders the USDZ as exported: ship one USDZ per
|
|
42
|
+
variant and switch `iosSrc` with the variant, or accept the default look.
|
|
43
|
+
|
|
44
|
+
## Hotspots
|
|
45
|
+
|
|
46
|
+
`ProductHotspotData = { id, position: [x, y, z], normal: [x, y, z], label?, description? }`
|
|
47
|
+
in model metres; serializable and validated (`parseHotspots`,
|
|
48
|
+
`serializeHotspots`). `<ProductHotspot>` renders a slotted callout model-viewer
|
|
49
|
+
keeps over the surface as the camera orbits, fading it when the surface
|
|
50
|
+
faces away.
|
|
51
|
+
|
|
52
|
+
`useProductHotspots(element, { initial, onChange })` is the authoring and
|
|
53
|
+
runtime state: `placeFromPointer(clientX, clientY, label)` raycasts the
|
|
54
|
+
click onto the model (`positionAndNormalFromPoint`), `add` / `update` /
|
|
55
|
+
`remove` / `clear`, `json` / `importJson`. The debug-ui
|
|
56
|
+
`<ProductHotspotPanel>` wraps it with click-to-place, label editing and a
|
|
57
|
+
JSON copy, so authored data goes back into the app as `initial`.
|
|
58
|
+
|
|
59
|
+
## Dimensions
|
|
60
|
+
|
|
61
|
+
`useProductDimensions(element)` returns the bounding box (`getDimensions`,
|
|
62
|
+
`getBoundingBoxCenter`) after load. `<ProductDimensions>` labels width,
|
|
63
|
+
height and depth on three box edges, anchored as hotspots (`dimensionLines`
|
|
64
|
+
computes the edges); `unit` is `metric` (cm under 1 m) or `imperial`.
|
|
65
|
+
Pair with `scale="fixed"` on the viewer so AR places the model at that
|
|
66
|
+
size with no pinch-to-resize.
|
|
67
|
+
|
|
68
|
+
## Manual validation
|
|
69
|
+
|
|
70
|
+
Run `/product-details` (model-viewer's variants shoe):
|
|
71
|
+
|
|
72
|
+
| Case | Steps | Expect |
|
|
73
|
+
| ---------- | ----------------------------------- | -------------------------------------------------------------- |
|
|
74
|
+
| Variants | tap the colourway buttons | the shoe changes material; the panel select follows |
|
|
75
|
+
| Hotspots | "place hotspot", click the shoe | a labelled dot stays on that point while orbiting |
|
|
76
|
+
| Authoring | edit the label, "copy JSON", reload | label persists; JSON matches the panel |
|
|
77
|
+
| Dimensions | toggle, switch unit | W / H / D labels on the box edges, cm ↔ in |
|
|
78
|
+
| AR (phone) | "View in your space" | true-size placement; variant carried into Scene Viewer / WebXR |
|