Skip to content
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
2 changes: 1 addition & 1 deletion docs/effects/autofocus.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ nav: 1

An auto-focus effect, that extends `<DepthOfField>`.

Based on [ektogamat/AutoFocusDOF](https://github.com/ektogamat/AutoFocusDOF).
Based on [ektogamat/AutoFocusDOF](https://github.com/ektogamat/AutoFocusDOF). Built on `<DepthPicking>` and `useDepthPicking` internally - use those directly if you want a picked position for something other than `<DepthOfField>`'s own focus target.

```tsx
export type AutofocusProps = typeof DepthOfField & {
Expand Down
73 changes: 73 additions & 0 deletions docs/passes/depth-picking.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: DepthPicking
nav: 1
---

Mounts a `postprocessing` `DepthPickingPass` and exposes its `readDepth` via ref - nothing else. Renders nothing, updates nothing automatically. Pair it with `useDepthPicking` for a world-space position instead of raw depth.

`<Autofocus>` is built on both of these - use them directly when you want a picked position for something other than `<DepthOfField>`'s own focus target.

```tsx
<EffectComposer>
<DepthPicking ref={pickRef} />
</EffectComposer>
```

Ref-api:

```tsx
type DepthPickingApi = {
readDepth: (ndc: THREE.Vector2 | THREE.Vector3) => Promise<number>
}
```

## `useDepthPicking`

```tsx
function useDepthPicking(
pass: RefObject<DepthPickingApi | null>,
camera?: THREE.Camera, // defaults to the composer's camera, then r3f's own
): (x: number, y: number) => Promise<THREE.Vector3 | false>
```

A hook that turns a screen position into a world-space point, using a mounted `<DepthPicking>`'s `readDepth`. Returns `false` if nothing was hit. Call it whenever you want - on click, on hover, every frame - nothing runs on its own. It only needs a ref to the mounted pass, not the `<EffectComposer>` context itself, so it works anywhere under `<Canvas>`:

```tsx
const pickRef = useRef<DepthPickingApi>(null)

function Cursor() {
const getHit = useDepthPicking(pickRef)
const meshRef = useRef<THREE.Mesh>(null)
useFrame(async ({ pointer }) => {
const hit = await getHit(pointer.x, pointer.y)
if (hit) meshRef.current?.position.copy(hit)
})
return (
<mesh ref={meshRef}>
<sphereGeometry args={[0.1, 16, 16]} />
{/* depthWrite false - see the warning below */}
<meshBasicMaterial color="white" depthWrite={false} />
</mesh>
)
}

return (
<>
<EffectComposer>
<DepthPicking ref={pickRef} />
</EffectComposer>
<Cursor />
</>
)
```

It unprojects using the `<EffectComposer>`'s own camera when called from inside it - the same one the pass renders depth from - falling back to r3f's own camera otherwise. Called from outside that `<EffectComposer>` (like `Cursor` above) with a non-default camera, pass that same camera as the second argument explicitly.

**If you render something at the picked position** (a cursor, a placement preview, ...), give its material `depthWrite={false}`. Depth is read from the same buffer everything else renders into - without this, your own marker sits at the last hitpoint, gets sampled by the *next* pick as the closest surface there, and the marker creeps toward the camera every frame, faster as it gets closer, until it resets and repeats. (`Autofocus`'s own debug markers already do this.)

Click-to-pick instead of every frame:

```tsx
const hit = await getHit(pointerNdcX, pointerNdcY)
if (hit) character.moveTo(hit)
```
81 changes: 23 additions & 58 deletions src/effects/Autofocus.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { createPortal, useFrame, useThree, type Vector3 as R3FVector3 } from '@react-three/fiber'
import { easing } from 'maath'
import { DepthOfFieldEffect, DepthPickingPass } from 'postprocessing'
import { DepthOfFieldEffect } from 'postprocessing'
import {
Ref,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
Expand All @@ -15,7 +13,7 @@ import {
} from 'react'
import { Mesh, Vector3 } from 'three'

import { EffectComposerContext } from '../EffectComposer'
import { DepthPicking, useDepthPicking, type DepthPickingApi } from '../passes/DepthPicking'
import { DepthOfField } from './DepthOfField'

export type AutofocusProps = Omit<ComponentProps<typeof DepthOfField>, 'ref'> & {
Expand Down Expand Up @@ -47,49 +45,22 @@ export function Autofocus({
...props
}: AutofocusProps) {
const dofRef = useRef<DepthOfFieldEffect>(null)
const hitpointRef = useRef<Mesh>(null)
const targetRef = useRef<Mesh>(null)
const pickRef = useRef<DepthPickingApi>(null)
const getHit = useDepthPicking(pickRef)
const hitpointMarkerRef = useRef<Mesh>(null)
const dofTargetMarkerRef = useRef<Mesh>(null)

const scene = useThree(({ scene }) => scene)
const pointer = useThree(({ pointer }) => pointer)
const { composer, camera } = useContext(EffectComposerContext)

const [depthPickingPass] = useState(() => new DepthPickingPass())
useEffect(() => {
// Fixed early index (right after RenderPass, which is always index 0),
// not appended - so this never risks becoming the structurally-last
// pass and silently stealing renderToScreen from whatever the real
// last pass is, regardless of what else adds/removes passes and when.
composer.addPass(depthPickingPass, 1)
return () => {
composer.removePass(depthPickingPass)
}
}, [composer, depthPickingPass])

useEffect(() => {
return () => {
depthPickingPass.dispose()
}
}, [depthPickingPass])

const [hitpoint] = useState(() => new Vector3(0, 0, 0))

const [ndc] = useState(() => new 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]
)
// A stable non-null value, purely to enable DepthOfField's own autoFocus
// mode (`target != null`) - the actual per-frame value is applied
// imperatively to dofRef.current.target below.
const [autoFocusMarker] = useState(() => new Vector3())
const [hitpoint] = useState(() => new Vector3())

const update = useCallback(
async (delta: number, updateTarget = true) => {
// Update hitpoint
if (target) {
hitpoint.set(...(target as unknown as [number, number, number]))
} else {
Expand All @@ -98,7 +69,6 @@ export function Autofocus({
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)
Expand All @@ -107,42 +77,37 @@ export function Autofocus({
}
}
},
[target, hitpoint, followMouse, getHit, smoothTime, pointer]
[target, hitpoint, followMouse, pointer, getHit, smoothTime]
)

useFrame(async (_, delta) => {
useFrame((_, delta) => {
if (!manual) {
update(delta)
}
if (hitpointRef.current) {
hitpointRef.current.position.copy(hitpoint)
if (hitpointMarkerRef.current) {
hitpointMarkerRef.current.position.copy(hitpoint)
}
if (targetRef.current && dofRef.current?.target) {
targetRef.current.position.copy(dofRef.current.target)
if (dofTargetMarkerRef.current && dofRef.current?.target) {
dofTargetMarkerRef.current.position.copy(dofRef.current.target)
}
})

// Ref API
const api = useMemo<AutofocusApi>(
() => ({
dofRef,
hitpoint,
update,
}),
[hitpoint, update]
)
const api = useMemo<AutofocusApi>(() => ({ dofRef, hitpoint, update }), [hitpoint, update])
useImperativeHandle(ref, () => api, [api])

return (
<>
<DepthPicking ref={pickRef} />

{debug
? createPortal(
<>
<mesh ref={hitpointRef}>
<mesh ref={hitpointMarkerRef}>
<sphereGeometry args={[debug, 16, 16]} />
<meshBasicMaterial color="#00ff00" opacity={1} transparent depthWrite={false} />
</mesh>
<mesh ref={targetRef}>
<mesh ref={dofTargetMarkerRef}>
<sphereGeometry args={[debug / 2, 16, 16]} />
<meshBasicMaterial color="#00ff00" opacity={0.5} transparent depthWrite={false} />
</mesh>
Expand All @@ -151,7 +116,7 @@ export function Autofocus({
)
: null}

<DepthOfField ref={dofRef} {...props} target={hitpoint} />
<DepthOfField ref={dofRef} {...props} target={autoFocusMarker} />
</>
)
}
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ export * from './effects/ToneMapping'
export * from './effects/Vignette'
export * from './effects/Water'

// These are not effect passes
export * from './effects/N8AO'
export * from './passes/DepthPicking'
export * from './passes/N8AO'
63 changes: 63 additions & 0 deletions src/passes/DepthPicking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useThree } from '@react-three/fiber'
import { CopyPass, DepthPickingPass as DepthPickingPassImpl } from 'postprocessing'
import { use, useCallback, useEffect, useImperativeHandle, useState, type Ref } from 'react'
import type { Camera } from 'three'
import { Vector2, Vector3 } from 'three'
import { EffectComposerContext } from '../EffectComposer'
import { useDispose } from '../util'

export type DepthPickingApi = {
readDepth: (ndc: Vector2 | Vector3) => Promise<number>
}

export type DepthPickingProps = {
ref?: Ref<DepthPickingApi>
}

// Mounts a vanilla `postprocessing` DepthPickingPass and exposes its
// `readDepth` via ref - nothing else. Renders nothing. Pair with
// `useDepthPicking` for a world-space position instead of raw depth.
export function DepthPicking({ ref }: DepthPickingProps) {
const { composer } = use(EffectComposerContext)

const [depthPickingPass] = useState(() => new DepthPickingPassImpl())
const [copyPass] = useState(() => new CopyPass())
useEffect(() => {
composer.addPass(depthPickingPass)
composer.addPass(copyPass)
return () => {
composer.removePass(depthPickingPass)
composer.removePass(copyPass)
}
}, [composer, depthPickingPass, copyPass])

useDispose(depthPickingPass)
useDispose(copyPass)

useImperativeHandle(ref, () => ({ readDepth: (ndc) => depthPickingPass.readDepth(ndc) }), [depthPickingPass])

return null
}

export function useDepthPicking(pass: React.RefObject<DepthPickingApi | null>, camera?: Camera) {
const composerCamera = use(EffectComposerContext)?.camera
const defaultCamera = useThree((state) => state.camera)
const resolvedCamera = camera ?? composerCamera ?? defaultCamera
const [ndc] = useState(() => new Vector3())

return useCallback(
async (x: number, y: number): Promise<Vector3 | false> => {
if (!pass.current) return false
ndc.x = x
ndc.y = y
ndc.z = await pass.current.readDepth(ndc)
ndc.z = ndc.z * 2.0 - 1.0
const hit = 1 - ndc.z > 0.0000001 // missed if ndc.z is close to 1
// clone - unproject mutates in place, and ndc is reused across calls,
// so returning it directly would hand out a reference that changes
// under the caller on the next pick.
return hit ? ndc.clone().unproject(resolvedCamera) : false
},
[pass, resolvedCamera, ndc]
)
}
File renamed without changes.
Loading