Skip to content
Open
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
74 changes: 48 additions & 26 deletions src/effects/DepthOfField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Ref } from 'react'
import { use, useMemo } from 'react'
import { type DepthPackingStrategies, type Texture, Vector3 } from 'three'
import { EffectComposerContext } from '../EffectComposer'
import { useDispose } from '../util'
import { applyPierced, readPierced, useDispose, useLiveDefaults } from '../util'

export type DepthOfFieldProps = ConstructorParameters<typeof DepthOfFieldEffect>[1] &
Partial<{
Expand All @@ -19,6 +19,37 @@ export type DepthOfFieldProps = ConstructorParameters<typeof DepthOfFieldEffect>
blur: number
}>

// Only bokehScale, focusDistance/focusRange (via the nested cocMaterial),
// depthTexture (via setDepthTexture) and blendFunction have real setters in
// postprocessing - every resolution option is construction-only. camera
// being a required constructor arg also rules out createEffectComponent
// (needs `new Effect()` to work with zero args).
const LIVE_KEYS = [
'blendMode-blendFunction',
'bokehScale',
'cocMaterial-focusDistance',
'cocMaterial-focusRange',
'depthTexture',
]

// cocMaterial.depthBuffer/depthPacking are write-only in postprocessing
// (setters with no matching getters) - depthPacking can't be read back at
// all, so a reverted default always re-applies BasicDepthPacking (the same
// value setDepthTexture itself defaults to when packing is omitted).
function get(effect: DepthOfFieldEffect, key: string): unknown {
if (key !== 'depthTexture') return readPierced(effect, key)
const texture = (effect.cocMaterial as unknown as { uniforms: { depthBuffer: { value: unknown } } }).uniforms
.depthBuffer.value
return texture ? { texture } : undefined
}

function set(effect: DepthOfFieldEffect, key: string, value: unknown): void {
if (key === 'depthTexture') {
const dt = value as { texture?: Texture; packing?: DepthPackingStrategies } | undefined
effect.setDepthTexture(dt?.texture as never, dt?.packing)
} else applyPierced(effect, key, value)
}

export function DepthOfField({
ref,
blendFunction,
Expand All @@ -42,13 +73,9 @@ export function DepthOfField({

const effect = useMemo(() => {
const effect = new DepthOfFieldEffect(camera, {
blendFunction,
worldFocusDistance,
worldFocusRange,
focusDistance,
focusRange,
focalLength,
bokehScale,
resolutionScale,
resolutionX,
resolutionY,
Expand All @@ -57,29 +84,24 @@ export function DepthOfField({
})
// Creating a target enables autofocus, R3F will set via props
if (autoFocus) effect.target = new Vector3()
// Depth texture for depth picking with optional packing strategy
if (depthTexture) effect.setDepthTexture(depthTexture.texture, depthTexture.packing as DepthPackingStrategies)
// Temporary fix that restores DOF 6.21.3 behavior, everything since then lets shapes leak through the blur
const maskPass = (effect as any).maskPass
maskPass.maskFunction = MaskFunction.MULTIPLY_RGB_SET_ALPHA
effect.maskFunction = MaskFunction.MULTIPLY_RGB_SET_ALPHA
return effect
}, [
camera,
blendFunction,
worldFocusDistance,
worldFocusRange,
focusDistance,
focusRange,
focalLength,
bokehScale,
resolutionScale,
resolutionX,
resolutionY,
width,
height,
autoFocus,
depthTexture,
])
}, [camera, worldFocusDistance, worldFocusRange, focalLength, resolutionScale, resolutionX, resolutionY, width, height, autoFocus])

useLiveDefaults(
effect,
{
'blendMode-blendFunction': blendFunction,
bokehScale,
'cocMaterial-focusDistance': focusDistance,
'cocMaterial-focusRange': focusRange,
depthTexture,
},
LIVE_KEYS,
get,
set
)

useDispose(effect)

Expand Down
88 changes: 82 additions & 6 deletions src/effects/GodRays.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,94 @@
import { useThree } from '@react-three/fiber'
import { GodRaysEffect } from 'postprocessing'
import { Ref, RefObject, useContext, useLayoutEffect, useMemo } from 'react'
import { Ref, RefObject, use, useLayoutEffect, useMemo } from 'react'
import { Mesh, Points } from 'three'
import { EffectComposerContext } from '../EffectComposer'
import { resolveRef, useDispose } from '../util'
import { applyPierced, readPierced, resolveRef, useDispose, useLiveDefaults } from '../util'

type GodRaysProps = ConstructorParameters<typeof GodRaysEffect>[2] & {
sun: Mesh | Points | RefObject<Mesh | Points>
ref?: Ref<GodRaysEffect>
}

export function GodRays({ ref, ...props }: GodRaysProps) {
const { camera } = useContext(EffectComposerContext)
const effect = useMemo(() => new GodRaysEffect(camera, resolveRef(props.sun), props), [camera, props])
useLayoutEffect(() => void (effect.lightSource = resolveRef(props.sun)), [effect, props.sun])
// GodRaysMaterial (godRaysMaterial) is where density/decay/weight/exposure
// actually live - clampMax maps to its differently-named maxIntensity.
// resolutionScale/resolutionX/resolutionY have no setter at all in
// postprocessing - construction-only. camera+sun being required constructor
// args also rule out createEffectComponent (needs `new Effect()` to work
// with zero args).
const LIVE_KEYS = [
'blendMode-blendFunction',
'godRaysMaterial-density',
'godRaysMaterial-decay',
'godRaysMaterial-weight',
'godRaysMaterial-exposure',
'clampMax',
'blur',
'kernelSize',
'samples',
'width',
'height',
]

function get(effect: GodRaysEffect, key: string): unknown {
return key === 'clampMax' ? effect.godRaysMaterial.maxIntensity : readPierced(effect, key)
}

function set(effect: GodRaysEffect, key: string, value: unknown): void {
if (key === 'clampMax') effect.godRaysMaterial.maxIntensity = value as number
else applyPierced(effect, key, value)
}

export function GodRays({
sun,
blendFunction,
density,
decay,
weight,
exposure,
clampMax,
blur,
kernelSize,
samples,
width,
height,
resolutionScale,
resolutionX,
resolutionY,
ref,
}: GodRaysProps) {
const { camera } = use(EffectComposerContext)
const invalidate = useThree((state) => state.invalidate)

const effect = useMemo(
() => new GodRaysEffect(camera, resolveRef(sun), { resolutionScale, resolutionX, resolutionY }),
[camera, resolutionScale, resolutionX, resolutionY]
)

useLayoutEffect(() => {
effect.lightSource = resolveRef(sun)
invalidate()
}, [effect, sun, invalidate])

useLiveDefaults(
effect,
{
'blendMode-blendFunction': blendFunction,
'godRaysMaterial-density': density,
'godRaysMaterial-decay': decay,
'godRaysMaterial-weight': weight,
'godRaysMaterial-exposure': exposure,
clampMax,
blur,
kernelSize,
samples,
width,
height,
},
LIVE_KEYS,
get,
set
)

useDispose(effect)

Expand Down
20 changes: 9 additions & 11 deletions src/effects/LUT.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useThree } from '@react-three/fiber'
import { BlendFunction, LUT3DEffect } from 'postprocessing'
import { Ref, useLayoutEffect, useMemo } from 'react'
import { Ref, useMemo } from 'react'
import type { Texture } from 'three'
import { useDispose } from '../util'
import { useDispose, useLiveDefaults } from '../util'

export type LUTProps = {
lut: Texture
Expand All @@ -11,16 +10,15 @@ export type LUTProps = {
ref?: Ref<LUT3DEffect>
}

export function LUT({ lut, tetrahedralInterpolation, ref, ...props }: LUTProps) {
const effect = useMemo(() => new LUT3DEffect(lut, props), [lut, props])
const invalidate = useThree((state) => state.invalidate)
const LIVE_KEYS = ['blendMode-blendFunction', 'lut', 'tetrahedralInterpolation']

useLayoutEffect(() => {
if (tetrahedralInterpolation) effect.tetrahedralInterpolation = tetrahedralInterpolation
if (lut) effect.lut = lut
invalidate()
}, [effect, invalidate, lut, tetrahedralInterpolation])
// lut is LUT3DEffect's required constructor arg (no default) - only used
// for the initial instance, later changes go through its own live setter
// (via useLiveDefaults below) instead of reconstructing.
export function LUT({ lut, blendFunction, tetrahedralInterpolation, ref }: LUTProps) {
const effect = useMemo(() => new LUT3DEffect(lut), [])

useLiveDefaults(effect, { 'blendMode-blendFunction': blendFunction, lut, tetrahedralInterpolation }, LIVE_KEYS)
useDispose(effect)

return <primitive ref={ref} object={effect} />
Expand Down
13 changes: 10 additions & 3 deletions src/effects/N8AO.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function N8AO({
renderMode = 0,
ref,
}: N8AOProps) {
const { camera, scene } = useThree()
const { camera, scene, invalidate } = useThree()
const effect = useMemo(() => new N8AOPostPass(scene, camera), [camera, scene])

// TODO: implement dispose upstream; this effect has memory leaks without
Expand All @@ -58,6 +58,9 @@ export function N8AO({
halfRes,
depthAwareUpsampling,
})
// effect.configuration is a plain object, never r3f-managed - applyProps'
// own invalidate (gated behind object.__r3f) never fires for it.
invalidate()
}, [
screenSpaceRadius,
color,
Expand All @@ -71,11 +74,15 @@ export function N8AO({
halfRes,
depthAwareUpsampling,
effect,
invalidate,
])

useLayoutEffect(() => {
if (quality) effect.setQualityMode(quality.charAt(0).toUpperCase() + quality.slice(1))
}, [effect, quality])
if (quality) {
effect.setQualityMode(quality.charAt(0).toUpperCase() + quality.slice(1))
invalidate()
}
}, [effect, quality, invalidate])

return <primitive ref={ref} object={effect} />
}
Loading