@react-three/drei 9.73.4 → 9.74.1

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/README.md CHANGED
@@ -52,6 +52,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
52
52
  <li><a href="#scrollcontrols">ScrollControls</a></li>
53
53
  <li><a href="#presentationcontrols">PresentationControls</a></li>
54
54
  <li><a href="#keyboardcontrols">KeyboardControls</a></li>
55
+ <li><a href="#FaceControls">FaceControls</a></li>
55
56
  </ul>
56
57
  <li><a href="#gizmos">Gizmos</a></li>
57
58
  <ul>
@@ -120,6 +121,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
120
121
  <li><a href="#useboxprojectedenv">useBoxProjectedEnv</a></li>
121
122
  <li><a href="#useTrail">useTrail</a></li>
122
123
  <li><a href="#useSurfaceSampler">useSurfaceSampler</a></li>
124
+ <li><a href="#facelandmarker">FaceLandmarker</a></li>
123
125
  </ul>
124
126
  <li><a href="#loading">Loaders</a></li>
125
127
  <ul>
@@ -359,7 +361,7 @@ If available controls have damping enabled by default, they manage their own upd
359
361
  const controls = useThree((state) => state.controls)
360
362
  ```
361
363
 
362
- Drei currently exports OrbitControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) and CameraControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story)
364
+ Drei currently exports OrbitControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) CameraControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story) and FaceControls [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-facecontrols)
363
365
 
364
366
  All controls react to the default camera. If you have a `<PerspectiveCamera makeDefault />` in your scene, they will control it. If you need to inject an imperative camera or one that isn't the default, use the `camera` prop: `<OrbitControls camera={MyCamera} />`.
365
367
 
@@ -596,6 +598,84 @@ function Foo() {
596
598
  }
597
599
  ```
598
600
 
601
+ #### FaceControls
602
+
603
+ [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/controls-facecontrols)
604
+
605
+ The camera follows your face.
606
+
607
+ <p>
608
+ <a href="https://codesandbox.io/s/jfx2t6"><img width="20%" src="https://github-production-user-asset-6210df.s3.amazonaws.com/76580/243503368-6239eb74-8473-4131-9203-33b29c1bbec0.png" alt="demo"/></a>
609
+ </p>
610
+
611
+ Pre-requisite: wrap into a `FaceLandmarker` provider
612
+
613
+ ```tsx
614
+ <FaceLandmarker>...</FaceLandmarker>
615
+ ```
616
+
617
+ ```tsx
618
+ <FaceControls />
619
+ ```
620
+
621
+ ```tsx
622
+ type FaceControlsProps = {
623
+ /** The camera to be controlled, default: global state camera */
624
+ camera?: THREE.Camera
625
+ /** Whether to autostart the webcam, default: true */
626
+ autostart?: boolean
627
+ /** Enable/disable the webcam, default: true */
628
+ webcam?: boolean
629
+ /** A custom video URL or mediaStream, default: undefined */
630
+ webcamVideoTextureSrc?: VideoTextureSrc
631
+ /** Disable the rAF camera position/rotation update, default: false */
632
+ manualUpdate?: boolean
633
+ /** Disable the rVFC face-detection, default: false */
634
+ manualDetect?: boolean
635
+ /** Callback function to call on "videoFrame" event, default: undefined */
636
+ onVideoFrame?: (e: THREE.Event) => void
637
+ /** Reference this FaceControls instance as state's `controls` */
638
+ makeDefault?: boolean
639
+ /** Approximate time to reach the target. A smaller value will reach the target faster. */
640
+ smoothTime?: number
641
+ /** Apply position offset extracted from `facialTransformationMatrix` */
642
+ offset?: boolean
643
+ /** Offset sensitivity factor, less is more sensible, default: 80 */
644
+ offsetScalar?: number
645
+ /** Enable eye-tracking */
646
+ eyes?: boolean
647
+ /** Force Facemesh's `origin` to be the middle of the 2 eyes, default: true */
648
+ eyesAsOrigin?: boolean
649
+ /** Constant depth of the Facemesh, default: .15 */
650
+ depth?: number
651
+ /** Enable debug mode, default: false */
652
+ debug?: boolean
653
+ /** Facemesh options, default: undefined */
654
+ facemesh?: FacemeshProps
655
+ }
656
+ ```
657
+
658
+ ```tsx
659
+ type FaceControlsApi = THREE.EventDispatcher & {
660
+ /** Detect faces from the video */
661
+ detect: (video: HTMLVideoElement, time: number) => void
662
+ /** Compute the target for the camera */
663
+ computeTarget: () => THREE.Object3D
664
+ /** Update camera's position/rotation to the `target` */
665
+ update: (delta: number, target?: THREE.Object3D) => void
666
+ /** <Facemesh> ref api */
667
+ facemeshApiRef: RefObject<FacemeshApi>
668
+ /** <Webcam> ref api */
669
+ webcamApiRef: RefObject<WebcamApi>
670
+ /** Play the video */
671
+ play: () => void
672
+ /** Pause the video */
673
+ pause: () => void
674
+ }
675
+ ```
676
+
677
+ > **Note** <br>`FaceControls` uses [`requestVideoFrameCallback`](https://caniuse.com/mdn-api_htmlvideoelement_requestvideoframecallback), you may need [a polyfill](https://github.com/ThaUnknown/rvfc-polyfill) (for Firefox).
678
+
599
679
  # Gizmos
600
680
 
601
681
  #### GizmoHelper
@@ -938,42 +1018,59 @@ Renders a THREE.Line2 using THREE.CatmullRomCurve3 for interpolation.
938
1018
 
939
1019
  [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/shapes-facemesh--facemesh-st)
940
1020
 
941
- Renders an oriented [MediaPipe `face` mesh](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection):
942
-
943
- ```jsx
944
- const face = {
945
- keypoints: [
946
- {x: 406.53152857172876, y: 256.8054528661723, z: 10.2, name: "lips"},
947
- {x: 406.544237446397, y: 230.06933367750395, z: 8},
948
- ...
949
- ],
950
- box: {
951
- xMin: 304.6476503248806,
952
- xMax: 502.5079975897382,
953
- yMin: 102.16298762367356,
954
- yMax: 349.035215984403,
955
- width: 197.86034726485758,
956
- height: 246.87222836072945
1021
+ Renders an oriented [MediaPipe face mesh](https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results):
1022
+
1023
+ ```jsx
1024
+ const faceLandmarkerResult = {
1025
+ "faceLandmarks": [
1026
+ [
1027
+ { "x": 0.5760777592658997, "y": 0.8639070391654968, "z": -0.030997956171631813 },
1028
+ { "x": 0.572094738483429, "y": 0.7886289358139038, "z": -0.07189624011516571 },
1029
+ // ...
1030
+ ],
1031
+ // ...
1032
+ ],
1033
+ "faceBlendshapes": [
1034
+ // ...
1035
+ ],
1036
+ "facialTransformationMatrixes": [
1037
+ // ...
1038
+ ]
957
1039
  },
958
1040
  }
1041
+ const points = faceLandmarkerResult.faceLandmarks[0]
959
1042
 
960
- <Facemesh face={face} />
1043
+ <Facemesh points={points} />
961
1044
  ```
962
1045
 
963
1046
  ```tsx
964
- type FacemeshProps = {
965
- /** a MediaPipeFaceMesh object, default: a lambda face */
1047
+ export type FacemeshProps = {
1048
+ /** an array of 468+ keypoints as returned by google/mediapipe tasks-vision, default: a sample face */
1049
+ points?: MediaPipePoints
1050
+ /** @deprecated an face object as returned by tensorflow/tfjs-models face-landmarks-detection */
966
1051
  face?: MediaPipeFaceMesh
967
- /** width of the mesh, default: undefined */
1052
+ /** constant width of the mesh, default: undefined */
968
1053
  width?: number
969
- /** or height of the mesh, default: undefined */
1054
+ /** or constant height of the mesh, default: undefined */
970
1055
  height?: number
971
- /** or depth of the mesh, default: 1 */
1056
+ /** or constant depth of the mesh, default: 1 */
972
1057
  depth?: number
973
1058
  /** a landmarks tri supposed to be vertical, default: [159, 386, 200] (see: https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection#mediapipe-facemesh-keypoints) */
974
1059
  verticalTri?: [number, number, number]
975
- /** a landmark index to be the origin of the mesh. default: undefined (ie. the bbox center) */
976
- origin?: number
1060
+ /** a landmark index (to get the position from) or a vec3 to be the origin of the mesh. default: undefined (ie. the bbox center) */
1061
+ origin?: number | THREE.Vector3
1062
+ /** A facial transformation matrix, as returned by FaceLandmarkerResult.facialTransformationMatrixes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */
1063
+ facialTransformationMatrix?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.facialTransformationMatrixes[0]
1064
+ /** Apply position offset extracted from `facialTransformationMatrix` */
1065
+ offset?: boolean
1066
+ /** Offset sensitivity factor, less is more sensible */
1067
+ offsetScalar?: number
1068
+ /** Fface blendshapes, as returned by FaceLandmarkerResult.faceBlendshapes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */
1069
+ faceBlendshapes?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.faceBlendshapes[0]
1070
+ /** whether to enable eyes (nb. `faceBlendshapes` is required for), default: true */
1071
+ eyes?: boolean
1072
+ /** Force `origin` to be the middle of the 2 eyes (nb. `eyes` is required for), default: false */
1073
+ eyesAsOrigin?: boolean
977
1074
  /** debug mode, default: false */
978
1075
  debug?: boolean
979
1076
  }
@@ -984,20 +1081,28 @@ Ref-api:
984
1081
  ```tsx
985
1082
  const api = useRef<FacemeshApi>()
986
1083
 
987
- <Facemesh ref={api} face={face} />
1084
+ <Facemesh ref={api} points={points} />
988
1085
  ```
989
1086
 
990
1087
  ```tsx
991
1088
  type FacemeshApi = {
992
1089
  meshRef: React.RefObject<THREE.Mesh>
993
1090
  outerRef: React.RefObject<THREE.Group>
1091
+ eyeRightRef: React.RefObject<FacemeshEyeApi>
1092
+ eyeLeftRef: React.RefObject<FacemeshEyeApi>
994
1093
  }
995
1094
  ```
996
1095
 
997
- NB: `outerRef` group is oriented as your `face`. You can for example get its world direction:
1096
+ You can for example get face mesh world direction:
1097
+
1098
+ ```tsx
1099
+ api.meshRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
1100
+ ```
1101
+
1102
+ or get L/R iris direction:
998
1103
 
999
1104
  ```tsx
1000
- meshRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
1105
+ api.eyeRightRef.current.irisDirRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
1001
1106
  ```
1002
1107
 
1003
1108
  # Abstractions
@@ -2432,6 +2537,12 @@ const buffer = useSurfaceSampler(
2432
2537
  )
2433
2538
  ```
2434
2539
 
2540
+ ### FaceLandmarker
2541
+
2542
+ ![](https://img.shields.io/badge/-suspense-brightgreen)
2543
+
2544
+ A @mediapipe/tasks-vision [`FaceLandmarker`](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.facelandmarker) provider, as well as a `useFaceLandmarker` hook.
2545
+
2435
2546
  # Loading
2436
2547
 
2437
2548
  #### Loader
@@ -3861,7 +3972,7 @@ If you provide a single string it will use THREE.RGBELoader.
3861
3972
  You can also use [@pmndrs/assets](https://github.com/pmndrs/assets) to easily self host common assets. Always use dynamic imports to avoid making this part of your main bundle.
3862
3973
 
3863
3974
  ```jsx
3864
- <Environment files={import('@pmndrs/assets/hdri/city')} />
3975
+ <Environment files={import('@pmndrs/assets/hdri/city.exr')} />
3865
3976
  ```
3866
3977
 
3867
3978
  If you already have a cube texture you can pass it directly:
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("@babel/runtime/helpers/extends"),t=require("three"),r=require("react"),a=require("@react-three/fiber"),n=require("maath"),o=require("suspend-react"),c=require("./useVideoTexture.cjs.js"),u=require("./Facemesh.cjs.js"),s=require("./FaceLandmarker.cjs.js");function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}function l(e){if(e&&e.__esModule)return e;var t=Object.create(null);return e&&Object.keys(e).forEach((function(r){if("default"!==r){var a=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,a.get?a:{enumerable:!0,get:function(){return e[r]}})}})),t.default=e,Object.freeze(t)}require("./Line.cjs.js"),require("three-stdlib"),require("@mediapipe/tasks-vision");var d=i(e),f=l(t),p=l(r);function m(e,t){return e.clone().add(t).multiplyScalar(.5)}function v(e,t,r){const a=e.localToWorld(t);return r.worldToLocal(a)}const b=r.createContext({}),y=r.forwardRef((({camera:e,autostart:t=!0,webcam:o=!0,webcamVideoTextureSrc:c,manualUpdate:i=!1,manualDetect:l=!1,onVideoFrame:y,smoothTime:h=.25,offset:x=!0,offsetScalar:T=80,eyes:E=!1,eyesAsOrigin:j=!0,depth:k=.15,debug:V=!1,facemesh:g,makeDefault:F},R)=>{var S,q;const O=a.useThree((e=>e.scene)),C=a.useThree((e=>e.camera)),M=a.useThree((e=>e.set)),D=a.useThree((e=>e.get)),A=e||C,L=r.useRef(null),P=r.useRef(null),[I]=r.useState((()=>new f.Object3D)),[_]=r.useState((()=>new f.Vector3)),[B]=r.useState((()=>new f.Vector3)),[H]=r.useState((()=>new f.Vector3)),[W]=r.useState((()=>new f.Vector3)),z=r.useCallback((()=>{I.parent=A.parent;const e=P.current;if(e){const{outerRef:t,eyeRightRef:r,eyeLeftRef:a}=e;if(r.current&&a.current){const{irisDirRef:e}=r.current,{irisDirRef:n}=a.current;e.current&&n.current&&t.current&&(_.copy(v(e.current,new f.Vector3(0,0,0),t.current)),B.copy(v(n.current,new f.Vector3(0,0,0),t.current)),I.position.copy(v(t.current,m(_,B),A.parent||O)),H.copy(v(e.current,new f.Vector3(0,0,1),t.current)),W.copy(v(n.current,new f.Vector3(0,0,1),t.current)),I.lookAt(t.current.localToWorld(m(H,W))))}else t.current&&(I.position.copy(v(t.current,new f.Vector3(0,0,0),A.parent||O)),I.lookAt(t.current.localToWorld(new f.Vector3(0,0,1))))}return I}),[A,B,W,_,H,O,I]),[U]=r.useState((()=>new f.Object3D)),G=r.useCallback((function(e,t){if(A){var r;if(null!==(r=t)&&void 0!==r||(t=z()),h>0){const r=1e-9;n.easing.damp3(U.position,t.position,h,e,void 0,void 0,r),n.easing.dampE(U.rotation,t.rotation,h,e,void 0,void 0,r)}else U.position.copy(t.position),U.rotation.copy(t.rotation);A.position.copy(U.position),A.rotation.copy(U.rotation)}}),[A,z,h,U.position,U.rotation]),[J,K]=r.useState(),N=s.useFaceLandmarker(),Q=r.useCallback(((e,t)=>{const r=null==N?void 0:N.detectForVideo(e,t);K(r)}),[N]);a.useFrame(((e,t)=>{i||G(t)}));const X=r.useMemo((()=>Object.assign(Object.create(f.EventDispatcher.prototype),{detect:Q,computeTarget:z,update:G,facemeshApiRef:P,webcamApiRef:L,play:()=>{var e,t;null==(e=L.current)||null==(t=e.videoTextureApiRef.current)||t.texture.source.data.play()},pause:()=>{var e,t;null==(e=L.current)||null==(t=e.videoTextureApiRef.current)||t.texture.source.data.pause()}})),[Q,z,G]);r.useImperativeHandle(R,(()=>X),[X]),r.useEffect((()=>{const e=e=>{l||Q(e.texture.source.data,e.time),y&&y(e)};return X.addEventListener("videoFrame",e),()=>{X.removeEventListener("videoFrame",e)}}),[X,Q,N,l,y]),r.useEffect((()=>{if(F){const e=D().controls;return M({controls:X}),()=>M({controls:e})}}),[F,X,D,M]);const Y=null==J?void 0:J.faceLandmarks[0],Z=null==J||null==(S=J.facialTransformationMatrixes)?void 0:S[0],$=null==J||null==(q=J.faceBlendshapes)?void 0:q[0];return p.createElement(b.Provider,{value:X},o&&p.createElement(r.Suspense,{fallback:null},p.createElement(w,{ref:L,autostart:t,videoTextureSrc:c})),p.createElement(u.Facemesh,d.default({ref:P},g,{points:Y,depth:k,facialTransformationMatrix:Z,faceBlendshapes:$,eyes:E,eyesAsOrigin:j,offset:x,offsetScalar:T,debug:V,"rotation-z":Math.PI,visible:V}),p.createElement("meshBasicMaterial",{side:f.DoubleSide})))})),h=()=>r.useContext(b),w=r.forwardRef((({videoTextureSrc:e,autostart:t=!0},a)=>{const n=r.useRef(null),c=h(),u=o.suspend((async()=>e?Promise.resolve(null):await navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:"user"}})),[e]);r.useEffect((()=>(c.dispatchEvent({type:"stream",stream:u}),()=>{null==u||u.getTracks().forEach((e=>e.stop())),o.clear([e])})),[u,c,e]);const s=r.useMemo((()=>({videoTextureApiRef:n})),[]);return r.useImperativeHandle(a,(()=>s),[s]),p.createElement(r.Suspense,{fallback:null},p.createElement(x,{ref:n,src:e||u,start:t}))})),x=r.forwardRef((({src:e,start:t},a)=>{const n=c.useVideoTexture(e,{start:t}),o=n.source.data,u=h(),s=r.useCallback((e=>{u.dispatchEvent({type:"videoFrame",texture:n,time:e})}),[n,u]);T(o,s);const i=r.useMemo((()=>({texture:n})),[n]);return r.useImperativeHandle(a,(()=>i),[i]),p.createElement(p.Fragment,null)})),T=(e,t)=>{r.useEffect((()=>{if(!e||!e.requestVideoFrameCallback)return;let r;return e.requestVideoFrameCallback((function a(...n){t(...n),r=e.requestVideoFrameCallback(a)})),()=>e.cancelVideoFrameCallback(r)}),[e,t])};exports.FaceControls=y,exports.useFaceControls=h;
@@ -0,0 +1,42 @@
1
+ import * as THREE from 'three';
2
+ import * as React from 'react';
3
+ import { RefObject } from 'react';
4
+ import { useVideoTexture } from './useVideoTexture';
5
+ import { FacemeshApi, FacemeshProps } from './Facemesh';
6
+ declare type VideoTextureSrc = Parameters<typeof useVideoTexture>[0];
7
+ export declare type FaceControlsProps = {
8
+ camera?: THREE.Camera;
9
+ autostart?: boolean;
10
+ webcam?: boolean;
11
+ webcamVideoTextureSrc?: VideoTextureSrc;
12
+ manualUpdate?: boolean;
13
+ manualDetect?: boolean;
14
+ onVideoFrame?: (e: THREE.Event) => void;
15
+ makeDefault?: boolean;
16
+ smoothTime?: number;
17
+ offset?: boolean;
18
+ offsetScalar?: number;
19
+ eyes?: boolean;
20
+ eyesAsOrigin?: boolean;
21
+ depth?: number;
22
+ debug?: boolean;
23
+ facemesh?: FacemeshProps;
24
+ };
25
+ export declare type FaceControlsApi = THREE.EventDispatcher & {
26
+ detect: (video: HTMLVideoElement, time: number) => void;
27
+ computeTarget: () => THREE.Object3D;
28
+ update: (delta: number, target?: THREE.Object3D) => void;
29
+ facemeshApiRef: RefObject<FacemeshApi>;
30
+ webcamApiRef: RefObject<WebcamApi>;
31
+ play: () => void;
32
+ pause: () => void;
33
+ };
34
+ export declare const FaceControls: React.ForwardRefExoticComponent<FaceControlsProps & React.RefAttributes<FaceControlsApi>>;
35
+ export declare const useFaceControls: () => FaceControlsApi;
36
+ declare type WebcamApi = {
37
+ videoTextureApiRef: RefObject<VideoTextureApi>;
38
+ };
39
+ declare type VideoTextureApi = {
40
+ texture: THREE.VideoTexture;
41
+ };
42
+ export {};
@@ -0,0 +1,315 @@
1
+ import _extends from '@babel/runtime/helpers/esm/extends';
2
+ import * as THREE from 'three';
3
+ import * as React from 'react';
4
+ import { forwardRef, useRef, useState, useCallback, useMemo, useImperativeHandle, useEffect, Suspense, useContext, createContext } from 'react';
5
+ import { useThree, useFrame } from '@react-three/fiber';
6
+ import { easing } from 'maath';
7
+ import { suspend, clear } from 'suspend-react';
8
+ import { useVideoTexture } from './useVideoTexture.js';
9
+ import { Facemesh } from './Facemesh.js';
10
+ import { useFaceLandmarker } from './FaceLandmarker.js';
11
+
12
+ // useVideoTexture 1st arg `src` type
13
+ function mean(v1, v2) {
14
+ return v1.clone().add(v2).multiplyScalar(0.5);
15
+ }
16
+
17
+ function localToLocal(objSrc, v, objDst) {
18
+ // see: https://discourse.threejs.org/t/object3d-localtolocal/51564
19
+ const v_world = objSrc.localToWorld(v);
20
+ return objDst.worldToLocal(v_world);
21
+ } //
22
+ //
23
+ //
24
+
25
+
26
+ const FaceControlsContext = /*#__PURE__*/createContext({});
27
+ const FaceControls = /*#__PURE__*/forwardRef(({
28
+ camera,
29
+ autostart = true,
30
+ webcam = true,
31
+ webcamVideoTextureSrc,
32
+ manualUpdate = false,
33
+ manualDetect = false,
34
+ onVideoFrame,
35
+ smoothTime = 0.25,
36
+ offset = true,
37
+ offsetScalar = 80,
38
+ eyes = false,
39
+ eyesAsOrigin = true,
40
+ depth = 0.15,
41
+ debug = false,
42
+ facemesh,
43
+ makeDefault
44
+ }, fref) => {
45
+ var _faces$facialTransfor, _faces$faceBlendshape;
46
+
47
+ const scene = useThree(state => state.scene);
48
+ const defaultCamera = useThree(state => state.camera);
49
+ const set = useThree(state => state.set);
50
+ const get = useThree(state => state.get);
51
+ const explCamera = camera || defaultCamera;
52
+ const webcamApiRef = useRef(null);
53
+ const facemeshApiRef = useRef(null); //
54
+ // computeTarget()
55
+ //
56
+ // Compute `target` position and rotation for the camera (according to <Facemesh>)
57
+ //
58
+ // 1. 👀 either following the 2 eyes
59
+ // 2. 👤 or just the head mesh
60
+ //
61
+
62
+ const [target] = useState(() => new THREE.Object3D());
63
+ const [irisRightDirPos] = useState(() => new THREE.Vector3());
64
+ const [irisLeftDirPos] = useState(() => new THREE.Vector3());
65
+ const [irisRightLookAt] = useState(() => new THREE.Vector3());
66
+ const [irisLeftLookAt] = useState(() => new THREE.Vector3());
67
+ const computeTarget = useCallback(() => {
68
+ // same parent as the camera
69
+ target.parent = explCamera.parent;
70
+ const facemeshApi = facemeshApiRef.current;
71
+
72
+ if (facemeshApi) {
73
+ const {
74
+ outerRef,
75
+ eyeRightRef,
76
+ eyeLeftRef
77
+ } = facemeshApi;
78
+
79
+ if (eyeRightRef.current && eyeLeftRef.current) {
80
+ // 1. 👀
81
+ const {
82
+ irisDirRef: irisRightDirRef
83
+ } = eyeRightRef.current;
84
+ const {
85
+ irisDirRef: irisLeftDirRef
86
+ } = eyeLeftRef.current;
87
+
88
+ if (irisRightDirRef.current && irisLeftDirRef.current && outerRef.current) {
89
+ //
90
+ // position: mean of irisRightDirPos,irisLeftDirPos
91
+ //
92
+ irisRightDirPos.copy(localToLocal(irisRightDirRef.current, new THREE.Vector3(0, 0, 0), outerRef.current));
93
+ irisLeftDirPos.copy(localToLocal(irisLeftDirRef.current, new THREE.Vector3(0, 0, 0), outerRef.current));
94
+ target.position.copy(localToLocal(outerRef.current, mean(irisRightDirPos, irisLeftDirPos), explCamera.parent || scene)); //
95
+ // lookAt: mean of irisRightLookAt,irisLeftLookAt
96
+ //
97
+
98
+ irisRightLookAt.copy(localToLocal(irisRightDirRef.current, new THREE.Vector3(0, 0, 1), outerRef.current));
99
+ irisLeftLookAt.copy(localToLocal(irisLeftDirRef.current, new THREE.Vector3(0, 0, 1), outerRef.current));
100
+ target.lookAt(outerRef.current.localToWorld(mean(irisRightLookAt, irisLeftLookAt)));
101
+ }
102
+ } else {
103
+ // 2. 👤
104
+ if (outerRef.current) {
105
+ target.position.copy(localToLocal(outerRef.current, new THREE.Vector3(0, 0, 0), explCamera.parent || scene));
106
+ target.lookAt(outerRef.current.localToWorld(new THREE.Vector3(0, 0, 1)));
107
+ }
108
+ }
109
+ }
110
+
111
+ return target;
112
+ }, [explCamera, irisLeftDirPos, irisLeftLookAt, irisRightDirPos, irisRightLookAt, scene, target]); //
113
+ // update()
114
+ //
115
+ // Updating the camera `current` position and rotation, following `target`
116
+ //
117
+
118
+ const [current] = useState(() => new THREE.Object3D());
119
+ const update = useCallback(function (delta, target) {
120
+ if (explCamera) {
121
+ var _target;
122
+
123
+ (_target = target) !== null && _target !== void 0 ? _target : target = computeTarget();
124
+
125
+ if (smoothTime > 0) {
126
+ // damping current
127
+ const eps = 1e-9;
128
+ easing.damp3(current.position, target.position, smoothTime, delta, undefined, undefined, eps);
129
+ easing.dampE(current.rotation, target.rotation, smoothTime, delta, undefined, undefined, eps);
130
+ } else {
131
+ // instant
132
+ current.position.copy(target.position);
133
+ current.rotation.copy(target.rotation);
134
+ }
135
+
136
+ explCamera.position.copy(current.position);
137
+ explCamera.rotation.copy(current.rotation);
138
+ }
139
+ }, [explCamera, computeTarget, smoothTime, current.position, current.rotation]); //
140
+ // detect()
141
+ //
142
+
143
+ const [faces, setFaces] = useState();
144
+ const faceLandmarker = useFaceLandmarker();
145
+ const detect = useCallback((video, time) => {
146
+ const faces = faceLandmarker == null ? void 0 : faceLandmarker.detectForVideo(video, time);
147
+ setFaces(faces);
148
+ }, [faceLandmarker]);
149
+ useFrame((_, delta) => {
150
+ if (!manualUpdate) {
151
+ update(delta);
152
+ }
153
+ }); // Ref API
154
+
155
+ const api = useMemo(() => Object.assign(Object.create(THREE.EventDispatcher.prototype), {
156
+ detect,
157
+ computeTarget,
158
+ update,
159
+ facemeshApiRef,
160
+ webcamApiRef,
161
+ // shorthands
162
+ play: () => {
163
+ var _webcamApiRef$current, _webcamApiRef$current2;
164
+
165
+ (_webcamApiRef$current = webcamApiRef.current) == null ? void 0 : (_webcamApiRef$current2 = _webcamApiRef$current.videoTextureApiRef.current) == null ? void 0 : _webcamApiRef$current2.texture.source.data.play();
166
+ },
167
+ pause: () => {
168
+ var _webcamApiRef$current3, _webcamApiRef$current4;
169
+
170
+ (_webcamApiRef$current3 = webcamApiRef.current) == null ? void 0 : (_webcamApiRef$current4 = _webcamApiRef$current3.videoTextureApiRef.current) == null ? void 0 : _webcamApiRef$current4.texture.source.data.pause();
171
+ }
172
+ }), [detect, computeTarget, update]);
173
+ useImperativeHandle(fref, () => api, [api]); //
174
+ // events callbacks
175
+ //
176
+
177
+ useEffect(() => {
178
+ const onVideoFrameCb = e => {
179
+ if (!manualDetect) detect(e.texture.source.data, e.time);
180
+ if (onVideoFrame) onVideoFrame(e);
181
+ };
182
+
183
+ api.addEventListener('videoFrame', onVideoFrameCb);
184
+ return () => {
185
+ api.removeEventListener('videoFrame', onVideoFrameCb);
186
+ };
187
+ }, [api, detect, faceLandmarker, manualDetect, onVideoFrame]); // `controls` global state
188
+
189
+ useEffect(() => {
190
+ if (makeDefault) {
191
+ const old = get().controls;
192
+ set({
193
+ controls: api
194
+ });
195
+ return () => set({
196
+ controls: old
197
+ });
198
+ }
199
+ }, [makeDefault, api, get, set]);
200
+ const points = faces == null ? void 0 : faces.faceLandmarks[0];
201
+ const facialTransformationMatrix = faces == null ? void 0 : (_faces$facialTransfor = faces.facialTransformationMatrixes) == null ? void 0 : _faces$facialTransfor[0];
202
+ const faceBlendshapes = faces == null ? void 0 : (_faces$faceBlendshape = faces.faceBlendshapes) == null ? void 0 : _faces$faceBlendshape[0];
203
+ return /*#__PURE__*/React.createElement(FaceControlsContext.Provider, {
204
+ value: api
205
+ }, webcam && /*#__PURE__*/React.createElement(Suspense, {
206
+ fallback: null
207
+ }, /*#__PURE__*/React.createElement(Webcam, {
208
+ ref: webcamApiRef,
209
+ autostart: autostart,
210
+ videoTextureSrc: webcamVideoTextureSrc
211
+ })), /*#__PURE__*/React.createElement(Facemesh, _extends({
212
+ ref: facemeshApiRef
213
+ }, facemesh, {
214
+ points: points,
215
+ depth: depth,
216
+ facialTransformationMatrix: facialTransformationMatrix,
217
+ faceBlendshapes: faceBlendshapes,
218
+ eyes: eyes,
219
+ eyesAsOrigin: eyesAsOrigin,
220
+ offset: offset,
221
+ offsetScalar: offsetScalar,
222
+ debug: debug,
223
+ "rotation-z": Math.PI,
224
+ visible: debug
225
+ }), /*#__PURE__*/React.createElement("meshBasicMaterial", {
226
+ side: THREE.DoubleSide
227
+ })));
228
+ });
229
+ const useFaceControls = () => useContext(FaceControlsContext); //
230
+ // Webcam
231
+ //
232
+
233
+ const Webcam = /*#__PURE__*/forwardRef(({
234
+ videoTextureSrc,
235
+ autostart = true
236
+ }, fref) => {
237
+ const videoTextureApiRef = useRef(null);
238
+ const faceControls = useFaceControls();
239
+ const stream = suspend(async () => {
240
+ return !videoTextureSrc ? await navigator.mediaDevices.getUserMedia({
241
+ audio: false,
242
+ video: {
243
+ facingMode: 'user'
244
+ }
245
+ }) : Promise.resolve(null);
246
+ }, [videoTextureSrc]);
247
+ useEffect(() => {
248
+ faceControls.dispatchEvent({
249
+ type: 'stream',
250
+ stream
251
+ });
252
+ return () => {
253
+ stream == null ? void 0 : stream.getTracks().forEach(track => track.stop());
254
+ clear([videoTextureSrc]);
255
+ };
256
+ }, [stream, faceControls, videoTextureSrc]); // ref-api
257
+
258
+ const api = useMemo(() => ({
259
+ videoTextureApiRef
260
+ }), []);
261
+ useImperativeHandle(fref, () => api, [api]);
262
+ return /*#__PURE__*/React.createElement(Suspense, {
263
+ fallback: null
264
+ }, /*#__PURE__*/React.createElement(VideoTexture, {
265
+ ref: videoTextureApiRef,
266
+ src: videoTextureSrc || stream,
267
+ start: autostart
268
+ }));
269
+ }); //
270
+ // VideoTexture
271
+ //
272
+
273
+ const VideoTexture = /*#__PURE__*/forwardRef(({
274
+ src,
275
+ start
276
+ }, fref) => {
277
+ const texture = useVideoTexture(src, {
278
+ start
279
+ });
280
+ const video = texture.source.data;
281
+ const faceControls = useFaceControls();
282
+ const onVideoFrame = useCallback(time => {
283
+ faceControls.dispatchEvent({
284
+ type: 'videoFrame',
285
+ texture,
286
+ time
287
+ });
288
+ }, [texture, faceControls]);
289
+ useVideoFrame(video, onVideoFrame); // ref-api
290
+
291
+ const api = useMemo(() => ({
292
+ texture
293
+ }), [texture]);
294
+ useImperativeHandle(fref, () => api, [api]);
295
+ return /*#__PURE__*/React.createElement(React.Fragment, null);
296
+ });
297
+
298
+ const useVideoFrame = (video, f) => {
299
+ // https://web.dev/requestvideoframecallback-rvfc/
300
+ // https://www.remotion.dev/docs/video-manipulation
301
+ useEffect(() => {
302
+ if (!video || !video.requestVideoFrameCallback) return;
303
+ let handle;
304
+
305
+ function callback(...args) {
306
+ f(...args);
307
+ handle = video.requestVideoFrameCallback(callback);
308
+ }
309
+
310
+ video.requestVideoFrameCallback(callback);
311
+ return () => video.cancelVideoFrameCallback(handle);
312
+ }, [video, f]);
313
+ };
314
+
315
+ export { FaceControls, useFaceControls };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react"),t=require("@mediapipe/tasks-vision"),r=require("suspend-react");function a(e){if(e&&e.__esModule)return e;var t=Object.create(null);return e&&Object.keys(e).forEach((function(r){if("default"!==r){var a=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,a.get?a:{enumerable:!0,get:function(){return e[r]}})}})),t.default=e,Object.freeze(t)}var s=a(e);const n=e.createContext({}),o={basePath:"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm",options:{baseOptions:{modelAssetPath:"https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",delegate:"GPU"},runningMode:"VIDEO",outputFaceBlendshapes:!0,outputFacialTransformationMatrixes:!0}};exports.FaceLandmarker=function({basePath:a=o.basePath,options:i=o.options,children:c}){const u=JSON.stringify(i),l=r.suspend((async()=>await t.FilesetResolver.forVisionTasks(a).then((e=>t.FaceLandmarker.createFromOptions(e,i)))),[a,u]);return e.useEffect((()=>()=>{null==l||l.close(),r.clear([a,u])}),[l,a,u]),s.createElement(n.Provider,{value:l},c)},exports.FaceLandmarkerDefaults=o,exports.useFaceLandmarker=function(){return e.useContext(n)};
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+ import { FaceLandmarker as FaceLandmarkerImpl, FaceLandmarkerOptions } from '@mediapipe/tasks-vision';
3
+ declare type FaceLandmarkerProps = {
4
+ basePath?: string;
5
+ options?: FaceLandmarkerOptions;
6
+ children?: ReactNode;
7
+ };
8
+ export declare const FaceLandmarkerDefaults: {
9
+ basePath: string;
10
+ options: FaceLandmarkerOptions;
11
+ };
12
+ export declare function FaceLandmarker({ basePath, options, children, }: FaceLandmarkerProps): JSX.Element;
13
+ export declare function useFaceLandmarker(): FaceLandmarkerImpl | undefined;
14
+ export {};