@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 +140 -29
- package/core/FaceControls.cjs.js +1 -0
- package/core/FaceControls.d.ts +42 -0
- package/core/FaceControls.js +315 -0
- package/core/FaceLandmarker.cjs.js +1 -0
- package/core/FaceLandmarker.d.ts +14 -0
- package/core/FaceLandmarker.js +43 -0
- package/core/Facemesh.cjs.js +1 -1
- package/core/Facemesh.d.ts +113 -4
- package/core/Facemesh.js +2471 -47
- package/core/index.cjs.js +1 -1
- package/core/index.d.ts +2 -0
- package/core/index.js +5 -1
- package/index.cjs.js +1 -1
- package/index.js +3 -1
- package/native/index.cjs.js +1 -1
- package/native/index.js +5 -1
- package/package.json +2 -1
- package/web/index.cjs.js +1 -1
- package/web/index.js +4 -1
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://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story)
|
|
364
|
+
Drei currently exports OrbitControls [](https://drei.vercel.app/?path=/story/controls-orbitcontrols--orbit-controls-story), MapControls [](https://drei.vercel.app/?path=/story/controls-mapcontrols--map-controls-scene-st), TrackballControls, ArcballControls, FlyControls, DeviceOrientationControls, PointerLockControls [](https://drei.vercel.app/?path=/story/controls-pointerlockcontrols--pointer-lock-controls-scene-st), FirstPersonControls [](https://drei.vercel.app/?path=/story/controls-firstpersoncontrols--first-person-controls-story) CameraControls [](https://drei.vercel.app/?path=/story/controls-cameracontrols--camera-controls-story) and FaceControls [](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://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://drei.vercel.app/?path=/story/shapes-facemesh--facemesh-st)
|
|
940
1020
|
|
|
941
|
-
Renders an oriented [MediaPipe
|
|
942
|
-
|
|
943
|
-
```jsx
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
|
1043
|
+
<Facemesh points={points} />
|
|
961
1044
|
```
|
|
962
1045
|
|
|
963
1046
|
```tsx
|
|
964
|
-
type FacemeshProps = {
|
|
965
|
-
/**
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+

|
|
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 {};
|