@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.
Files changed (287) hide show
  1. package/GROUNDING.md +1188 -14
  2. package/dist/{DebuggerUI-C_rbLdkh.js → DebuggerUI-C-ldiozL.js} +1 -1
  3. package/dist/{DebuggerUI-DYT_dk1E.cjs → DebuggerUI-CWX5EtQv.cjs} +1 -1
  4. package/dist/FaceMeshRenderer-BEEXZwS6.cjs +158 -0
  5. package/dist/FaceMeshRenderer-UuH5sTXG.js +1322 -0
  6. package/dist/FaceRetouch-C7h7x9kd.js +683 -0
  7. package/dist/FaceRetouch-DqftT26Y.cjs +134 -0
  8. package/dist/GLTFLoader-C5q63sKO.js +1633 -0
  9. package/dist/GLTFLoader-Cb_CRwmG.cjs +1 -0
  10. package/dist/SegmentationOverlay-DQuW3dsc.js +133 -0
  11. package/dist/SegmentationOverlay-aWVod8Kl.cjs +1 -0
  12. package/dist/assets/cache.d.ts +99 -0
  13. package/dist/assets/estimate.d.ts +11 -0
  14. package/dist/assets/index.d.ts +7 -0
  15. package/dist/assets/loaders.d.ts +28 -0
  16. package/dist/assets/react.d.ts +33 -0
  17. package/dist/calibration/calibration.d.ts +70 -0
  18. package/dist/calibration/index.d.ts +6 -0
  19. package/dist/calibration/storage.d.ts +13 -0
  20. package/dist/calibration/types.d.ts +53 -0
  21. package/dist/calibration/useXRCalibration.d.ts +46 -0
  22. package/dist/camera/CameraPermissionGate.d.ts +26 -0
  23. package/dist/camera/devices.d.ts +26 -0
  24. package/dist/camera/index.d.ts +8 -0
  25. package/dist/camera/permission.d.ts +86 -0
  26. package/dist/camera/useCameraPermission.d.ts +35 -0
  27. package/dist/capabilities/detect.d.ts +45 -0
  28. package/dist/capabilities/index.d.ts +5 -0
  29. package/dist/capabilities/types.d.ts +120 -0
  30. package/dist/capabilities/useXRCapabilities.d.ts +18 -0
  31. package/dist/compat/environment.d.ts +5 -0
  32. package/dist/compat/index.d.ts +4 -0
  33. package/dist/compat/run.d.ts +18 -0
  34. package/dist/compat/scenarios.d.ts +10 -0
  35. package/dist/compat/types.d.ts +101 -0
  36. package/dist/components/BodyTracker/index.d.ts +9 -0
  37. package/dist/components/FaceMesh/FaceMeshRenderer.d.ts +11 -3
  38. package/dist/components/FaceMesh/canonical/CanonicalFaceMeshRenderer.d.ts +41 -5
  39. package/dist/components/FaceMesh/canonical/parseCanonicalFaceModel.d.ts +12 -0
  40. package/dist/components/FaceMesh/canonical/shaders.d.ts +1 -1
  41. package/dist/components/FaceMesh/index.d.ts +2 -2
  42. package/dist/components/GestureListener/index.d.ts +11 -0
  43. package/dist/components/HandTracker/index.d.ts +10 -0
  44. package/dist/components/HolisticTracker/index.d.ts +69 -0
  45. package/dist/components/HolisticTracker/settings.d.ts +58 -0
  46. package/dist/components/HolisticTracker/signals.d.ts +49 -0
  47. package/dist/components/IrisEffect/IrisMaterial.d.ts +73 -0
  48. package/dist/components/IrisEffect/index.d.ts +51 -0
  49. package/dist/components/IrisEffect/settings.d.ts +117 -0
  50. package/dist/components/LipEffect/LipEffect.d.ts +7 -1
  51. package/dist/components/LipEffect/LipEffectMaterial.d.ts +43 -7
  52. package/dist/components/LipEffect/LipEffects.d.ts +9 -0
  53. package/dist/components/LipEffect/defaults.d.ts +27 -4
  54. package/dist/components/LipEffect/index.d.ts +11 -3
  55. package/dist/components/LipEffect/settings.d.ts +271 -0
  56. package/dist/components/LipEffect/types.d.ts +56 -9
  57. package/dist/components/LipEffect/useLipDesignTextures.d.ts +11 -0
  58. package/dist/components/LipEffect/uvGuide.d.ts +59 -0
  59. package/dist/components/PersonCutout/index.d.ts +50 -0
  60. package/dist/components/PersonCutout/personCutout.d.ts +16 -0
  61. package/dist/components/SceneObjectRenderer/index.d.ts +9 -2
  62. package/dist/components/ScreenImage/components/ScreenImageBody.d.ts +1 -1
  63. package/dist/components/ScreenImage/index.d.ts +3 -1
  64. package/dist/components/ScreenImage/materials/ScreenImageBlendMaterial.d.ts +1 -1
  65. package/dist/components/ScreenImage/materials/ScreenImageMaskMaterial.d.ts +11 -0
  66. package/dist/components/ScreenImage/source.d.ts +6 -0
  67. package/dist/components/ScreenImage/types.d.ts +18 -0
  68. package/dist/components/SegmentationOverlay/SegmentationOverlay.d.ts +37 -0
  69. package/dist/components/SegmentationOverlay/SegmentationOverlayMaterial.d.ts +31 -0
  70. package/dist/components/SegmentationOverlay/index.d.ts +9 -0
  71. package/dist/components/SegmentationOverlay/maskTexture.d.ts +23 -0
  72. package/dist/components/SegmentationOverlay/providers.d.ts +72 -0
  73. package/dist/components/SegmentationOverlay/settings.d.ts +65 -0
  74. package/dist/components/XRMediaSurface/index.d.ts +2 -1
  75. package/dist/components/XRProvider/index.d.ts +16 -2
  76. package/dist/components/XRViewport/hooks/useViewportExternalFaces.d.ts +1 -1
  77. package/dist/components/XRViewport/hooks/useViewportMediaSource.d.ts +1 -1
  78. package/dist/components/XRViewport/hooks/useViewportPipeline.d.ts +1 -1
  79. package/dist/components/index.d.ts +3 -0
  80. package/dist/constants/index.d.ts +4 -1
  81. package/dist/contexts-CO0sd4JQ.cjs +1 -0
  82. package/dist/{contexts-DvQI4PqI.js → contexts-FzlnnMYD.js} +252 -246
  83. package/dist/coordinates/feed.d.ts +52 -0
  84. package/dist/coordinates/index.d.ts +5 -0
  85. package/dist/coordinates/spaces.d.ts +49 -0
  86. package/dist/coordinates/viewport.d.ts +22 -0
  87. package/dist/coordinates/world.d.ts +15 -0
  88. package/dist/debug-ui/gesture-settings/index.d.ts +14 -0
  89. package/dist/debug-ui/holistic-tracker-settings/index.d.ts +16 -0
  90. package/dist/debug-ui/index.d.ts +14 -0
  91. package/dist/debug-ui/iris-effect-settings/index.d.ts +12 -0
  92. package/dist/debug-ui/lip-effect-settings/index.d.ts +15 -0
  93. package/dist/debug-ui/media-source.cjs +1 -1
  94. package/dist/debug-ui/media-source.js +1 -1
  95. package/dist/debug-ui/product-hotspots/index.d.ts +24 -0
  96. package/dist/debug-ui/segmentation-overlay-settings/index.d.ts +12 -0
  97. package/dist/debug-ui/xr-diagnostics/index.d.ts +25 -0
  98. package/dist/debug-ui.cjs +3 -3
  99. package/dist/debug-ui.js +2057 -1687
  100. package/dist/detector-K8aej4MB.js +59 -0
  101. package/dist/detector-ijypGtse.cjs +1 -0
  102. package/dist/entries/core.d.ts +21 -3
  103. package/dist/entries/face-effects.d.ts +12 -4
  104. package/dist/entries/low-level.d.ts +22 -3
  105. package/dist/entries/product.d.ts +11 -0
  106. package/dist/entries/tracking.d.ts +26 -1
  107. package/dist/face-effects.cjs +155 -1
  108. package/dist/face-effects.js +1134 -2196
  109. package/dist/gestures/definitions.d.ts +85 -0
  110. package/dist/gestures/detector.d.ts +87 -0
  111. package/dist/gestures/index.d.ts +8 -0
  112. package/dist/gestures/measures.d.ts +101 -0
  113. package/dist/gestures/settings.d.ts +49 -0
  114. package/dist/{getFaceInfo-B43Cwq4n.js → getFaceInfo-BkZfaQs-.js} +5 -5
  115. package/dist/{getFaceInfo-DEzcL_Ub.cjs → getFaceInfo-ahPxQFGu.cjs} +1 -1
  116. package/dist/hooks/selectors.d.ts +68 -1
  117. package/dist/hooks/useCalibrationFaceTracker.d.ts +1 -1
  118. package/dist/hooks/useFaceInfo.d.ts +13 -1
  119. package/dist/hooks/useFaceInfos.d.ts +1 -1
  120. package/dist/hooks/useGesture.d.ts +32 -0
  121. package/dist/hooks/useIrisTracking.d.ts +45 -0
  122. package/dist/hooks/useXRDiagnostics.d.ts +81 -0
  123. package/dist/hooks/useXRModel.d.ts +7 -0
  124. package/dist/hooks/useXRSession/facade.d.ts +44 -2
  125. package/dist/hooks/useXRSession/index.d.ts +2 -2
  126. package/dist/hooks/useXRSession/reason.d.ts +3 -1
  127. package/dist/hooks/useXRSession/registry.d.ts +14 -0
  128. package/dist/hooks/useXRSession/store.d.ts +52 -3
  129. package/dist/index--RgGzZ6X.cjs +47 -0
  130. package/dist/{index-uE3sKczj.js → index-CHVbH0hz.js} +4 -4
  131. package/dist/index-CeVkgsag.cjs +1 -0
  132. package/dist/index-CnGox6Xr.js +3054 -0
  133. package/dist/{index-B-s4YbWu.js → index-CsUdQ-pn.js} +113 -111
  134. package/dist/{index-CY7Z8cHV.cjs → index-DKBAh6hV.cjs} +1 -1
  135. package/dist/index-DsZwTfG0.cjs +1 -0
  136. package/dist/index-Dw6tj2Mi.js +144 -0
  137. package/dist/index-OCPjOaeP.cjs +1 -0
  138. package/dist/{index-G_5h_FJQ.js → index-s9pF4zkG.js} +999 -1014
  139. package/dist/internal-BYza1h8_.cjs +1 -0
  140. package/dist/{internal-ldPYiUF0.js → internal-vsfNQycj.js} +15 -16
  141. package/dist/internal.cjs +1 -1
  142. package/dist/internal.js +2 -2
  143. package/dist/iris/compute.d.ts +25 -0
  144. package/dist/iris/index.d.ts +4 -0
  145. package/dist/iris/lead.d.ts +19 -0
  146. package/dist/iris/types.d.ts +87 -0
  147. package/dist/landmarkMask-CbTFzBYG.js +224 -0
  148. package/dist/landmarkMask-u7es2k4X.cjs +1 -0
  149. package/dist/low-level.cjs +1 -1
  150. package/dist/low-level.js +1104 -639
  151. package/dist/main.cjs +1 -1
  152. package/dist/main.js +1519 -462
  153. package/dist/measures-9dQPdw96.js +154 -0
  154. package/dist/measures-KfJzpvod.cjs +1 -0
  155. package/dist/models/Base/index.d.ts +49 -0
  156. package/dist/models/Body/index.d.ts +1 -1
  157. package/dist/models/Face/index.d.ts +1 -1
  158. package/dist/models/Hand/GestureTracking.d.ts +1 -1
  159. package/dist/models/Hand/HandTracking.d.ts +1 -1
  160. package/dist/models/Holistic/index.d.ts +49 -0
  161. package/dist/models/Holistic/result.d.ts +68 -0
  162. package/dist/models/SemanticSegmentation/index.d.ts +31 -0
  163. package/dist/models/SemanticSegmentation/presets.d.ts +28 -0
  164. package/dist/models/index.d.ts +2 -0
  165. package/dist/product/ProductDimensions.d.ts +16 -0
  166. package/dist/product/ProductHotspot.d.ts +16 -0
  167. package/dist/product/ProductViewer.d.ts +9 -0
  168. package/dist/product/details.d.ts +53 -0
  169. package/dist/product/index.d.ts +14 -0
  170. package/dist/product/loadModelViewer.d.ts +4 -0
  171. package/dist/product/types.d.ts +71 -0
  172. package/dist/product/useProductAr.d.ts +37 -0
  173. package/dist/product/useProductDetails.d.ts +60 -0
  174. package/dist/product.cjs +1 -0
  175. package/dist/product.js +414 -0
  176. package/dist/providers-C0Dlhh9Z.js +365 -0
  177. package/dist/providers-YNwkmNeK.cjs +62 -0
  178. package/dist/quality/XRQualityController.d.ts +25 -0
  179. package/dist/quality/controller.d.ts +33 -0
  180. package/dist/quality/index.d.ts +6 -0
  181. package/dist/quality/types.d.ts +100 -0
  182. package/dist/quality/useXRQuality.d.ts +28 -0
  183. package/dist/{renderGroups-CFt1yYLB.js → renderGroups-CN1cGFUo.js} +1 -1
  184. package/dist/{renderGroups-BJsx32q5.cjs → renderGroups-O4KmaMl9.cjs} +1 -1
  185. package/dist/renderPasses-Cg-zqmEz.cjs +21 -0
  186. package/dist/renderPasses-NnKZVfBy.js +1460 -0
  187. package/dist/scene-object.cjs +1 -1
  188. package/dist/scene-object.js +2 -2
  189. package/dist/screen2d/coordinates.d.ts +20 -0
  190. package/dist/screenTransform-BenfCtZt.js +44 -0
  191. package/dist/screenTransform-BiXdpVf0.cjs +1 -0
  192. package/dist/segmentation-9PhSvc-R.cjs +1 -0
  193. package/dist/segmentation-CeOTKemr.js +5 -0
  194. package/dist/segmentation2d/faceMemory.d.ts +28 -0
  195. package/dist/segmentation2d/faceRegions.d.ts +24 -0
  196. package/dist/segmentation2d/index.d.ts +9 -0
  197. package/dist/segmentation2d/maskUv.d.ts +23 -0
  198. package/dist/segmentation2d/rasterize.d.ts +14 -0
  199. package/dist/segmentation2d/types.d.ts +46 -0
  200. package/dist/selectors-C5G1PMYL.js +195 -0
  201. package/dist/selectors-Du_PsVnW.cjs +1 -0
  202. package/dist/{session-CANvoLD6.cjs → session-BBr0yN8h.cjs} +7 -7
  203. package/dist/{session-Dhi0OqZo.js → session-Cy0m7l7-.js} +76 -76
  204. package/dist/settings-DRR7-snV.cjs +1 -0
  205. package/dist/settings-aQk845NJ.js +73 -0
  206. package/dist/subjects/index.d.ts +2 -0
  207. package/dist/subjects/select.d.ts +62 -0
  208. package/dist/subjects/types.d.ts +55 -0
  209. package/dist/tracking/coordinates.d.ts +5 -0
  210. package/dist/tracking-config/apply.d.ts +57 -0
  211. package/dist/tracking-config/index.d.ts +7 -0
  212. package/dist/tracking-config/resolve.d.ts +25 -0
  213. package/dist/tracking-config/storage.d.ts +8 -0
  214. package/dist/tracking-config/types.d.ts +106 -0
  215. package/dist/tracking-config/useXRTrackingConfig.d.ts +38 -0
  216. package/dist/tracking.cjs +1 -1
  217. package/dist/tracking.js +1056 -380
  218. package/dist/types/common.d.ts +11 -1
  219. package/dist/types/compositor.d.ts +21 -0
  220. package/dist/types/gesture-tracking.d.ts +2 -2
  221. package/dist/types/hand-tracking.d.ts +3 -3
  222. package/dist/types/holistic.d.ts +121 -0
  223. package/dist/types/index.d.ts +1 -0
  224. package/dist/types/model.d.ts +22 -2
  225. package/dist/types/nodes.d.ts +29 -4
  226. package/dist/types/segmentation.d.ts +19 -0
  227. package/dist/types/session.d.ts +48 -4
  228. package/dist/useFaceInfo-Blf7FCu3.js +303 -0
  229. package/dist/useFaceInfo-C5mO8NQY.cjs +1 -0
  230. package/dist/useXRCapabilities-B1R2ghy-.cjs +1 -0
  231. package/dist/useXRCapabilities-C68v__wB.js +30 -0
  232. package/dist/utils/fns.d.ts +6 -3
  233. package/dist/utils/integrity.d.ts +4 -1
  234. package/dist/utils/updateThrottle.d.ts +2 -0
  235. package/dist/utils/useStableUniforms.d.ts +18 -0
  236. package/docs/SDK_DOCUMENTATION.md +3 -0
  237. package/docs/api/adaptive-quality.mdx +104 -0
  238. package/docs/api/asset-lifecycle.mdx +78 -0
  239. package/docs/api/camera-permission.mdx +142 -0
  240. package/docs/api/capture-recording.mdx +9 -1
  241. package/docs/api/coordinate-spaces.mdx +165 -0
  242. package/docs/api/cross-camera.mdx +88 -0
  243. package/docs/api/diagnostics-calibration.mdx +97 -0
  244. package/docs/api/face-effects.mdx +6 -0
  245. package/docs/api/face-mesh.mdx +35 -0
  246. package/docs/api/gestures.mdx +173 -0
  247. package/docs/api/holistic-tracker.mdx +159 -0
  248. package/docs/api/iris-tracking.mdx +242 -0
  249. package/docs/api/multi-subject.mdx +95 -0
  250. package/docs/api/person-cutout.mdx +136 -0
  251. package/docs/api/product-ar.mdx +111 -0
  252. package/docs/api/product-details.mdx +78 -0
  253. package/docs/api/runtime-3d.mdx +5 -0
  254. package/docs/api/screen-image.mdx +3 -1
  255. package/docs/api/screen-space-coordinates.mdx +35 -1
  256. package/docs/api/segmentation-overlay.mdx +301 -0
  257. package/docs/api/session-lifecycle.mdx +136 -0
  258. package/docs/api/trackers.mdx +18 -0
  259. package/docs/api/tracking-config.mdx +108 -0
  260. package/docs/docs.json +23 -1
  261. package/docs/examples/lip-shaders.mdx +224 -0
  262. package/docs/guides/lip-effect-textures.mdx +292 -0
  263. package/docs/guides/lip-uv-template.png +0 -0
  264. package/docs/guides/lip-uv-template.svg +1 -0
  265. package/package.json +16 -1
  266. package/dist/FaceMeshRenderer-D1WsrTLA.cjs +0 -155
  267. package/dist/FaceMeshRenderer-DSCd1H55.js +0 -1264
  268. package/dist/FaceRetouch-CMJbGThL.js +0 -551
  269. package/dist/FaceRetouch-DsE0Dbw-.cjs +0 -134
  270. package/dist/contexts-h-9TRH0r.cjs +0 -1
  271. package/dist/index-C0-fg1Hk.cjs +0 -1
  272. package/dist/index-CxMEeCRP.cjs +0 -1
  273. package/dist/index-D67hHm44.cjs +0 -1
  274. package/dist/index-DPPpU5By.cjs +0 -17
  275. package/dist/index-DYfY9ljy.js +0 -111
  276. package/dist/index-I4eF5TKT.js +0 -1457
  277. package/dist/internal-MvKrz0Nt.cjs +0 -1
  278. package/dist/renderLayers-lDtL0_43.cjs +0 -1
  279. package/dist/renderLayers-zrK2pv5O.js +0 -13
  280. package/dist/renderPasses-C72MbZ-a.js +0 -1500
  281. package/dist/renderPasses-D-Krl6D0.cjs +0 -21
  282. package/dist/selectors-ByKNTXeA.js +0 -59
  283. package/dist/selectors-CAwmL9VW.cjs +0 -1
  284. package/dist/useFaceInfo-BSZq7AO1.cjs +0 -1
  285. package/dist/useFaceInfo-FVQBBNdY.js +0 -62
  286. package/dist/useFaceResults-C3oZkn2Z.cjs +0 -1
  287. package/dist/useFaceResults-D62STzT1.js +0 -19
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: "Camera Permission & Privacy"
3
+ description: "Distinguish unknown, requesting, granted, denied, blocked and unavailable camera states; request, retry and recover without remounting; release the camera cleanly; keep every frame on the device until the viewer allows access."
4
+ sidebarTitle: "Camera Permission"
5
+ icon: "camera"
6
+ ---
7
+
8
+ Camera access is the first thing that can go wrong in an AR experience, and
9
+ the browser's `NotAllowedError` hides four different situations. The SDK
10
+ splits them into states a creator can act on, offers the request flow as a
11
+ hook and a gate component, and guarantees no camera frame is processed
12
+ before explicit, usable access.
13
+
14
+ ## States
15
+
16
+ | State | Meaning | Next step |
17
+ | ------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------- |
18
+ | `unknown` | Never asked (the next request prompts), or the browser cannot say. | Show a primer and a request button. |
19
+ | `requesting` | A prompt is open. | Wait. |
20
+ | `granted` | Usable. | Mount the session. |
21
+ | `denied` | This prompt was declined or dismissed; the next request prompts again. | Offer "try again". |
22
+ | `blocked` | Persistently denied in the site settings; a request fails without a prompt. | Explain how to change the setting. |
23
+ | `unavailable` | No camera, insecure context, unsupported browser, or held by another app (`reason`). | Fall back; `reason.recoverable` says if a retry can help. |
24
+
25
+ `denied` and `blocked` are told apart by reading the Permissions API _after_
26
+ the failed request: `denied` there means blocked. `reason` is an
27
+ `XRFallbackReason` (`camera-permission-denied`, `camera-in-use`,
28
+ `no-camera`, `camera-insecure-context`, `camera-unsupported`, `camera-lost`).
29
+
30
+ ## Before the session: the gate
31
+
32
+ ```tsx
33
+ import { CameraPermissionGate, useCameraPermission } from "@vincentt-xr/sdk";
34
+
35
+ // Component form: children render only while access is granted.
36
+ <CameraPermissionGate
37
+ fallback={({ state, reason, canPrompt, request }) => (
38
+ <Primer state={state} reason={reason} onAllow={canPrompt ? () => request() : undefined} />
39
+ )}
40
+ >
41
+ <XRProvider>
42
+ <Experience />
43
+ </XRProvider>
44
+ </CameraPermissionGate>;
45
+
46
+ // Hook form, for your own layout.
47
+ const { state, reason, canPrompt, usable, request, refresh } = useCameraPermission();
48
+ ```
49
+
50
+ Neither needs an `<XRProvider>`. On mount they only _query_ the permission
51
+ (no prompt); `request()` prompts, and must run from a user gesture (a
52
+ button) as browsers require. The probe stream is stopped at once, so the
53
+ camera indicator goes off again and nothing is captured: the call only
54
+ moves the permission. Pass `auto` to prompt on mount. Both follow the
55
+ site-setting changes through the Permissions API, so a viewer who allows
56
+ the camera in the address bar flips the gate without a reload.
57
+
58
+ The built-in fallback of `<CameraPermissionGate>` announces state changes
59
+ with `aria-live="polite"`, keeps the request behind a real `<button>`,
60
+ disables it when a prompt cannot help, and labels the region "Camera access".
61
+ Keep those in a custom fallback: a visible button, `aria-live` on the
62
+ status text, `aria-disabled` when blocked, and the explanation for
63
+ `blocked` next to the button rather than in a toast.
64
+
65
+ ## Inside the session
66
+
67
+ The session reports its own live state through `useXRSessionCameraPermission()`
68
+ (`@vincentt-xr/sdk/low-level`): `requesting` while its webcam
69
+ `getUserMedia` is pending, `granted` once bound, the classified failure
70
+ otherwise, and `blocked` / `unavailable` after a mid-session revoke or a
71
+ lost camera. Stream and canvas sources leave it `unknown`.
72
+
73
+ ```tsx
74
+ const { state, reason } = useXRSessionCameraPermission();
75
+ ```
76
+
77
+ ### Request, retry, recover without remounting
78
+
79
+ ```tsx
80
+ const { session } = useXRContext();
81
+ const retry = async () => {
82
+ await session.setMediaSource({ source: XRMediaSource.WEBCAM }); // prompts again if allowed
83
+ await session.start();
84
+ };
85
+ ```
86
+
87
+ A failed bind leaves the session where it was (`idle` stays idle, a running
88
+ session keeps its old source), so the same provider, models and scene are
89
+ reused. A revoke mid-session stops the session with a `camera-lost` /
90
+ `permission-denied` error; the same two calls recover it.
91
+
92
+ ### Privacy: releasing the camera
93
+
94
+ - `session.stop()` stops every media track: the browser's camera indicator
95
+ goes off and no frame is read. Models stay initialized, so a later
96
+ `setMediaSource` + `start` is fast. Use this for an in-app "privacy mode".
97
+ - A revoke (`permission-denied`) or a lost device (`camera-lost`) releases
98
+ the tracks once and suspends every model automatically.
99
+ - No model runs before `session.start()`, and `start()` needs a bound
100
+ source, so no camera data is processed before explicit access. The gate
101
+ adds the second guarantee: the session is not even mounted until
102
+ `granted`.
103
+ - Frames never leave the device: inference runs in the page, and the only
104
+ network traffic is the one-time model and engine download.
105
+
106
+ ## Choosing the camera
107
+
108
+ `setMediaSource({ source: XRMediaSource.WEBCAM, facingMode: "environment" })`
109
+ opens the rear camera; `"user"` (default) the front one; `deviceId` picks a
110
+ specific camera from `enumerateDevices()`. Binding again with a different
111
+ facing swaps the camera mid-session. The front camera is mirrored
112
+ (`sourceKind` `"webcam"`), the rear camera is drawn straight (`sourceKind`
113
+ `"rear-camera"`), and `mirrored: true | false` overrides that for a camera
114
+ whose facing is unknown. Trackers and the coordinate toolkit follow the
115
+ source kind, so content stays aligned either way. `useXRCameraFacing()`
116
+ (low-level) reports the bound facing. The full cross-camera and orientation
117
+ contract lands with the next sprint item.
118
+
119
+ ## Low-level helpers
120
+
121
+ `queryCameraPermission()`, `requestCameraAccess({ keepStream, constraints })`,
122
+ `subscribeCameraPermission(listener)` and `canPromptForCamera(state)` are
123
+ plain functions (core door) for non-React code; `CAMERA_PERMISSION_MESSAGES`
124
+ holds the default copy per state. `classifyCameraFailure(reason,
125
+ permissionState)` and `mapDomExceptionToReason(err)` live in `/low-level`
126
+ next to `XRErrorReason`, for callers that classify their own
127
+ `getUserMedia` failure.
128
+
129
+ ## Manual validation
130
+
131
+ Run `/camera-permission`:
132
+
133
+ | Case | Steps | Expect |
134
+ | ------------ | ----------------------------------------- | ----------------------------------------------------------------- |
135
+ | First visit | load | gate `unknown`, no prompt until "Allow camera" |
136
+ | Allow | click Allow, accept | `requesting` → `granted`; the experience mounts and binds |
137
+ | Dismiss | click Allow, close the prompt | `denied`, "Try again" enabled |
138
+ | Block | deny with "never" / site setting | `blocked`, button disabled, settings hint shown |
139
+ | Unblock | allow in the address bar | gate flips to `granted` without reload |
140
+ | Revoke | "simulate revoke" (or revoke in settings) | session `blocked` / `unavailable`, lifecycle `stopped` |
141
+ | Recover | "retry camera" | prompt if needed, lifecycle `running` |
142
+ | Privacy mode | "privacy mode (stop camera)" | camera indicator off, lifecycle `stopped`; "retry camera" resumes |
@@ -33,6 +33,12 @@ after exposing a failure through `status`, `error`, and `onError`. For
33
33
  synchronous low-level code, `session.captureFrame()` remains available through
34
34
  `useXRContext` from `@vincentt-xr/sdk/low-level`.
35
35
 
36
+ The SDK captures the active composition owner: an `XRScene` canvas when one is
37
+ mounted, otherwise the `XRViewport` DOM media canvas. For viewport-managed
38
+ media, source selection, cover/contain layout, and mirror state are composed
39
+ once before display, tracking, and capture. Arbitrary DOM HUD elements are not
40
+ part of canvas capture.
41
+
36
42
  | Returned field | Meaning |
37
43
  | ---------------- | ------------------------------------------------------------ |
38
44
  | `status` | `idle`, `capturing`, `completed`, or `error` |
@@ -53,7 +59,9 @@ an existing audio stream.
53
59
 
54
60
  ```tsx
55
61
  import { useMediaRecorder } from "@vincentt-xr/sdk";
62
+ import { useXRContext } from "@vincentt-xr/sdk/low-level";
56
63
 
64
+ const { session } = useXRContext();
57
65
  const recorder = useMediaRecorder({
58
66
  audio: true,
59
67
  onRecordingStart: () => console.log("recording"),
@@ -63,7 +71,7 @@ const recorder = useMediaRecorder({
63
71
  onRecordingComplete: uploadVideo,
64
72
  onError: reportError,
65
73
  });
66
- await recorder.start(canvas.captureStream(30));
74
+ await recorder.start(session.captureStream(30));
67
75
  recorder.pause();
68
76
  recorder.resume();
69
77
  recorder.stop();
@@ -0,0 +1,165 @@
1
+ ---
2
+ title: "Coordinate Spaces"
3
+ description: "One toolkit for source, feed, tracker, viewport, screen and world coordinates: cover crop, zoom, mirroring, orientation, and documented conversions."
4
+ sidebarTitle: "Coordinate Spaces"
5
+ icon: "arrows-to-dot"
6
+ ---
7
+
8
+ Every tracking consumer meets the same six spaces. Import the conversions from
9
+ `@vincentt-xr/sdk`; each one performs its origin, axis, crop and mirror change
10
+ exactly once, so never chain a manual flip or crop after it.
11
+
12
+ ## The spaces
13
+
14
+ ```
15
+ source (media px) feed (0..1) viewport (CSS px)
16
+ ┌──────────────────┐ ┌────────┐ ┌────────┐
17
+ │ raw video/image │ cover│ │ identity │ │
18
+ │ 1280 × 720 │ crop │ 720 × │ per axis │ 390 × │
19
+ │ never mirrored │──────▶ 1280 │◀────────────────▶│ 844 │
20
+ └──────────────────┘ zoom │ webcam │ │ │
21
+ mirror│ mirror │ └────────┘
22
+ └───┬────┘ │
23
+ │ x·2−1, 1−y·2 │ design canvas
24
+ ▼ ▼
25
+ tracker (−1..1, Y up) screen (720×1280 design px, Y up)
26
+ │
27
+ │ camera ray → z = 0 plane
28
+ ▼
29
+ world (three.js)
30
+ ```
31
+
32
+ | Space | Origin / axes | Who produces it |
33
+ | ------------ | ---------------------------------------------- | -------------------------------------------------------------- |
34
+ | **source** | top-left, Y down, media pixels, never mirrored | the `<video>` / `<img>` the session was given |
35
+ | **feed** | top-left, Y down, 0..1 | the camera texture; every landmark and SDK mask |
36
+ | **tracker** | center, X right, Y up, Z toward the viewer | hand, body, holistic nodes (`TrackerPoint`) |
37
+ | **viewport** | top-left, Y down, CSS pixels | the rendered canvas, pointer events via `clientToScreenPixels` |
38
+ | **screen** | center, Y up, 720×1280 design pixels | `ScreenSpaceUI`, `ScreenImage`, labels |
39
+ | **world** | three.js world; tracked content on z = 0 | `TrackingAnchor`, face mesh, iris quads |
40
+
41
+ The feed is the source **cover-cropped to the render size**, enlarged by
42
+ `perf.cameraZoom` (default 1.2) about the centre, and **mirrored for the
43
+ webcam only**. Image and video sources are drawn straight. Because the feed
44
+ fills the viewport, feed and viewport agree per axis; a landmark at feed
45
+ (0.5, 0.5) is the middle of the canvas on every aspect ratio.
46
+
47
+ ## The feed crop, once
48
+
49
+ `FeedCrop` is the single description of source → feed. The session's blit
50
+ loop draws with it, `computeMaskUvTransform` fits source-space masks with it,
51
+ and the point conversions use it, so what the trackers saw and what you
52
+ compute agree.
53
+
54
+ ```ts
55
+ import { computeFeedCrop, feedToSource, sourceToFeed } from "@vincentt-xr/sdk";
56
+ import { useXRFeedCrop } from "@vincentt-xr/sdk/low-level";
57
+
58
+ // Inside the provider: the live crop (null until media is bound).
59
+ const crop = useXRFeedCrop();
60
+
61
+ // Or build one yourself for an offline pipeline.
62
+ const offline = computeFeedCrop({
63
+ source: { width: 1280, height: 720 },
64
+ feed: { width: 720, height: 1280 },
65
+ zoom: 1.2,
66
+ mirrored: true, // webcam
67
+ });
68
+
69
+ // A landmark back onto the raw frame (for a crop, a server call, an overlay on the <video>).
70
+ const px = feedToSource({ x: 0.62, y: 0.4 }, crop); // { x, y } in source pixels
71
+ // A detection made on the raw frame into landmark space.
72
+ const fp = sourceToFeed({ x: 800, y: 300 }, crop); // { x, y } in 0..1
73
+ ```
74
+
75
+ `feedToSource` can return points outside the source when the feed shows
76
+ less than the frame (zoom > 1 never does; a contain-shaped source does);
77
+ `isFeedPointInSource` tells you.
78
+
79
+ ## Mirroring
80
+
81
+ - Positions never need a flip: landmarks already live in the (possibly
82
+ mirrored) feed, and the feed is what you see.
83
+ - **Direction signs** do. Holistic hand labels and the iris `offset.x` are
84
+ corrected with `isMirroredSourceKind(sourceKind)` so "the person's left"
85
+ means the same thing on every source. Use the same helper, never
86
+ `sourceKind === "webcam"` inline.
87
+ - `mirrorFeedX` flips a feed point's x (for a direction vector, or to compare
88
+ a selfie feed with a straight recording).
89
+
90
+ ## Camera orientation
91
+
92
+ A frame delivered sideways (a native capture, a rotated file) must be
93
+ rotated before it becomes the source. `orientSourcePoint(point, size,
94
+ orientation)` and `orientedSize(size, orientation)` move a point and a frame
95
+ size through a clockwise 0 / 90 / 180 / 270 rotation; apply them before
96
+ `sourceToFeed`. The browser already orients webcam frames, so the session
97
+ does not rotate.
98
+
99
+ ## Conversions
100
+
101
+ | From → to | Helper |
102
+ | ---------------------- | ------------------------------------------------------------------- |
103
+ | source ↔ feed | `sourceToFeed`, `feedToSource` (with a `FeedCrop`) |
104
+ | feed ↔ tracker | `feedToTracker`, `trackerToFeed` |
105
+ | feed ↔ viewport | `feedToViewport`, `viewportToFeed` (viewport size) |
106
+ | feed ↔ screen | `feedToScreenPixels`, `screenPixelsToFeed` |
107
+ | feed / tracker → world | `feedToWorld`, `trackerToWorld` (camera, optional `planeZ`) |
108
+ | world → feed / tracker | `worldToFeed`, `worldToTracker` (`z` is NDC depth) |
109
+ | media layout | `computeFitLayout(source, destination, "cover" \| "contain", zoom)` |
110
+
111
+ The screen-space helpers (`normalizedToScreenPixels`, `worldToScreenPixels`,
112
+ `screenPointToRay`, `mediaToScreenPixels` …) are unchanged and documented in
113
+ [Screen-Space Coordinates](/api/screen-space-coordinates); a feed point is a
114
+ "normalized viewport point" there.
115
+
116
+ ### Worked examples
117
+
118
+ **A hand joint to a DOM overlay**
119
+
120
+ ```ts
121
+ const joint = handNode.hands?.[0].coordinates?.[8]; // tracker space
122
+ const feed = trackerToFeed(joint); // 0..1
123
+ const css = feedToViewport(feed, { width: el.clientWidth, height: el.clientHeight });
124
+ badge.style.transform = `translate(${css.x}px, ${css.y}px)`;
125
+ ```
126
+
127
+ **A face landmark into the 3D scene** (what `TrackingAnchor` does)
128
+
129
+ ```ts
130
+ useFrame(() => {
131
+ const tip = face.landmarks[1]; // feed space, from useFaceInfo
132
+ feedToWorld(tip, camera, mesh.position); // onto the z = 0 tracking plane
133
+ });
134
+ ```
135
+
136
+ **A tap to the raw frame** (send the touched pixel to a server)
137
+
138
+ ```ts
139
+ const screen = clientToScreenPixels({ point: { x: e.clientX, y: e.clientY }, rect });
140
+ const feed = screenPixelsToFeed(screen, viewportSize);
141
+ const sourcePx = feedToSource(feed, useXRFeedCrop()!);
142
+ ```
143
+
144
+ **A mask made on the raw video**
145
+
146
+ ```ts
147
+ computeMaskUvTransform({
148
+ space: "source",
149
+ maskWidth: 1280,
150
+ maskHeight: 720,
151
+ feedWidth,
152
+ feedHeight,
153
+ zoom: 1.2, // the session's perf.cameraZoom
154
+ mirrored: true, // webcam
155
+ });
156
+ ```
157
+
158
+ ## Regression coverage
159
+
160
+ `lib/coordinates/coordinates.test.ts` pins: the cover crop with zoom against
161
+ the blit loop's rectangle for landscape→portrait, portrait→landscape and 4:3
162
+ pairs; webcam mirroring (source left edge → feed right edge, round trip);
163
+ feed/tracker/viewport/screen round trips and agreement with
164
+ `trackerPointToScreenPixels`; feed→world depth independence and plane
165
+ selection; and the mask fit matching the feed crop, zoom included.
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: "Cross-Camera & Orientation"
3
+ description: "Front / rear camera selection and switching, device enumeration, explicit mirror policy for preview and tracking, and orientation-safe alignment."
4
+ sidebarTitle: "Cross-Camera"
5
+ icon: "camera-rotate"
6
+ ---
7
+
8
+ ## Choosing and switching cameras
9
+
10
+ ```tsx
11
+ import { XRMediaSource, listCameras, findCameraByFacing } from "@vincentt-xr/sdk";
12
+
13
+ // Bind: front (default) or rear, or a specific device.
14
+ await session.setMediaSource({ source: XRMediaSource.WEBCAM, facingMode: "environment" });
15
+ await session.setMediaSource({ source: XRMediaSource.WEBCAM, deviceId });
16
+
17
+ // Switch on a live session (models, scene and lifecycle stay; a bound or
18
+ // stopped session is started). Resolves to the facing the track reports.
19
+ const facing = await session.switchCamera({ facingMode: "environment" });
20
+
21
+ // Enumerate (labels appear once permission is granted).
22
+ const cameras = await listCameras(); // [{ deviceId, label, facing: "user" | "environment" | null }]
23
+ const rear = findCameraByFacing(cameras, "environment");
24
+ ```
25
+
26
+ `facingMode` is a soft hint: a laptop with one camera answers `environment`
27
+ with that camera, and `cameraFacing` then reports what the track says.
28
+ `deviceId` is exact and beats the hint. The same request again is a no-op;
29
+ a different facing, device or mirror policy releases the old stream and
30
+ tracks once and binds the new camera under the same source canvas, so every
31
+ model keeps its input binding. `useXRCameraFacing()` and
32
+ `useXRCameraDeviceId()` (low-level) read the result.
33
+
34
+ ## Mirror policy
35
+
36
+ There is exactly one mirror decision per session and it is keyed on the
37
+ source kind:
38
+
39
+ | Source | `sourceKind` | Mirrored |
40
+ | ---------------------------- | ----------------- | -------- |
41
+ | front camera (`user`) | `webcam` | yes |
42
+ | rear camera (`environment`) | `rear-camera` | no |
43
+ | image / video stream, canvas | `image` / `video` | no |
44
+
45
+ The kind follows the _effective_ facing (what the track reports), so a
46
+ one-camera laptop that answers an `environment` request with its front
47
+ camera is still mirrored; `mirrored: true | false` on the bind pins it
48
+ either way. Preview and tracking read the same feed, so a landmark
49
+ unprojected to the scene lands where the preview shows it on every camera.
50
+ Consumers never flip positions; `useXRMirrored()` (low-level, the same
51
+ answer as `isMirroredSourceKind(sourceKind)`) is for _direction_ semantics
52
+ only, such as which hand is the person's left. Every tracker and the
53
+ coordinate toolkit already key off the kind.
54
+
55
+ ## Orientation
56
+
57
+ `useXROrientation()` (low-level) reports `{ type: "portrait" | "landscape",
58
+ angle }` from `screen.orientation` (viewport aspect as the fallback). On a
59
+ change the session re-reads the live video size, so `useXRFeedCrop()` and
60
+ everything built on the coordinate toolkit re-align without waiting for the
61
+ next frame; the blit loop also resyncs the source size every frame. Content
62
+ anchored to landmarks (`TrackingAnchor`, `feedToWorld`, …) therefore stays
63
+ on the face through a rotation and a camera switch. Use
64
+ `calibration.orientation` only for sources the browser does not rotate
65
+ (a raw sideways capture).
66
+
67
+ ## Lifecycle guarantees
68
+
69
+ - Switching releases the previous stream exactly once (`ended` from the
70
+ replaced tracks is ignored), keeps models initialized, and keeps
71
+ `running` / `paused` as it was; `switchCamera` additionally starts a
72
+ `media-bound` / `stopped` session.
73
+ - A switch that fails (device unplugged, in use) leaves the session where it
74
+ was with the error published; call again or pick another device.
75
+ - `session.dispose()` (provider unmount) removes the orientation listeners.
76
+
77
+ ## Manual validation
78
+
79
+ Run `/camera-switching` on a phone:
80
+
81
+ | Case | Steps | Expect |
82
+ | ------------------- | ----------------------------------- | ---------------------------------------------------------------------- |
83
+ | Front camera | load | `front · mirrored`; the cyan dot sits on the nose |
84
+ | Rear camera | "front → rear" | `rear · straight`; text in view reads correctly; dot still on the nose |
85
+ | Device list | after permission | labelled cameras; tapping one opens it (device id shown) |
86
+ | Mirror override | mirror = straight, "apply mirror" | front feed un-mirrored; dot still aligned |
87
+ | Rotate | turn the phone | orientation line flips, source size updates, dot stays aligned |
88
+ | Switch while paused | pause (session-health), switch here | stays paused; resume runs on the new camera |
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: "Diagnostics & Calibration"
3
+ description: "Opt-in tracking diagnostics (readiness, inference time and rate, dropped frames, confidence, errors) and a device calibration workflow for orientation, mirror, scale, face distance and body proportions."
4
+ sidebarTitle: "Diagnostics & Calibration"
5
+ icon: "gauge-high"
6
+ ---
7
+
8
+ ## Tracking diagnostics
9
+
10
+ ```tsx
11
+ import { useXRDiagnostics } from "@vincentt-xr/sdk/low-level";
12
+ import { XRDiagnosticsPanel } from "@vincentt-xr/sdk/debug-ui";
13
+
14
+ // Data (any component under <XRProvider>, outside the R3F canvas or inside):
15
+ const diagnostics = useXRDiagnostics({ intervalMs: 250, models: [XRModel.FACE_TRACKER] });
16
+ diagnostics.session.feedFps; // frames the session blits per second
17
+ diagnostics.models[0].state; // "unavailable" | "loading" | "ready" | "paused" | "error"
18
+ diagnostics.models[0].inferenceMs; // last inference, ms
19
+ diagnostics.models[0].inferenceFps; // inferences per second (~1 s window)
20
+ diagnostics.models[0].skippedFrames; // RAF ticks not analysed (throttle / busy)
21
+ diagnostics.models[0].detections; // faces / hands / bodies in the last inference
22
+ diagnostics.models[0].confidence; // primary-subject score, when the model has one
23
+
24
+ // Panel (debug UI, outside the canvas):
25
+ <XRDiagnosticsPanel models={[XRModel.FACE_TRACKER, XRModel.HAND_TRACKER]} />;
26
+ ```
27
+
28
+ The hook samples on an interval, never per frame, and only while a consumer
29
+ is mounted. Nothing is collected in production unless you render it, so it is
30
+ safe to ship behind a debug flag. The `models` list defaults to every
31
+ `XRModel`, so models nobody acquired show as `unavailable`.
32
+
33
+ Per-model fields come from the model instance (`getDiagnostics()` on every
34
+ `BaseXRModel`): inference timing is recorded around each MediaPipe call;
35
+ `detections` and `confidence` are what each model can report (face: count,
36
+ no score; hand / gesture: count and the handedness or gesture score; body /
37
+ holistic: count and mean landmark visibility; segmenters: 1 when a mask was
38
+ produced, no score). `targetFps` is the per-tracker cap when one was
39
+ requested. `owners` lists who holds the model and who wants it running.
40
+
41
+ Session fields mirror the store: lifecycle, ready, source and kind, camera
42
+ and feed sizes, feed fps and the session error. `inferenceMsTotal` sums the
43
+ last inference of every running model, a quick "how much ML per frame".
44
+
45
+ ## Calibration
46
+
47
+ Cameras differ in orientation, mirroring and field of view, and people stand
48
+ at different distances. The calibration workflow measures a device once,
49
+ persists it, and applies it only where you ask. **Raw tracker results are
50
+ never altered.**
51
+
52
+ ```tsx
53
+ import { useXRCalibration } from "@vincentt-xr/sdk/tracking";
54
+
55
+ const cal = useXRCalibration({ storageKey: "myapp.calibration" });
56
+
57
+ // Capture references while the person stands at their normal distance:
58
+ cal.calibrateFace(faceInfo.landmarks); // feed-space face landmarks
59
+ cal.calibrateBody(poseLandmarksInFeedSpace); // 33 pose landmarks, feed space
60
+
61
+ // Device transform:
62
+ cal.update({ orientation: 90, mirrored: "auto", scale: 1.1, offset: { x: 0, y: 0.02 } });
63
+
64
+ // Apply where it matters:
65
+ const nose = cal.applyToFeedPoint(faceInfo.landmarks[4]); // new point, input untouched
66
+ const distance = cal.faceDistance(faceInfo.landmarks); // 1 at reference, >1 closer
67
+ const bodyScale = cal.bodyScale(poseLandmarksInFeedSpace); // 1 at reference
68
+
69
+ cal.reset(); // defaults; clears the persisted value
70
+ ```
71
+
72
+ | Field | Meaning |
73
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
74
+ | `orientation` | `0 / 90 / 180 / 270`: clockwise rotation applied to feed points. |
75
+ | `mirrored` | `"auto"` follows the session (only the webcam is mirrored); `true`/`false` flip only when they disagree with the session. |
76
+ | `scale`, `offset` | Uniform scale about the feed centre, then a feed-unit nudge. |
77
+ | `face` | `faceWidth` (temples 234↔454) and `eyeDistance` (33↔263) in feed-width units, plus `capturedAt`. |
78
+ | `body` | `shoulderWidth` (11↔12), `hipWidth` (23↔24), `torsoLength` (mid-shoulder to mid-hip). |
79
+
80
+ The pure helpers (`applyCalibrationToFeedPoint`, `measureFaceCalibration`,
81
+ `measureBodyCalibration`, `estimateFaceDistance`, `estimateBodyScale`,
82
+ `updateCalibration`, `resetCalibration`, `loadCalibration`,
83
+ `saveCalibration`, `clearCalibration`) work without React. Persistence uses
84
+ `localStorage` under `vincentt.xr.calibration` by default, validates every
85
+ field on load, and drops blobs from another `version`.
86
+
87
+ ## Manual validation
88
+
89
+ Run `/session-health`:
90
+
91
+ | Case | Steps | Expect |
92
+ | ----------------- | ----------------------------- | ------------------------------------------------------------------------- |
93
+ | Diagnostics panel | load, pause / resume | face + body `ready`, inference ms / fps live; `paused` and 0 fps on pause |
94
+ | Unavailable model | untick "mount hand tracker" | hand row reads `unavailable`; tick again → `loading` → `ready` |
95
+ | Face calibration | "calibrate face", move closer | distance rises above 1; reload keeps the reference |
96
+ | Orientation | set 90° | calibrated nose point rotates; raw point unchanged |
97
+ | Reset | "reset" | defaults, distance `–`, storage cleared |
@@ -13,6 +13,12 @@ This section is split into focused pages:
13
13
  paint onto the live face mesh.
14
14
  - [Face Retouching](/api/face-retouching) for the legacy skin-retouch shader
15
15
  and its controls.
16
+ - [Segmentation Overlay](/api/segmentation-overlay) for 2D semantic region
17
+ layers (lips, eyes, brows, face skin, person, background) without 3D
18
+ geometry, and when to prefer them over the canonical mesh.
19
+ - [Lip Effect Textures](/guides/lip-effect-textures) for `LipEffect`: the
20
+ design-texture canvas contract, the generated lip UV template, mask modes,
21
+ and the custom material context.
16
22
 
17
23
  Use `@vincentt-xr/sdk/tracking` for trackers, `FaceInfo`, Face Actions, and the
18
24
  bare deforming `FaceMesh`. Use `@vincentt-xr/sdk/face-effects` for the modular
@@ -25,6 +25,12 @@ Prefer `FaceMeshRenderer` for editor/runtime scene-object integrations. It owns
25
25
  face tracking internally through `FaceTracker`, reads live landmarks, deforms
26
26
  the canonical face mesh, and uses the shared FaceMesh material settings.
27
27
 
28
+ Face mesh landmarks arrive as raw MediaPipe image coordinates: X/Y use a
29
+ top-left normalized image plane and Z is tracker-relative depth. The renderer
30
+ projects those points into its camera scene itself. They are not SDK
31
+ center-origin tracker points, so do not feed them to
32
+ `trackerPointToScreenPixels` or apply an additional mirror/origin conversion.
33
+
28
34
  ```tsx
29
35
  import {
30
36
  DEFAULT_FACE_MESH_RENDERER,
@@ -58,6 +64,35 @@ faceMesh.settings = {
58
64
  patch)` is the edit-time source of truth: use it whenever material, shader,
59
65
  texture, blend, or mask state changes.
60
66
 
67
+ ### Runtime custom materials
68
+
69
+ Material types and shader presets are quick SDK defaults. Use
70
+ `materialOverride` when the face needs a custom Three.js material or shader.
71
+ It accepts a material node or a factory that receives the current camera texture,
72
+ the geometry key, and `faceUv`: the shared live camera-UV `BufferAttribute`
73
+ (`null` until the canonical mesh loads), also bound to every group's geometry as
74
+ `attribute vec2 faceUv`. This prop wins over preset settings and is intentionally not
75
+ part of `FaceMeshRendererSettings`, because materials, uniforms, and factories
76
+ are not serializable scene data.
77
+
78
+ ```tsx
79
+ <FaceMeshRenderer
80
+ value={faceMeshSettings}
81
+ materialOverride={({ cameraTexture, geometryKey }) => (
82
+ <shaderMaterial
83
+ key={geometryKey}
84
+ uniforms={{ cameraTexture: { value: cameraTexture } }}
85
+ vertexShader={vertexShader}
86
+ fragmentShader={fragmentShader}
87
+ />
88
+ )}
89
+ />
90
+ ```
91
+
92
+ When rendering a serialized scene store, supply the same runtime-only override
93
+ through `SceneObjectRenderer`'s `faceMeshMaterialOverrides`, keyed by the
94
+ FaceMeshRenderer component ID. R3F owns material node disposal.
95
+
61
96
  FaceMesh `Transform` is intentionally not normal world-space `Transform3D`.
62
97
  `SceneObjectRenderer` passes an authored `Transform` into `FaceMeshRenderer`,
63
98
  and FaceMesh applies supported values in MediaPipe landmark space before camera