diff --git a/README.md b/README.md
index 33db723..5b05e4f 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,54 @@ Selection can be nested and group multiple object, higher up selection take prec
```
+#### Autofocus
+
+
+
+
+
+An auto-focus effect, that extends ``.
+
+```tsx
+export type AutofocusProps = typeof DepthOfField & {
+ target?: [number, number, number] // undefined
+ mouse?: boolean // false
+ debug?: number // undefined
+ manual?: boolean // false
+ smoothTime?: number // .25
+}
+```
+
+```tsx
+
+
+
+```
+
+Ref-api:
+
+```tsx
+type AutofocusApi = {
+ dofRef: RefObject
+ hitpoint: THREE.Vector3
+ update: (delta: number, updateTarget: boolean) => void
+}
+```
+
+```tsx
+
+```
+
+Associated with `manual` prop, you can for example, animate the DOF target yourself:
+
+```tsx
+useFrame((_, delta) => {
+ const api = autofocusRef.current
+ api.update(delta, false) // update hitpoint only
+ easing.damp3(api.dofRef.curent.target, api.hitpoint, 0.5, delta) // custom easing
+})
+```
+
#### Selective bloom
Bloom is selective by default, you control it not on the effect pass but on the materials by lifting their colors out of 0-1 range. a `luminanceThreshold` of 1 ensures that ootb nothing will glow, only the materials you pick. For this to work `toneMapped` has to be false on the materials, because it would otherwise clamp colors between 0 and 1 again.
diff --git a/package.json b/package.json
index 204b206..cc909f7 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"typegen": "tsc --emitDeclarationOnly || true"
},
"dependencies": {
+ "maath": "^0.5.3",
"postprocessing": "^6.30.2",
"screen-space-reflections": "2.5.0",
"three-stdlib": "^2.21.10"
diff --git a/src/Autofocus.tsx b/src/Autofocus.tsx
new file mode 100644
index 0000000..4df4115
--- /dev/null
+++ b/src/Autofocus.tsx
@@ -0,0 +1,159 @@
+import * as THREE from 'three'
+import React, {
+ useRef,
+ useContext,
+ useState,
+ useEffect,
+ useCallback,
+ forwardRef,
+ useImperativeHandle,
+ RefObject,
+} from 'react'
+import { useThree, useFrame, createPortal } from '@react-three/fiber'
+import { CopyPass, DepthPickingPass } from 'postprocessing'
+import { DepthOfField, EffectComposerContext } from './index'
+import { DepthOfFieldEffect } from 'postprocessing'
+import { easing } from 'maath'
+
+export type AutofocusProps = typeof DepthOfField & {
+ target?: [number, number, number]
+ mouse?: boolean
+ debug?: number
+ manual?: boolean
+ smoothTime?: number
+}
+
+export type AutofocusApi = {
+ dofRef: RefObject
+ hitpoint: THREE.Vector3
+ update: (delta: number, updateTarget: boolean) => void
+}
+
+export const Autofocus = forwardRef(
+ (
+ { target = undefined, mouse: followMouse = false, debug = undefined, manual = false, smoothTime = 0, ...props },
+ fref
+ ) => {
+ const dofRef = useRef(null)
+ const hitpointRef = useRef(null)
+ const targetRef = useRef(null)
+
+ const { size, gl, scene } = useThree()
+ const { composer, camera } = useContext(EffectComposerContext)
+
+ // see: https://codesandbox.io/s/depthpickingpass-x130hg
+ const [depthPickingPass] = useState(new DepthPickingPass())
+ useEffect(() => {
+ const copyPass = new CopyPass()
+ composer.addPass(depthPickingPass)
+ composer.addPass(copyPass)
+ return () => {
+ composer.removePass(copyPass)
+ composer.removePass(depthPickingPass)
+ }
+ }, [composer, depthPickingPass])
+
+ const [hitpoint] = useState(new THREE.Vector3(0, 0, 0))
+
+ const [ndc] = useState(new THREE.Vector3(0, 0, 0))
+ const getHit = useCallback(
+ async (x: number, y: number) => {
+ ndc.x = x
+ ndc.y = y
+ ndc.z = await depthPickingPass.readDepth(ndc)
+ ndc.z = ndc.z * 2.0 - 1.0
+ const hit = 1 - ndc.z > 0.0000001 // it is missed if ndc.z is close to 1
+ return hit ? ndc.unproject(camera) : false
+ },
+ [ndc, depthPickingPass, camera]
+ )
+
+ const [pointer] = useState(new THREE.Vector2())
+ useEffect(() => {
+ if (!followMouse) return
+
+ async function onPointermove(e: PointerEvent) {
+ const clientX = e.clientX - size.left
+ const clientY = e.clientY - size.top
+ const x = (clientX / size.width) * 2.0 - 1.0
+ const y = -(clientY / size.height) * 2.0 + 1.0
+
+ pointer.set(x, y)
+ }
+ gl.domElement.addEventListener('pointermove', onPointermove, {
+ passive: true,
+ })
+
+ return () => void gl.domElement.removeEventListener('pointermove', onPointermove)
+ }, [gl.domElement, hitpoint, size, followMouse, getHit, pointer])
+
+ const update = useCallback(
+ async (delta: number, updateTarget = true) => {
+ // Update hitpoint
+ if (target) {
+ hitpoint.set(...target)
+ } else {
+ const { x, y } = followMouse ? pointer : { x: 0, y: 0 }
+ const hit = await getHit(x, y)
+ if (hit) hitpoint.copy(hit)
+ }
+
+ // Update target
+ if (updateTarget && dofRef.current?.target) {
+ if (smoothTime > 0 && delta > 0) {
+ easing.damp3(dofRef.current.target, hitpoint, smoothTime, delta)
+ } else {
+ dofRef.current.target.copy(hitpoint)
+ }
+ }
+ },
+ [target, hitpoint, followMouse, getHit, smoothTime, pointer]
+ )
+
+ useFrame(async (_, delta) => {
+ if (manual) return
+ update(delta)
+ })
+
+ useFrame(() => {
+ if (hitpointRef.current) {
+ hitpointRef.current.position.copy(hitpoint)
+ }
+ if (targetRef.current && dofRef.current?.target) {
+ targetRef.current.position.copy(dofRef.current.target)
+ }
+ })
+
+ // Ref API
+ useImperativeHandle(
+ fref,
+ () => ({
+ dofRef,
+ hitpoint,
+ update,
+ }),
+ [hitpoint, update]
+ )
+
+ return (
+ <>
+ {debug &&
+ createPortal(
+ <>
+
+
+
+
+
+
+
+
+ >,
+ scene
+ )}
+
+
+ >
+ )
+ }
+)
diff --git a/src/index.tsx b/src/index.tsx
index d7e31f6..41de17e 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -30,5 +30,6 @@ export * from './effects/TiltShift2'
export * from './effects/SSR'
export * from './Selection'
+export * from './Autofocus'
export * from './EffectComposer'
export * from './util'
diff --git a/yarn.lock b/yarn.lock
index f965795..9456971 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3219,6 +3219,11 @@ lru-cache@^9.0.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1"
integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==
+maath@^0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/maath/-/maath-0.5.3.tgz#777a1f9b8463c6ffb199ea43406874a357c0cd58"
+ integrity sha512-ut63A4zTd9abtpi+sOHW1fPWPtAFrjK0E17eAthx1k93W/T2cWLKV5oaswyotJVDvvW1EXSdokAqhK5KOu0Qdw==
+
magic-string@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"