@react-three/postprocessing 3.0.5 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +6 -7
  2. package/dist/EffectComposer.d.ts +9 -2
  3. package/dist/EffectGroup.d.ts +9 -0
  4. package/dist/createEffectComponent.d.ts +30 -0
  5. package/dist/effects/ASCII.d.ts +28 -7
  6. package/dist/effects/Autofocus.d.ts +1 -1
  7. package/dist/effects/Bloom.d.ts +9 -1
  8. package/dist/effects/BrightnessContrast.d.ts +14 -1
  9. package/dist/effects/ChromaticAberration.d.ts +26 -3
  10. package/dist/effects/ColorAverage.d.ts +7 -7
  11. package/dist/effects/ColorDepth.d.ts +7 -1
  12. package/dist/effects/Depth.d.ts +12 -1
  13. package/dist/effects/DotScreen.d.ts +14 -1
  14. package/dist/effects/FXAA.d.ts +10 -1
  15. package/dist/effects/Glitch.d.ts +16 -11
  16. package/dist/effects/GodRays.d.ts +1 -1
  17. package/dist/effects/Grid.d.ts +7 -6
  18. package/dist/effects/HueSaturation.d.ts +14 -1
  19. package/dist/effects/LUT.d.ts +1 -1
  20. package/dist/effects/LensFlare.d.ts +46 -5
  21. package/dist/effects/Noise.d.ts +7 -1
  22. package/dist/effects/Outline.d.ts +6 -3
  23. package/dist/effects/Pixelation.d.ts +1 -1
  24. package/dist/effects/Ramp.d.ts +58 -6
  25. package/dist/effects/SMAA.d.ts +9 -1
  26. package/dist/effects/SSAO.d.ts +1 -1
  27. package/dist/effects/ScanlineEffect.d.ts +12 -1
  28. package/dist/effects/SelectiveBloom.d.ts +1 -1
  29. package/dist/effects/Sepia.d.ts +12 -1
  30. package/dist/effects/ShockWave.d.ts +11 -1
  31. package/dist/effects/Texture.d.ts +4 -4
  32. package/dist/effects/TiltShift.d.ts +9 -1
  33. package/dist/effects/TiltShift2.d.ts +44 -4
  34. package/dist/effects/ToneMapping.d.ts +9 -3
  35. package/dist/effects/Vignette.d.ts +18 -1
  36. package/dist/effects/Water.d.ts +17 -1
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.js +2205 -713
  39. package/dist/index.js.map +1 -1
  40. package/dist/passes/DepthPicking.d.ts +11 -0
  41. package/dist/{effects → passes}/N8AO.d.ts +2 -1
  42. package/dist/util.d.ts +9 -15
  43. package/dist/wrapEffect.d.ts +3 -3
  44. package/package.json +6 -5
  45. package/src/EffectComposer.tsx +143 -42
  46. package/src/EffectGroup.tsx +74 -0
  47. package/src/createEffectComponent.tsx +71 -0
  48. package/src/effects/ASCII.tsx +75 -28
  49. package/src/effects/Autofocus.tsx +24 -77
  50. package/src/effects/Bloom.tsx +32 -4
  51. package/src/effects/BrightnessContrast.tsx +5 -2
  52. package/src/effects/ChromaticAberration.tsx +13 -21
  53. package/src/effects/ColorAverage.tsx +3 -16
  54. package/src/effects/ColorDepth.tsx +23 -2
  55. package/src/effects/Depth.tsx +4 -2
  56. package/src/effects/DepthOfField.tsx +48 -26
  57. package/src/effects/DotScreen.tsx +5 -2
  58. package/src/effects/FXAA.tsx +4 -2
  59. package/src/effects/Glitch.tsx +26 -31
  60. package/src/effects/GodRays.tsx +90 -6
  61. package/src/effects/Grid.tsx +17 -18
  62. package/src/effects/HueSaturation.tsx +5 -2
  63. package/src/effects/LUT.tsx +9 -11
  64. package/src/effects/LensFlare.tsx +209 -55
  65. package/src/effects/Noise.tsx +14 -2
  66. package/src/effects/Outline.tsx +55 -56
  67. package/src/effects/Pixelation.tsx +11 -8
  68. package/src/effects/Ramp.tsx +96 -7
  69. package/src/effects/SMAA.tsx +18 -2
  70. package/src/effects/SSAO.tsx +130 -22
  71. package/src/effects/ScanlineEffect.tsx +6 -6
  72. package/src/effects/SelectiveBloom.tsx +26 -44
  73. package/src/effects/Sepia.tsx +4 -2
  74. package/src/effects/ShockWave.tsx +30 -2
  75. package/src/effects/Texture.tsx +10 -9
  76. package/src/effects/TiltShift.tsx +30 -2
  77. package/src/effects/TiltShift2.tsx +72 -6
  78. package/src/effects/ToneMapping.tsx +16 -3
  79. package/src/effects/Vignette.tsx +5 -2
  80. package/src/effects/Water.tsx +21 -6
  81. package/src/index.ts +4 -2
  82. package/src/passes/DepthPicking.tsx +63 -0
  83. package/src/{effects → passes}/N8AO.tsx +28 -4
  84. package/src/tests/Autofocus.test.tsx +38 -0
  85. package/src/tests/Bloom.test.tsx +76 -0
  86. package/src/tests/ChromaticAberration.test.tsx +50 -0
  87. package/src/tests/ColorDepth.test.tsx +71 -0
  88. package/src/tests/DepthOfField.test.tsx +130 -0
  89. package/src/tests/DepthPicking.test.tsx +250 -0
  90. package/src/tests/EffectComposer.test.tsx +501 -8
  91. package/src/tests/EffectGroup.test.tsx +337 -0
  92. package/src/tests/Glitch.test.tsx +56 -0
  93. package/src/tests/GodRays.test.tsx +101 -0
  94. package/src/tests/Grid.test.tsx +34 -0
  95. package/src/tests/LUT.test.tsx +64 -0
  96. package/src/tests/N8AO.test.tsx +92 -0
  97. package/src/tests/Outline.test.tsx +122 -0
  98. package/src/tests/SSAO.test.tsx +140 -0
  99. package/src/tests/SelectiveBloom.test.tsx +48 -0
  100. package/src/tests/ShockWave.test.tsx +95 -0
  101. package/src/tests/TiltShift.test.tsx +76 -0
  102. package/src/tests/createEffectComponent.test.tsx +286 -0
  103. package/src/tests/effects.smoke.test.tsx +46 -42
  104. package/src/util.tsx +148 -23
  105. package/src/wrapEffect.tsx +3 -2
  106. package/dist/tests/test-utils.d.ts +0 -12
@@ -0,0 +1,74 @@
1
+ import { Effect, EffectPass } from 'postprocessing'
2
+ import { use, useImperativeHandle, useLayoutEffect, useRef, useState, type ReactNode, type Ref } from 'react'
3
+ import type { Group } from 'three'
4
+ import { disposePassWithoutEffects, EffectComposerContext, groupPasses } from './EffectComposer'
5
+ import { readGroupChildren, updateIfChanged } from './util'
6
+
7
+ export type EffectGroupProps = {
8
+ /** Toggles the whole pass, like Pass.enabled in vanilla postprocessing - cheap, no reconstruction. */
9
+ enabled?: boolean
10
+ children: ReactNode
11
+ ref?: Ref<EffectPass>
12
+ }
13
+
14
+ // Groups its children into one EffectPass instead of relying on
15
+ // EffectComposer's automatic consecutive-effect merging. Renders children
16
+ // into a hidden inner <group> (invisible to EffectComposer's own top-level
17
+ // walk) and renders the resulting pass back out via <primitive> at this
18
+ // component's own JSX position, so EffectComposer's existing bare-Pass
19
+ // passthrough places it correctly relative to sibling effects.
20
+ export function EffectGroup({ enabled = true, children, ref }: EffectGroupProps) {
21
+ const { camera, requestRebuild } = use(EffectComposerContext)
22
+
23
+ const group = useRef<Group>(null!)
24
+ const effectsRef = useRef<Effect[]>([])
25
+ const [pass, setPass] = useState<EffectPass | null>(null)
26
+
27
+ // Mirrors EffectComposer's own buildPasses: always constructs a fresh
28
+ // EffectPass when the effect list changes, rather than updating one in
29
+ // place - so composer.addPass() runs its normal setSize/initialize dance
30
+ // for every effect, with nothing to replicate by hand here.
31
+ useLayoutEffect(() => {
32
+ const effects = readGroupChildren(group.current, (object): object is Effect => object instanceof Effect)
33
+ if (!updateIfChanged(effectsRef, effects)) return
34
+
35
+ if (!effects.length) {
36
+ setPass(null)
37
+ return
38
+ }
39
+
40
+ const newPass = new EffectPass(camera, ...effects)
41
+ groupPasses.add(newPass)
42
+ setPass(newPass)
43
+ })
44
+
45
+ // useLayoutEffect (not useEffect) so `enabled` is applied before
46
+ // EffectComposer's own effect adds this pass to the composer.
47
+ useLayoutEffect(() => {
48
+ if (pass) pass.enabled = enabled
49
+ }, [pass, enabled])
50
+
51
+ // A local state update above doesn't re-render EffectComposer, so its own
52
+ // tree walk would never notice this pass appearing/disappearing.
53
+ useLayoutEffect(() => {
54
+ requestRebuild()
55
+ }, [pass, requestRebuild])
56
+
57
+ // Disposes whichever pass this replaces (or the last one, on unmount) -
58
+ // the only place a pass gets disposed, so there's no double-dispose risk
59
+ // from also doing it inline above where `pass` is replaced.
60
+ useLayoutEffect(() => {
61
+ return () => {
62
+ if (pass) disposePassWithoutEffects(pass)
63
+ }
64
+ }, [pass])
65
+
66
+ useImperativeHandle(ref, () => pass as EffectPass, [pass])
67
+
68
+ return (
69
+ <>
70
+ <group ref={group}>{children}</group>
71
+ {pass && <primitive object={pass} />}
72
+ </>
73
+ )
74
+ }
@@ -0,0 +1,71 @@
1
+ import { extend, useThree } from '@react-three/fiber'
2
+ import type { BlendFunction, Effect, Pass } from 'postprocessing'
3
+ import type { ExoticComponent, JSX, Ref } from 'react'
4
+ import { useRef } from 'react'
5
+ import { useLiveDefaults, useMergeRefs } from './util'
6
+
7
+ export type EffectConstructor = new (...args: any[]) => Effect | Pass
8
+
9
+ // The effect's own options type, straight off its constructor - postprocessing
10
+ // already types every effect's sole options object precisely (either inline or,
11
+ // like BloomEffect, as a named exported type); this just strips the `| undefined`
12
+ // that comes from the parameter being optional.
13
+ export type EffectOptions<T extends EffectConstructor> = NonNullable<ConstructorParameters<T>[0]>
14
+
15
+ const components = new WeakMap<EffectConstructor, ExoticComponent<any> | string>()
16
+ let i = 0
17
+
18
+ const BLEND_KEYS = ['blendMode-blendFunction', 'blendMode-opacity-value']
19
+
20
+ /**
21
+ * Registers `effect` as a JSX intrinsic once per class and returns a
22
+ * component that renders it. Everything else - construction from `args`,
23
+ * live prop application (with the same Color/Vector coercion and reset-
24
+ * to-default on removal any r3f element gets), disposal - is r3f's own
25
+ * reconciler, same rules as `<mesh>`/`<meshStandardMaterial>`. Only fits
26
+ * effects whose constructor works with zero arguments (`new Effect()`) -
27
+ * r3f's own reset-on-removal falls back to `0` otherwise, which is wrong
28
+ * for anything non-numeric. Effects that require e.g. scene/camera stay
29
+ * hand-rolled (see Outline.tsx, SelectiveBloom.tsx, ShockWave.tsx).
30
+ *
31
+ * `blendFunction`/`opacity` are pierced through to `blendMode-*` - every
32
+ * `Effect` has them on a nested `blendMode`, not on the effect itself, so a
33
+ * plain top-level prop would silently land on a stray, unread property.
34
+ * Applied via useLiveDefaults, not as plain JSX props: BlendMode's own
35
+ * constructor requires `blendFunction` (no default), so its constructor
36
+ * length isn't 0 either, and r3f's native reset-on-removal falls back to
37
+ * `changedProps[prop] = 0` - which is BlendFunction.SKIP, not a merely
38
+ * "wrong" blend function but one that hides the effect entirely.
39
+ */
40
+ export function createEffectComponent<T extends EffectConstructor, P extends object>(
41
+ effect: T
42
+ ): (
43
+ props: P & {
44
+ blendFunction?: BlendFunction
45
+ opacity?: number
46
+ args?: ConstructorParameters<T>
47
+ ref?: Ref<InstanceType<T>>
48
+ }
49
+ ) => JSX.Element {
50
+ return function EffectComponent({ blendFunction, opacity, ref, ...props }: any) {
51
+ let Component = components.get(effect)
52
+
53
+ if (!Component) {
54
+ const key = `@react-three/postprocessing/${effect.name}-${i++}`
55
+ extend({ [key]: effect })
56
+ components.set(effect, (Component = key))
57
+ }
58
+
59
+ const camera = useThree((state) => state.camera)
60
+ const localRef = useRef<InstanceType<T>>(null)
61
+ const setRef = useMergeRefs(localRef, ref)
62
+
63
+ useLiveDefaults(
64
+ localRef,
65
+ { 'blendMode-blendFunction': blendFunction, 'blendMode-opacity-value': opacity },
66
+ BLEND_KEYS
67
+ )
68
+
69
+ return <Component ref={setRef} camera={camera} {...props} />
70
+ }
71
+ }
@@ -2,9 +2,9 @@
2
2
  // https://twitter.com/emilwidlund/status/1652386482420609024
3
3
 
4
4
  import { Effect } from 'postprocessing'
5
- import { Ref, useMemo } from 'react'
6
- import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
7
- import { useDispose } from '../util'
5
+ import type { Ref } from 'react'
6
+ import { CanvasTexture, Color, type ColorRepresentation, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
7
+ import { createEffectComponent } from '../createEffectComponent'
8
8
 
9
9
  const fragment = /* glsl */ `
10
10
  uniform sampler2D uCharacters;
@@ -47,17 +47,21 @@ const fragment = /* glsl */ `
47
47
  }
48
48
  `
49
49
 
50
- interface IASCIIEffectProps {
50
+ export type ASCIIProps = {
51
51
  font?: string
52
52
  characters?: string
53
53
  fontSize?: number
54
54
  cellSize?: number
55
- color?: string
55
+ color?: ColorRepresentation
56
56
  invert?: boolean
57
57
  ref?: Ref<ASCIIEffect>
58
58
  }
59
59
 
60
60
  class ASCIIEffect extends Effect {
61
+ private _font: string
62
+ private _characters: string
63
+ private _fontSize: number
64
+
61
65
  constructor({
62
66
  font = 'arial',
63
67
  characters = ` .:,'-^=*+?!|0#X%WM@`,
@@ -65,7 +69,7 @@ class ASCIIEffect extends Effect {
65
69
  cellSize = 16,
66
70
  color = '#ffffff',
67
71
  invert = false,
68
- }: Omit<IASCIIEffectProps, 'ref'> = {}) {
72
+ }: Omit<ASCIIProps, 'ref'> = {}) {
69
73
  const uniforms = new Map<string, Uniform>([
70
74
  ['uCharacters', new Uniform(new Texture())],
71
75
  ['uCellSize', new Uniform(cellSize)],
@@ -76,11 +80,71 @@ class ASCIIEffect extends Effect {
76
80
 
77
81
  super('ASCIIEffect', fragment, { uniforms })
78
82
 
79
- const charactersTextureUniform = this.uniforms.get('uCharacters')
83
+ this._font = font
84
+ this._characters = characters
85
+ this._fontSize = fontSize
86
+ this.updateCharactersTexture()
87
+ }
80
88
 
81
- if (charactersTextureUniform) {
82
- charactersTextureUniform.value = this.createCharactersTexture(characters, font, fontSize)
83
- }
89
+ get cellSize(): number {
90
+ return this.uniforms.get('uCellSize')!.value
91
+ }
92
+
93
+ set cellSize(value: number) {
94
+ this.uniforms.get('uCellSize')!.value = value
95
+ }
96
+
97
+ get invert(): boolean {
98
+ return this.uniforms.get('uInvert')!.value
99
+ }
100
+
101
+ set invert(value: boolean) {
102
+ this.uniforms.get('uInvert')!.value = value
103
+ }
104
+
105
+ get color(): Color {
106
+ return this.uniforms.get('uColor')!.value
107
+ }
108
+
109
+ set color(value: ColorRepresentation) {
110
+ this.uniforms.get('uColor')!.value.set(value)
111
+ }
112
+
113
+ get font(): string {
114
+ return this._font
115
+ }
116
+
117
+ set font(value: string) {
118
+ this._font = value
119
+ this.updateCharactersTexture()
120
+ }
121
+
122
+ get characters(): string {
123
+ return this._characters
124
+ }
125
+
126
+ set characters(value: string) {
127
+ this._characters = value
128
+ this.uniforms.get('uCharactersCount')!.value = value.length
129
+ this.updateCharactersTexture()
130
+ }
131
+
132
+ get fontSize(): number {
133
+ return this._fontSize
134
+ }
135
+
136
+ set fontSize(value: number) {
137
+ this._fontSize = value
138
+ this.updateCharactersTexture()
139
+ }
140
+
141
+ // Regenerates the character atlas texture - characters/font/fontSize have
142
+ // no cheaper live update path, unlike the plain-uniform props above.
143
+ private updateCharactersTexture(): void {
144
+ const uniform = this.uniforms.get('uCharacters')!
145
+ const previous = uniform.value as Texture
146
+ uniform.value = this.createCharactersTexture(this._characters, this._font, this._fontSize)
147
+ previous.dispose()
84
148
  }
85
149
 
86
150
  /** Draws the characters on a Canvas and returns a texture */
@@ -116,21 +180,4 @@ class ASCIIEffect extends Effect {
116
180
  }
117
181
  }
118
182
 
119
- export function ASCII({
120
- font = 'arial',
121
- characters = ` .:,'-^=*+?!|0#X%WM@`,
122
- fontSize = 54,
123
- cellSize = 16,
124
- color = '#ffffff',
125
- invert = false,
126
- ref,
127
- }: IASCIIEffectProps) {
128
- const effect = useMemo(
129
- () => new ASCIIEffect({ characters, font, fontSize, cellSize, color, invert }),
130
- [characters, fontSize, cellSize, color, invert, font]
131
- )
132
-
133
- useDispose(effect)
134
-
135
- return <primitive ref={ref} object={effect} />
136
- }
183
+ export const ASCII = /* @__PURE__ */ createEffectComponent<typeof ASCIIEffect, ASCIIProps>(ASCIIEffect)
@@ -1,11 +1,9 @@
1
1
  import { createPortal, useFrame, useThree, type Vector3 as R3FVector3 } from '@react-three/fiber'
2
2
  import { easing } from 'maath'
3
- import { CopyPass, DepthOfFieldEffect, DepthPickingPass } from 'postprocessing'
3
+ import { DepthOfFieldEffect } from 'postprocessing'
4
4
  import {
5
5
  Ref,
6
6
  useCallback,
7
- useContext,
8
- useEffect,
9
7
  useImperativeHandle,
10
8
  useMemo,
11
9
  useRef,
@@ -15,27 +13,10 @@ import {
15
13
  } from 'react'
16
14
  import { Mesh, Vector3 } from 'three'
17
15
 
18
- import { EffectComposerContext } from '../EffectComposer'
16
+ import { DepthPicking, useDepthPicking, type DepthPickingApi } from '../passes/DepthPicking'
19
17
  import { DepthOfField } from './DepthOfField'
20
18
 
21
- // EffectComposerImpl.dispose() disposes every pass it currently holds
22
- // including these two, since they're added via composer.addPass below.
23
- // When Autofocus unmounts alongside its ancestor EffectComposer (e.g. a
24
- // full tree unmount), both the composer's own teardown AND this
25
- // component's cleanup effect would dispose the same instances. Wrapping
26
- // dispose here makes it safe no matter which caller gets there first.
27
- function makeDisposeIdempotent<T extends { dispose: () => void }>(instance: T): T {
28
- let disposed = false
29
- const dispose = instance.dispose.bind(instance)
30
- instance.dispose = () => {
31
- if (disposed) return
32
- disposed = true
33
- dispose()
34
- }
35
- return instance
36
- }
37
-
38
- export type AutofocusProps = ComponentProps<typeof DepthOfField> & {
19
+ export type AutofocusProps = Omit<ComponentProps<typeof DepthOfField>, 'ref'> & {
39
20
  target?: R3FVector3
40
21
  /** should the target follow the pointer */
41
22
  mouse?: boolean
@@ -64,50 +45,22 @@ export function Autofocus({
64
45
  ...props
65
46
  }: AutofocusProps) {
66
47
  const dofRef = useRef<DepthOfFieldEffect>(null)
67
- const hitpointRef = useRef<Mesh>(null)
68
- const targetRef = useRef<Mesh>(null)
48
+ const pickRef = useRef<DepthPickingApi>(null)
49
+ const getHit = useDepthPicking(pickRef)
50
+ const hitpointMarkerRef = useRef<Mesh>(null)
51
+ const dofTargetMarkerRef = useRef<Mesh>(null)
69
52
 
70
53
  const scene = useThree(({ scene }) => scene)
71
54
  const pointer = useThree(({ pointer }) => pointer)
72
- const { composer, camera } = useContext(EffectComposerContext)
73
-
74
- // see: https://codesandbox.io/s/depthpickingpass-x130hg
75
- const [depthPickingPass] = useState(() => makeDisposeIdempotent(new DepthPickingPass()))
76
- const [copyPass] = useState(() => makeDisposeIdempotent(new CopyPass()))
77
- useEffect(() => {
78
- composer.addPass(depthPickingPass)
79
- composer.addPass(copyPass)
80
- return () => {
81
- composer.removePass(depthPickingPass)
82
- composer.removePass(copyPass)
83
- }
84
- }, [composer, depthPickingPass, copyPass])
85
-
86
- useEffect(() => {
87
- return () => {
88
- depthPickingPass.dispose()
89
- copyPass.dispose()
90
- }
91
- }, [depthPickingPass, copyPass])
92
55
 
93
- const [hitpoint] = useState(() => new Vector3(0, 0, 0))
94
-
95
- const [ndc] = useState(() => new Vector3(0, 0, 0))
96
- const getHit = useCallback(
97
- async (x: number, y: number) => {
98
- ndc.x = x
99
- ndc.y = y
100
- ndc.z = await depthPickingPass.readDepth(ndc)
101
- ndc.z = ndc.z * 2.0 - 1.0
102
- const hit = 1 - ndc.z > 0.0000001 // it is missed if ndc.z is close to 1
103
- return hit ? ndc.unproject(camera) : false
104
- },
105
- [ndc, depthPickingPass, camera]
106
- )
56
+ // A stable non-null value, purely to enable DepthOfField's own autoFocus
57
+ // mode (`target != null`) - the actual per-frame value is applied
58
+ // imperatively to dofRef.current.target below.
59
+ const [autoFocusMarker] = useState(() => new Vector3())
60
+ const [hitpoint] = useState(() => new Vector3())
107
61
 
108
62
  const update = useCallback(
109
63
  async (delta: number, updateTarget = true) => {
110
- // Update hitpoint
111
64
  if (target) {
112
65
  hitpoint.set(...(target as unknown as [number, number, number]))
113
66
  } else {
@@ -116,7 +69,6 @@ export function Autofocus({
116
69
  if (hit) hitpoint.copy(hit)
117
70
  }
118
71
 
119
- // Update target
120
72
  if (updateTarget && dofRef.current?.target) {
121
73
  if (smoothTime > 0 && delta > 0) {
122
74
  easing.damp3(dofRef.current.target, hitpoint, smoothTime, delta)
@@ -125,42 +77,37 @@ export function Autofocus({
125
77
  }
126
78
  }
127
79
  },
128
- [target, hitpoint, followMouse, getHit, smoothTime, pointer]
80
+ [target, hitpoint, followMouse, pointer, getHit, smoothTime]
129
81
  )
130
82
 
131
- useFrame(async (_, delta) => {
83
+ useFrame((_, delta) => {
132
84
  if (!manual) {
133
85
  update(delta)
134
86
  }
135
- if (hitpointRef.current) {
136
- hitpointRef.current.position.copy(hitpoint)
87
+ if (hitpointMarkerRef.current) {
88
+ hitpointMarkerRef.current.position.copy(hitpoint)
137
89
  }
138
- if (targetRef.current && dofRef.current?.target) {
139
- targetRef.current.position.copy(dofRef.current.target)
90
+ if (dofTargetMarkerRef.current && dofRef.current?.target) {
91
+ dofTargetMarkerRef.current.position.copy(dofRef.current.target)
140
92
  }
141
93
  })
142
94
 
143
95
  // Ref API
144
- const api = useMemo<AutofocusApi>(
145
- () => ({
146
- dofRef,
147
- hitpoint,
148
- update,
149
- }),
150
- [hitpoint, update]
151
- )
96
+ const api = useMemo<AutofocusApi>(() => ({ dofRef, hitpoint, update }), [hitpoint, update])
152
97
  useImperativeHandle(ref, () => api, [api])
153
98
 
154
99
  return (
155
100
  <>
101
+ <DepthPicking ref={pickRef} />
102
+
156
103
  {debug
157
104
  ? createPortal(
158
105
  <>
159
- <mesh ref={hitpointRef}>
106
+ <mesh ref={hitpointMarkerRef}>
160
107
  <sphereGeometry args={[debug, 16, 16]} />
161
108
  <meshBasicMaterial color="#00ff00" opacity={1} transparent depthWrite={false} />
162
109
  </mesh>
163
- <mesh ref={targetRef}>
110
+ <mesh ref={dofTargetMarkerRef}>
164
111
  <sphereGeometry args={[debug / 2, 16, 16]} />
165
112
  <meshBasicMaterial color="#00ff00" opacity={0.5} transparent depthWrite={false} />
166
113
  </mesh>
@@ -169,7 +116,7 @@ export function Autofocus({
169
116
  )
170
117
  : null}
171
118
 
172
- <DepthOfField ref={dofRef} {...props} target={hitpoint} />
119
+ <DepthOfField ref={dofRef} {...props} target={autoFocusMarker} />
173
120
  </>
174
121
  )
175
122
  }
@@ -1,6 +1,34 @@
1
1
  import { BlendFunction, BloomEffect } from 'postprocessing'
2
- import { wrapEffect } from '../wrapEffect'
2
+ import type { Ref } from 'react'
3
+ import { useMemo } from 'react'
4
+ import { createEffectComponent, type EffectOptions } from '../createEffectComponent'
3
5
 
4
- export const Bloom = /* @__PURE__ */ wrapEffect(BloomEffect, {
5
- blendFunction: BlendFunction.ADD,
6
- })
6
+ type BloomOptions = EffectOptions<typeof BloomEffect>
7
+
8
+ const BloomImpl = /* @__PURE__ */ createEffectComponent<typeof BloomEffect, BloomOptions>(BloomEffect)
9
+
10
+ export type BloomProps = BloomOptions & { opacity?: number; ref?: Ref<BloomEffect> }
11
+
12
+ // luminanceThreshold/luminanceSmoothing/mipmapBlur/radius/levels/resolution*
13
+ // have no live setter in postprocessing - routed through args so they still
14
+ // work as plain props, just via reconstruction instead of mutation.
15
+ export function Bloom({
16
+ blendFunction = BlendFunction.ADD,
17
+ luminanceThreshold,
18
+ luminanceSmoothing,
19
+ mipmapBlur,
20
+ radius,
21
+ levels,
22
+ resolutionScale,
23
+ resolutionX,
24
+ resolutionY,
25
+ ...liveProps
26
+ }: BloomProps) {
27
+ const args = useMemo<[BloomOptions]>(
28
+ () => [
29
+ { luminanceThreshold, luminanceSmoothing, mipmapBlur, radius, levels, resolutionScale, resolutionX, resolutionY },
30
+ ],
31
+ [luminanceThreshold, luminanceSmoothing, mipmapBlur, radius, levels, resolutionScale, resolutionX, resolutionY]
32
+ )
33
+ return <BloomImpl blendFunction={blendFunction} args={args} {...liveProps} />
34
+ }
@@ -1,4 +1,7 @@
1
1
  import { BrightnessContrastEffect } from 'postprocessing'
2
- import { wrapEffect } from '../wrapEffect'
2
+ import { createEffectComponent, type EffectOptions } from '../createEffectComponent'
3
3
 
4
- export const BrightnessContrast = /* @__PURE__ */ wrapEffect(BrightnessContrastEffect)
4
+ export const BrightnessContrast = /* @__PURE__ */ createEffectComponent<
5
+ typeof BrightnessContrastEffect,
6
+ EffectOptions<typeof BrightnessContrastEffect>
7
+ >(BrightnessContrastEffect)
@@ -1,30 +1,22 @@
1
1
  import type { ReactThreeFiber } from '@react-three/fiber'
2
2
  import { ChromaticAberrationEffect } from 'postprocessing'
3
3
  import type { Ref } from 'react'
4
- import { useMemo } from 'react'
5
- import { useDispose, useVector2 } from '../util'
4
+ import { createEffectComponent, type EffectOptions } from '../createEffectComponent'
6
5
 
6
+ // radialModulation/modulationOffset are typed as required by postprocessing's
7
+ // own .d.ts, but its JSDoc confirms both are optional with defaults - an
8
+ // upstream declaration bug, not a real constraint.
7
9
  export type ChromaticAberrationProps = Omit<
8
- Partial<ConstructorParameters<typeof ChromaticAberrationEffect>[0]>,
9
- 'offset'
10
+ EffectOptions<typeof ChromaticAberrationEffect>,
11
+ 'offset' | 'radialModulation' | 'modulationOffset'
10
12
  > & {
11
- ref?: Ref<ChromaticAberrationEffect>
12
13
  offset?: ReactThreeFiber.Vector2
14
+ radialModulation?: boolean
15
+ modulationOffset?: number
16
+ ref?: Ref<ChromaticAberrationEffect>
13
17
  }
14
18
 
15
- export function ChromaticAberration({ ref, ...props }: ChromaticAberrationProps) {
16
- const offset = useVector2(props, 'offset')
17
-
18
- const effect = useMemo(
19
- () =>
20
- new ChromaticAberrationEffect({
21
- ...props,
22
- offset,
23
- } as ConstructorParameters<typeof ChromaticAberrationEffect>[0]),
24
- [offset, props]
25
- )
26
-
27
- useDispose(effect)
28
-
29
- return <primitive object={effect} ref={ref} />
30
- }
19
+ export const ChromaticAberration = /* @__PURE__ */ createEffectComponent<
20
+ typeof ChromaticAberrationEffect,
21
+ ChromaticAberrationProps
22
+ >(ChromaticAberrationEffect)
@@ -1,17 +1,4 @@
1
- import { BlendFunction, ColorAverageEffect } from 'postprocessing'
2
- import type { Ref } from 'react'
3
- import { useMemo } from 'react'
4
- import { useDispose } from '../util'
1
+ import { ColorAverageEffect } from 'postprocessing'
2
+ import { createEffectComponent } from '../createEffectComponent'
5
3
 
6
- export type ColorAverageProps = {
7
- blendFunction?: BlendFunction
8
- ref?: Ref<ColorAverageEffect>
9
- }
10
-
11
- export function ColorAverage({ blendFunction = BlendFunction.NORMAL, ref }: ColorAverageProps) {
12
- const effect = useMemo(() => new ColorAverageEffect(blendFunction), [blendFunction])
13
-
14
- useDispose(effect)
15
-
16
- return <primitive object={effect} ref={ref} />
17
- }
4
+ export const ColorAverage = /* @__PURE__ */ createEffectComponent<typeof ColorAverageEffect, object>(ColorAverageEffect)
@@ -1,4 +1,25 @@
1
1
  import { ColorDepthEffect } from 'postprocessing'
2
- import { wrapEffect } from '../wrapEffect'
2
+ import type { Ref } from 'react'
3
+ import { createEffectComponent, type EffectOptions } from '../createEffectComponent'
3
4
 
4
- export const ColorDepth = /* @__PURE__ */ wrapEffect(ColorDepthEffect)
5
+ const ColorDepthImpl = /* @__PURE__ */ createEffectComponent<
6
+ typeof ColorDepthEffect,
7
+ Omit<EffectOptions<typeof ColorDepthEffect>, 'bits'> & { bitDepth?: number }
8
+ >(ColorDepthEffect)
9
+
10
+ export type ColorDepthProps = EffectOptions<typeof ColorDepthEffect> & {
11
+ opacity?: number
12
+ ref?: Ref<ColorDepthEffect>
13
+ }
14
+
15
+ // bits (the constructor's option name) has no live setter of its own in
16
+ // postprocessing - only the differently-named bitDepth does (bits is a
17
+ // plain, dead field on the instance). Renamed here so it still works as a
18
+ // plain prop after the initial mount.
19
+ export function ColorDepth({ bits, ...props }: ColorDepthProps) {
20
+ // Only set bitDepth when bits is actually provided - r3f's reset-on-
21
+ // removal only fires when a key is absent from the new props, not when
22
+ // it's present but undefined.
23
+ if (bits !== undefined) (props as Record<string, unknown>).bitDepth = bits
24
+ return <ColorDepthImpl {...props} />
25
+ }
@@ -1,4 +1,6 @@
1
1
  import { DepthEffect } from 'postprocessing'
2
- import { wrapEffect } from '../wrapEffect'
2
+ import { createEffectComponent, type EffectOptions } from '../createEffectComponent'
3
3
 
4
- export const Depth = /* @__PURE__ */ wrapEffect(DepthEffect)
4
+ export const Depth = /* @__PURE__ */ createEffectComponent<typeof DepthEffect, EffectOptions<typeof DepthEffect>>(
5
+ DepthEffect
6
+ )