Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FaceControls #1461

Merged
merged 16 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .storybook/stories/FaceControls.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint react-hooks/exhaustive-deps: 1 */
import * as THREE from 'three'
import * as React from 'react'

import { Setup } from '../Setup'

import { FaceLandmarker, FaceControls, Box } from '../../src'

export default {
title: 'Controls/FaceControls',
component: FaceControls,
decorators: [(storyFn) => <Setup cameraFov={60}>{storyFn()}</Setup>],
}

function FaceControlsScene(props) {
return (
<>
<color attach="background" args={['#303030']} />
<axesHelper />

<React.Suspense fallback={null}>
Copy link
Member Author

@abernier abernier Jun 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess React.Suspense is now required here, since <FaceLandmarker> provider is now suspended?

I'm not sure

<FaceLandmarker>
<FaceControls {...props} />
</FaceLandmarker>
</React.Suspense>

<Box args={[0.1, 0.1, 0.1]}>
<meshStandardMaterial />
</Box>
</>
)
}

export const FaceControlsSt = (args) => <FaceControlsScene {...args} />
FaceControlsSt.args = {
eyes: undefined,
}

FaceControlsSt.argTypes = {
eyes: { control: { type: 'boolean' } },
}

FaceControlsSt.storyName = 'Default'
33 changes: 23 additions & 10 deletions .storybook/stories/Facemesh.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Vector3 } from 'three'

import { Setup } from '../Setup'

import { Facemesh } from '../../src'
import { Facemesh, FacemeshDatas } from '../../src'

export default {
title: 'Shapes/Facemesh',
Expand All @@ -20,31 +20,44 @@ export default {
],
}

export const FacemeshSt = ({ depth, origin, wireframe, flat, skin, debug }) => (
export const FacemeshSt = ({ depth, origin, eyes, eyesAsOrigin, offset, offsetScalar, debug }) => (
<>
<color attach="background" args={['#303030']} />
<axesHelper />

<Facemesh depth={depth} origin={origin} debug={debug} rotation-z={Math.PI}>
<meshStandardMaterial side={THREE.DoubleSide} color={skin} flatShading={flat} wireframe={wireframe} />
<Facemesh
depth={depth}
origin={origin}
eyes={eyes}
faceBlendshapes={FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.faceBlendshapes[0]}
eyesAsOrigin={eyesAsOrigin}
offset={offset}
facialTransformationMatrix={FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.facialTransformationMatrixes[0]}
offsetScalar={offsetScalar}
debug={debug}
rotation-z={Math.PI}
>
<meshStandardMaterial side={THREE.DoubleSide} color="#cbcbcb" flatShading={true} transparent opacity={0.98} />
</Facemesh>
</>
)
FacemeshSt.args = {
depth: undefined,
origin: undefined,
wireframe: false,
flat: true,
skin: '#cbcbcb',
eyes: undefined,
eyesAsOrigin: undefined,
offset: undefined,
offsetScalar: undefined,
debug: true,
}

FacemeshSt.argTypes = {
depth: { control: { type: 'range', min: 0, max: 6.5, step: 0.01 } },
origin: { control: 'select', options: [undefined, 168, 9] },
wireframe: { control: { type: 'boolean' } },
flat: { control: { type: 'boolean' } },
skin: { control: { type: 'color' } },
eyes: { control: { type: 'boolean' } },
eyesAsOrigin: { control: { type: 'boolean' } },
offset: { control: { type: 'boolean' } },
offsetScalar: { control: { type: 'range', min: 0, max: 200, step: 1 } },
debug: { control: { type: 'boolean' } },
}

Expand Down
165 changes: 137 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
<li><a href="#scrollcontrols">ScrollControls</a></li>
<li><a href="#presentationcontrols">PresentationControls</a></li>
<li><a href="#keyboardcontrols">KeyboardControls</a></li>
<li><a href="#FaceControls">FaceControls</a></li>
</ul>
<li><a href="#gizmos">Gizmos</a></li>
<ul>
Expand Down Expand Up @@ -120,6 +121,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
<li><a href="#useboxprojectedenv">useBoxProjectedEnv</a></li>
<li><a href="#useTrail">useTrail</a></li>
<li><a href="#useSurfaceSampler">useSurfaceSampler</a></li>
<li><a href="#facelandmarker">FaceLandmarker</a></li>
</ul>
<li><a href="#loading">Loaders</a></li>
<ul>
Expand Down Expand Up @@ -358,7 +360,7 @@ If available controls have damping enabled by default, they manage their own upd
const controls = useThree((state) => state.controls)
```

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)
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)

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} />`.

Expand Down Expand Up @@ -595,6 +597,82 @@ function Foo() {
}
```

#### FaceControls

The camera follows your face.

<p>
<a href="https://codesandbox.io/s/bf01sb"><img width="20%" src="https://github.com/abernier/abernier/assets/76580/2138ef30-48f3-4ae9-b2bb-4f600de0a35e" alt="demo"/></a>
</p>

Pre-requisite: wrap into a `FaceLandmarker` provider

```tsx
<FaceLandmarker>...</FaceLandmarker>
```

```tsx
<FaceControls />
```

```tsx
type FaceControlsProps = {
/** The camera to be controlled, default: global state camera */
camera?: THREE.Camera
/** Whether to autostart the webcam, default: true */
autostart?: boolean
/** Enable/disable the webcam, default: true */
webcam?: boolean
/** A custom video URL or mediaStream, default: undefined */
webcamVideoTextureSrc?: VideoTextureSrc
/** Disable the rAF camera position/rotation update, default: false */
manualUpdate?: boolean
/** Disable the rVFC face-detection, default: false */
manualDetect?: boolean
/** Callback function to call on "videoFrame" event, default: undefined */
onVideoFrame?: (e: THREE.Event) => void
/** Reference this FaceControls instance as state's `controls` */
makeDefault?: boolean
/** Approximate time to reach the target. A smaller value will reach the target faster. */
smoothTime?: number
/** Apply position offset extracted from `facialTransformationMatrix` */
offset?: boolean
/** Offset sensitivity factor, less is more sensible, default: 80 */
offsetScalar?: number
/** Enable eye-tracking */
eyes?: boolean
/** Force Facemesh's `origin` to be the middle of the 2 eyes, default: true */
eyesAsOrigin?: boolean
/** Constant depth of the Facemesh, default: .15 */
depth?: number
/** Enable debug mode, default: false */
debug?: boolean
/** Facemesh options, default: undefined */
facemesh?: FacemeshProps
}
```

```tsx
type FaceControlsApi = THREE.EventDispatcher & {
/** Detect faces from the video */
detect: (video: HTMLVideoElement, time: number) => void
/** Compute the target for the camera */
computeTarget: () => THREE.Object3D
/** Update camera's position/rotation to the `target` */
update: (delta: number, target?: THREE.Object3D) => void
/** <Facemesh> ref api */
facemeshApiRef: RefObject<FacemeshApi>
/** <Webcam> ref api */
webcamApiRef: RefObject<WebcamApi>
/** Play the video */
play: () => void
/** Pause the video */
pause: () => void
}
```

> **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).

# Gizmos

#### GizmoHelper
Expand Down Expand Up @@ -937,42 +1015,59 @@ Renders a THREE.Line2 using THREE.CatmullRomCurve3 for interpolation.

[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/shapes-facemesh--facemesh-st)

Renders an oriented [MediaPipe `face` mesh](https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection):

```jsx
const face = {
keypoints: [
{x: 406.53152857172876, y: 256.8054528661723, z: 10.2, name: "lips"},
{x: 406.544237446397, y: 230.06933367750395, z: 8},
...
],
box: {
xMin: 304.6476503248806,
xMax: 502.5079975897382,
yMin: 102.16298762367356,
yMax: 349.035215984403,
width: 197.86034726485758,
height: 246.87222836072945
Renders an oriented [MediaPipe face mesh](https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results):

```jsx
const faceLandmarkerResult = {
"faceLandmarks": [
[
{ "x": 0.5760777592658997, "y": 0.8639070391654968, "z": -0.030997956171631813 },
{ "x": 0.572094738483429, "y": 0.7886289358139038, "z": -0.07189624011516571 },
// ...
],
// ...
],
"faceBlendshapes": [
// ...
],
"facialTransformationMatrixes": [
// ...
]
},
}
const points = faceLandmarkerResult.faceLandmarks[0]

<Facemesh face={face} />
<Facemesh points={points} />
```

```tsx
type FacemeshProps = {
/** a MediaPipeFaceMesh object, default: a lambda face */
export type FacemeshProps = {
/** an array of 468+ keypoints as returned by google/mediapipe tasks-vision, default: a sample face */
points?: MediaPipePoints
/** @deprecated an face object as returned by tensorflow/tfjs-models face-landmarks-detection */
face?: MediaPipeFaceMesh
/** width of the mesh, default: undefined */
/** constant width of the mesh, default: undefined */
width?: number
/** or height of the mesh, default: undefined */
/** or constant height of the mesh, default: undefined */
height?: number
/** or depth of the mesh, default: 1 */
/** or constant depth of the mesh, default: 1 */
depth?: number
/** 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) */
verticalTri?: [number, number, number]
/** a landmark index to be the origin of the mesh. default: undefined (ie. the bbox center) */
origin?: number
/** a landmark index (to get the position from) or a vec3 to be the origin of the mesh. default: undefined (ie. the bbox center) */
origin?: number | THREE.Vector3
/** 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) */
facialTransformationMatrix?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.facialTransformationMatrixes[0]
/** Apply position offset extracted from `facialTransformationMatrix` */
offset?: boolean
/** Offset sensitivity factor, less is more sensible */
offsetScalar?: number
/** Fface blendshapes, as returned by FaceLandmarkerResult.faceBlendshapes (see: https://developers.google.com/mediapipe/solutions/vision/face_landmarker/web_js#handle_and_display_results) */
faceBlendshapes?: typeof FacemeshDatas.SAMPLE_FACELANDMARKER_RESULT.faceBlendshapes[0]
/** whether to enable eyes (nb. `faceBlendshapes` is required for), default: true */
eyes?: boolean
/** Force `origin` to be the middle of the 2 eyes (nb. `eyes` is required for), default: false */
eyesAsOrigin?: boolean
/** debug mode, default: false */
debug?: boolean
}
Expand All @@ -983,20 +1078,28 @@ Ref-api:
```tsx
const api = useRef<FacemeshApi>()

<Facemesh ref={api} face={face} />
<Facemesh ref={api} points={points} />
```

```tsx
type FacemeshApi = {
meshRef: React.RefObject<THREE.Mesh>
outerRef: React.RefObject<THREE.Group>
eyeRightRef: React.RefObject<FacemeshEyeApi>
eyeLeftRef: React.RefObject<FacemeshEyeApi>
}
```

NB: `outerRef` group is oriented as your `face`. You can for example get its world direction:
You can for example get face mesh world direction:

```tsx
meshRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
api.meshRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
```

or get L/R iris direction:

```tsx
api.eyeRightRef.current.irisDirRef.current.localToWorld(new THREE.Vector3(0, 0, -1))
```

# Abstractions
Expand Down Expand Up @@ -2431,6 +2534,12 @@ const buffer = useSurfaceSampler(
)
```

### FaceLandmarker

![](https://img.shields.io/badge/-suspense-brightgreen)

A @mediapipe/tasks-vision [`FaceLandmarker`](https://developers.google.com/mediapipe/api/solutions/js/tasks-vision.facelandmarker) provider, as well as a `useFaceLandmarker` hook.

# Loading

#### Loader
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
},
"dependencies": {
"@babel/runtime": "^7.11.2",
"@mediapipe/tasks-vision": "^0.10.0",
"@react-spring/three": "~9.6.1",
"@use-gesture/react": "^10.2.24",
"camera-controls": "^2.3.1",
Expand Down
Loading