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

+ Autofocus demo +

+ +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"