|
| 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 | +) |
0 commit comments