Skip to content

Commit 7593ea8

Browse files
committed
feat(Autofocus): new effect
1 parent 4865a95 commit 7593ea8

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed

README.md

+48
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,54 @@ Selection can be nested and group multiple object, higher up selection take prec
123123
</Select>
124124
```
125125

126+
#### Autofocus
127+
128+
<p>
129+
<a href="https://codesandbox.io/s/dfw6w4"><img width="20%" src="https://codesandbox.io/api/v1/sandboxes/dfw6w4/thumbnail.png" alt="Autofocus demo"/></a>
130+
</p>
131+
132+
An auto-focus effect, that extends `<DepthOfField>`.
133+
134+
```tsx
135+
export type AutofocusProps = typeof DepthOfField & {
136+
target?: [number, number, number] // undefined
137+
mouse?: boolean // false
138+
debug?: number // undefined
139+
manual?: boolean // false
140+
smoothTime?: number // .25
141+
}
142+
```
143+
144+
```tsx
145+
<EffectComposer>
146+
<Autofocus />
147+
</EffectComposer>
148+
```
149+
150+
Ref-api:
151+
152+
```tsx
153+
type AutofocusApi = {
154+
dofRef: RefObject<DepthOfFieldEffect>
155+
hitpoint: THREE.Vector3
156+
update: (delta: number, updateTarget: boolean) => void
157+
}
158+
```
159+
160+
```tsx
161+
<Autofocus ref={autofocusRef} />
162+
```
163+
164+
Associated with `manual` prop, you can for example, animate the DOF target yourself:
165+
166+
```tsx
167+
useFrame((_, delta) => {
168+
const api = autofocusRef.current
169+
api.update(delta, false) // update hitpoint only
170+
easing.damp3(api.dofRef.curent.target, api.hitpoint, 0.5, delta) // custom easing
171+
})
172+
```
173+
126174
#### Selective bloom
127175
128176
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.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"typegen": "tsc --emitDeclarationOnly || true"
4545
},
4646
"dependencies": {
47+
"maath": "^0.5.3",
4748
"postprocessing": "^6.30.2",
4849
"screen-space-reflections": "2.5.0",
4950
"three-stdlib": "^2.21.10"

src/Autofocus.tsx

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import * as THREE from 'three'
2+
import React, {
3+
useRef,
4+
useContext,
5+
useState,
6+
useEffect,
7+
useCallback,
8+
forwardRef,
9+
useImperativeHandle,
10+
RefObject,
11+
} from 'react'
12+
import { useThree, useFrame, createPortal } from '@react-three/fiber'
13+
import { CopyPass, DepthPickingPass } from 'postprocessing'
14+
import { DepthOfField, EffectComposerContext } from './index'
15+
import { DepthOfFieldEffect } from 'postprocessing'
16+
import { easing } from 'maath'
17+
18+
export type AutofocusProps = typeof DepthOfField & {
19+
target?: [number, number, number]
20+
mouse?: boolean
21+
debug?: number
22+
manual?: boolean
23+
smoothTime?: number
24+
}
25+
26+
export type AutofocusApi = {
27+
dofRef: RefObject<DepthOfFieldEffect>
28+
hitpoint: THREE.Vector3
29+
update: (delta: number, updateTarget: boolean) => void
30+
}
31+
32+
export const Autofocus = forwardRef<AutofocusApi, AutofocusProps>(
33+
(
34+
{ target = undefined, mouse: followMouse = false, debug = undefined, manual = false, smoothTime = 0, ...props },
35+
fref
36+
) => {
37+
const dofRef = useRef<DepthOfFieldEffect>(null)
38+
const hitpointRef = useRef<THREE.Mesh>(null)
39+
const targetRef = useRef<THREE.Mesh>(null)
40+
41+
const { size, gl, scene } = useThree()
42+
const { composer, camera } = useContext(EffectComposerContext)
43+
44+
// see: https://codesandbox.io/s/depthpickingpass-x130hg
45+
const [depthPickingPass] = useState(new DepthPickingPass())
46+
useEffect(() => {
47+
const copyPass = new CopyPass()
48+
composer.addPass(depthPickingPass)
49+
composer.addPass(copyPass)
50+
return () => {
51+
composer.removePass(copyPass)
52+
composer.removePass(depthPickingPass)
53+
}
54+
}, [composer, depthPickingPass])
55+
56+
const [hitpoint] = useState(new THREE.Vector3(0, 0, 0))
57+
58+
const [ndc] = useState(new THREE.Vector3(0, 0, 0))
59+
const getHit = useCallback(
60+
async (x: number, y: number) => {
61+
ndc.x = x
62+
ndc.y = y
63+
ndc.z = await depthPickingPass.readDepth(ndc)
64+
ndc.z = ndc.z * 2.0 - 1.0
65+
const hit = 1 - ndc.z > 0.0000001 // it is missed if close to 1
66+
return hit ? ndc.unproject(camera) : false
67+
},
68+
[ndc, depthPickingPass, camera]
69+
)
70+
71+
const [pointer] = useState(new THREE.Vector2())
72+
useEffect(() => {
73+
if (!followMouse) return
74+
75+
async function onPointermove(e: PointerEvent) {
76+
const clientX = e.clientX - size.left
77+
const clientY = e.clientY - size.top
78+
const x = (clientX / size.width) * 2.0 - 1.0
79+
const y = -(clientY / size.height) * 2.0 + 1.0
80+
81+
pointer.set(x, y)
82+
}
83+
gl.domElement.addEventListener('pointermove', onPointermove, {
84+
passive: true,
85+
})
86+
87+
return () => void gl.domElement.removeEventListener('pointermove', onPointermove)
88+
}, [gl.domElement, hitpoint, size, followMouse, getHit, pointer])
89+
90+
const update = useCallback(
91+
async (delta: number, updateTarget = true) => {
92+
// Update hitpoint
93+
if (target) {
94+
hitpoint.set(...target)
95+
} else {
96+
const { x, y } = followMouse ? pointer : { x: 0, y: 0 }
97+
const hit = await getHit(x, y)
98+
if (hit) hitpoint.copy(hit)
99+
}
100+
101+
// Update target
102+
if (updateTarget && dofRef.current?.target) {
103+
if (smoothTime > 0 && delta > 0) {
104+
easing.damp3(dofRef.current.target, hitpoint, smoothTime, delta)
105+
} else {
106+
dofRef.current.target.copy(hitpoint)
107+
}
108+
}
109+
},
110+
[target, hitpoint, followMouse, getHit, smoothTime, pointer]
111+
)
112+
113+
useFrame(async (_, delta) => {
114+
if (manual) return
115+
update(delta)
116+
})
117+
118+
useFrame(() => {
119+
if (hitpointRef.current) {
120+
hitpointRef.current.position.copy(hitpoint)
121+
}
122+
if (targetRef.current && dofRef.current?.target) {
123+
targetRef.current.position.copy(dofRef.current.target)
124+
}
125+
})
126+
127+
// Ref API
128+
useImperativeHandle(
129+
fref,
130+
() => ({
131+
dofRef,
132+
hitpoint,
133+
update,
134+
}),
135+
[hitpoint, update]
136+
)
137+
138+
return (
139+
<>
140+
{debug &&
141+
createPortal(
142+
<>
143+
<mesh ref={hitpointRef}>
144+
<sphereGeometry args={[debug, 16, 16]} />
145+
<meshBasicMaterial color="#00ff00" opacity={1} transparent depthWrite={false} />
146+
</mesh>
147+
<mesh ref={targetRef}>
148+
<sphereGeometry args={[debug / 2, 16, 16]} />
149+
<meshBasicMaterial color="#00ff00" opacity={0.5} transparent depthWrite={false} />
150+
</mesh>
151+
</>,
152+
scene
153+
)}
154+
155+
<DepthOfField ref={dofRef} {...props} target={hitpoint} />
156+
</>
157+
)
158+
}
159+
)

src/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ export * from './effects/TiltShift2'
3030
export * from './effects/SSR'
3131

3232
export * from './Selection'
33+
export * from './Autofocus'
3334
export * from './EffectComposer'
3435
export * from './util'

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3219,6 +3219,11 @@ lru-cache@^9.0.0:
32193219
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1"
32203220
integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==
32213221

3222+
maath@^0.5.3:
3223+
version "0.5.3"
3224+
resolved "https://registry.yarnpkg.com/maath/-/maath-0.5.3.tgz#777a1f9b8463c6ffb199ea43406874a357c0cd58"
3225+
integrity sha512-ut63A4zTd9abtpi+sOHW1fPWPtAFrjK0E17eAthx1k93W/T2cWLKV5oaswyotJVDvvW1EXSdokAqhK5KOu0Qdw==
3226+
32223227
magic-string@^0.27.0:
32233228
version "0.27.0"
32243229
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"

0 commit comments

Comments
 (0)