@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,63 @@
1
+ import { useThree } from '@react-three/fiber'
2
+ import { CopyPass, DepthPickingPass as DepthPickingPassImpl } from 'postprocessing'
3
+ import { use, useCallback, useEffect, useImperativeHandle, useState, type Ref } from 'react'
4
+ import type { Camera } from 'three'
5
+ import { Vector2, Vector3 } from 'three'
6
+ import { EffectComposerContext } from '../EffectComposer'
7
+ import { useDispose } from '../util'
8
+
9
+ export type DepthPickingApi = {
10
+ readDepth: (ndc: Vector2 | Vector3) => Promise<number>
11
+ }
12
+
13
+ export type DepthPickingProps = {
14
+ ref?: Ref<DepthPickingApi>
15
+ }
16
+
17
+ // Mounts a vanilla `postprocessing` DepthPickingPass and exposes its
18
+ // `readDepth` via ref - nothing else. Renders nothing. Pair with
19
+ // `useDepthPicking` for a world-space position instead of raw depth.
20
+ export function DepthPicking({ ref }: DepthPickingProps) {
21
+ const { composer } = use(EffectComposerContext)
22
+
23
+ const [depthPickingPass] = useState(() => new DepthPickingPassImpl())
24
+ const [copyPass] = useState(() => new CopyPass())
25
+ useEffect(() => {
26
+ composer.addPass(depthPickingPass)
27
+ composer.addPass(copyPass)
28
+ return () => {
29
+ composer.removePass(depthPickingPass)
30
+ composer.removePass(copyPass)
31
+ }
32
+ }, [composer, depthPickingPass, copyPass])
33
+
34
+ useDispose(depthPickingPass)
35
+ useDispose(copyPass)
36
+
37
+ useImperativeHandle(ref, () => ({ readDepth: (ndc) => depthPickingPass.readDepth(ndc) }), [depthPickingPass])
38
+
39
+ return null
40
+ }
41
+
42
+ export function useDepthPicking(pass: React.RefObject<DepthPickingApi | null>, camera?: Camera) {
43
+ const composerCamera = use(EffectComposerContext)?.camera
44
+ const defaultCamera = useThree((state) => state.camera)
45
+ const resolvedCamera = camera ?? composerCamera ?? defaultCamera
46
+ const [ndc] = useState(() => new Vector3())
47
+
48
+ return useCallback(
49
+ async (x: number, y: number): Promise<Vector3 | false> => {
50
+ if (!pass.current) return false
51
+ ndc.x = x
52
+ ndc.y = y
53
+ ndc.z = await pass.current.readDepth(ndc)
54
+ ndc.z = ndc.z * 2.0 - 1.0
55
+ const hit = 1 - ndc.z > 0.0000001 // missed if ndc.z is close to 1
56
+ // clone - unproject mutates in place, and ndc is reused across calls,
57
+ // so returning it directly would hand out a reference that changes
58
+ // under the caller on the next pick.
59
+ return hit ? ndc.clone().unproject(resolvedCamera) : false
60
+ },
61
+ [pass, resolvedCamera, ndc]
62
+ )
63
+ }
@@ -5,9 +5,11 @@ import { ReactThreeFiber, applyProps, useThree } from '@react-three/fiber'
5
5
  import { Ref, useLayoutEffect, useMemo } from 'react'
6
6
  /* @ts-ignore */
7
7
  import { N8AOPostPass } from 'n8ao'
8
+ import { groupPasses } from '../EffectComposer'
8
9
  import { useDispose } from '../util'
9
10
 
10
11
  export type N8AOProps = {
12
+ enabled?: boolean
11
13
  aoRadius?: number
12
14
  distanceFalloff?: number
13
15
  intensity?: number
@@ -24,6 +26,7 @@ export type N8AOProps = {
24
26
  }
25
27
 
26
28
  export function N8AO({
29
+ enabled = true,
27
30
  halfRes,
28
31
  screenSpaceRadius,
29
32
  quality,
@@ -38,12 +41,26 @@ export function N8AO({
38
41
  renderMode = 0,
39
42
  ref,
40
43
  }: N8AOProps) {
41
- const { camera, scene } = useThree()
42
- const effect = useMemo(() => new N8AOPostPass(scene, camera), [camera, scene])
44
+ const { camera, scene, invalidate } = useThree()
45
+ const effect = useMemo(() => {
46
+ const instance = new N8AOPostPass(scene, camera)
47
+ // N8AOPostPass isn't a postprocessing Effect, so it's not mergeable via
48
+ // EffectGroup - it's already a standalone Pass, hence its own `enabled`
49
+ // prop below instead. Registering it here still gets it the shared
50
+ // trailing CopyPass safety net (see EffectComposer.tsx) so disabling it
51
+ // while it's the last pass in the chain doesn't blank the canvas.
52
+ groupPasses.add(instance)
53
+ return instance
54
+ }, [camera, scene])
43
55
 
44
56
  // TODO: implement dispose upstream; this effect has memory leaks without
45
57
  useDispose(effect)
46
58
 
59
+ useLayoutEffect(() => {
60
+ effect.enabled = enabled
61
+ invalidate()
62
+ }, [effect, enabled, invalidate])
63
+
47
64
  useLayoutEffect(() => {
48
65
  applyProps(effect.configuration, {
49
66
  color,
@@ -58,6 +75,9 @@ export function N8AO({
58
75
  halfRes,
59
76
  depthAwareUpsampling,
60
77
  })
78
+ // effect.configuration is a plain object, never r3f-managed - applyProps'
79
+ // own invalidate (gated behind object.__r3f) never fires for it.
80
+ invalidate()
61
81
  }, [
62
82
  screenSpaceRadius,
63
83
  color,
@@ -71,11 +91,15 @@ export function N8AO({
71
91
  halfRes,
72
92
  depthAwareUpsampling,
73
93
  effect,
94
+ invalidate,
74
95
  ])
75
96
 
76
97
  useLayoutEffect(() => {
77
- if (quality) effect.setQualityMode(quality.charAt(0).toUpperCase() + quality.slice(1))
78
- }, [effect, quality])
98
+ if (quality) {
99
+ effect.setQualityMode(quality.charAt(0).toUpperCase() + quality.slice(1))
100
+ invalidate()
101
+ }
102
+ }, [effect, quality, invalidate])
79
103
 
80
104
  return <primitive ref={ref} object={effect} />
81
105
  }
@@ -0,0 +1,38 @@
1
+ import { DepthPickingPass, EffectComposer as EffectComposerImpl, EffectPass, RenderPass } from 'postprocessing'
2
+ import * as React from 'react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { EffectComposer } from '../EffectComposer'
5
+ import { Autofocus } from '../effects/Autofocus'
6
+ import { flush, root, strict, waitForComposer } from './test-utils'
7
+
8
+ describe('Autofocus', () => {
9
+ it('inserts depthPickingPass right after RenderPass, not appended at the end', async () => {
10
+ const composerRef = React.createRef<EffectComposerImpl>()
11
+
12
+ await React.act(async () => root.render(<EffectComposer ref={composerRef}>{<Autofocus />}</EffectComposer>))
13
+ const composer = await waitForComposer(composerRef)
14
+ await flush()
15
+
16
+ expect(composer.passes[0]).toBeInstanceOf(RenderPass)
17
+ expect(composer.passes[1]).toBeInstanceOf(DepthPickingPass)
18
+
19
+ await React.act(async () => root.render(null))
20
+ })
21
+
22
+ it('never lets depthPickingPass own renderToScreen, regardless of StrictMode double-invocation', async () => {
23
+ const composerRef = React.createRef<EffectComposerImpl>()
24
+
25
+ await React.act(async () => root.render(strict(<EffectComposer ref={composerRef}>{<Autofocus />}</EffectComposer>)))
26
+ const composer = await waitForComposer(composerRef)
27
+ for (let i = 0; i < 10; i++) await flush()
28
+
29
+ const depthPickingPass = composer.passes.find((p) => p instanceof DepthPickingPass)!
30
+ expect(depthPickingPass.renderToScreen).toBe(false)
31
+
32
+ // The real, visible output (DepthOfField's own EffectPass) must own it instead.
33
+ const effectPass = composer.passes.find((p) => p instanceof EffectPass)!
34
+ expect(effectPass.renderToScreen).toBe(true)
35
+
36
+ await React.act(async () => root.render(null))
37
+ })
38
+ })
@@ -0,0 +1,76 @@
1
+ import { BloomEffect, EffectComposer as EffectComposerImpl } from 'postprocessing'
2
+ import * as React from 'react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { EffectComposer } from '../EffectComposer'
5
+ import { Bloom } from '../effects/Bloom'
6
+ import { flush, root } from './test-utils'
7
+
8
+ describe('Bloom', () => {
9
+ it('applies intensity live, without reconstructing the effect', async () => {
10
+ const composerRef = React.createRef<EffectComposerImpl>()
11
+ const ref = React.createRef<BloomEffect>()
12
+
13
+ const render = (intensity: number) =>
14
+ root.render(
15
+ <EffectComposer ref={composerRef}>
16
+ <Bloom ref={ref} intensity={intensity} />
17
+ </EffectComposer>
18
+ )
19
+
20
+ await React.act(async () => render(1))
21
+ await flush()
22
+ const first = ref.current
23
+ expect(first!.intensity).toBe(1)
24
+
25
+ await React.act(async () => render(2))
26
+ await flush()
27
+
28
+ expect(ref.current).toBe(first)
29
+ expect(ref.current!.intensity).toBe(2)
30
+
31
+ await React.act(async () => root.render(null))
32
+ })
33
+
34
+ it('applies mipmapBlur (a construction-only option) as a plain prop, reconstructing under the hood', async () => {
35
+ const composerRef = React.createRef<EffectComposerImpl>()
36
+ const ref = React.createRef<BloomEffect>()
37
+
38
+ const render = (mipmapBlur: boolean) =>
39
+ root.render(
40
+ <EffectComposer ref={composerRef}>
41
+ <Bloom ref={ref} mipmapBlur={mipmapBlur} />
42
+ </EffectComposer>
43
+ )
44
+
45
+ await React.act(async () => render(true))
46
+ await flush()
47
+ const first = ref.current
48
+ expect(first!.mipmapBlurPass.enabled).toBe(true)
49
+
50
+ await React.act(async () => render(false))
51
+ await flush()
52
+
53
+ expect(ref.current).not.toBe(first)
54
+ expect(ref.current!.mipmapBlurPass.enabled).toBe(false)
55
+
56
+ await React.act(async () => root.render(null))
57
+ })
58
+
59
+ it('accepts opacity, as documented in the README (#opacity narrower than createEffectComponent allows)', async () => {
60
+ const composerRef = React.createRef<EffectComposerImpl>()
61
+ const ref = React.createRef<BloomEffect>()
62
+
63
+ await React.act(async () =>
64
+ root.render(
65
+ <EffectComposer ref={composerRef}>
66
+ <Bloom ref={ref} opacity={0.02} />
67
+ </EffectComposer>
68
+ )
69
+ )
70
+ await flush()
71
+
72
+ expect(ref.current!.blendMode.opacity.value).toBe(0.02)
73
+
74
+ await React.act(async () => root.render(null))
75
+ })
76
+ })
@@ -28,4 +28,54 @@ describe('ChromaticAberration', () => {
28
28
 
29
29
  await React.act(async () => root.render(null))
30
30
  })
31
+
32
+ it('applies offset live without reconstructing the effect', async () => {
33
+ const composerRef = React.createRef<EffectComposerImpl>()
34
+ const ref = React.createRef<ChromaticAberrationEffect>()
35
+
36
+ const render = (x: number) =>
37
+ root.render(
38
+ <EffectComposer ref={composerRef}>
39
+ <ChromaticAberration ref={ref} offset={[x, 0.001]} />
40
+ </EffectComposer>
41
+ )
42
+
43
+ await React.act(async () => render(0.01))
44
+ await flush()
45
+ const first = ref.current
46
+ expect(first!.offset.x).toBeCloseTo(0.01)
47
+
48
+ await React.act(async () => render(0.02))
49
+ await flush()
50
+
51
+ expect(ref.current).toBe(first)
52
+ expect(ref.current!.offset.x).toBeCloseTo(0.02)
53
+
54
+ await React.act(async () => root.render(null))
55
+ })
56
+
57
+ it('applies radialModulation live, without reconstructing the effect', async () => {
58
+ const composerRef = React.createRef<EffectComposerImpl>()
59
+ const ref = React.createRef<ChromaticAberrationEffect>()
60
+
61
+ const render = (radialModulation: boolean) =>
62
+ root.render(
63
+ <EffectComposer ref={composerRef}>
64
+ <ChromaticAberration ref={ref} radialModulation={radialModulation} />
65
+ </EffectComposer>
66
+ )
67
+
68
+ await React.act(async () => render(false))
69
+ await flush()
70
+ const first = ref.current
71
+ expect(first!.radialModulation).toBe(false)
72
+
73
+ await React.act(async () => render(true))
74
+ await flush()
75
+
76
+ expect(ref.current).toBe(first)
77
+ expect(ref.current!.radialModulation).toBe(true)
78
+
79
+ await React.act(async () => root.render(null))
80
+ })
31
81
  })
@@ -0,0 +1,71 @@
1
+ import { ColorDepthEffect, EffectComposer as EffectComposerImpl } from 'postprocessing'
2
+ import * as React from 'react'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { ColorDepth } from '../effects/ColorDepth'
5
+ import { EffectComposer } from '../EffectComposer'
6
+ import { flush, root } from './test-utils'
7
+
8
+ describe('ColorDepth', () => {
9
+ it('applies bits live via the differently-named bitDepth setter, without reconstructing the effect', async () => {
10
+ const composerRef = React.createRef<EffectComposerImpl>()
11
+ const ref = React.createRef<ColorDepthEffect>()
12
+
13
+ const render = (bits: number) =>
14
+ root.render(
15
+ <EffectComposer ref={composerRef}>
16
+ <ColorDepth ref={ref} bits={bits} />
17
+ </EffectComposer>
18
+ )
19
+
20
+ await React.act(async () => render(4))
21
+ await flush()
22
+ const first = ref.current
23
+ expect(first!.bitDepth).toBe(4)
24
+
25
+ await React.act(async () => render(8))
26
+ await flush()
27
+
28
+ expect(ref.current).toBe(first)
29
+ expect(ref.current!.bitDepth).toBe(8)
30
+
31
+ await React.act(async () => root.render(null))
32
+ })
33
+
34
+ it('resets bitDepth to its constructor default when bits is removed', async () => {
35
+ const composerRef = React.createRef<EffectComposerImpl>()
36
+ const ref = React.createRef<ColorDepthEffect>()
37
+
38
+ await React.act(async () =>
39
+ root.render(
40
+ <EffectComposer ref={composerRef}>
41
+ <ColorDepth ref={ref} />
42
+ </EffectComposer>
43
+ )
44
+ )
45
+ await flush()
46
+ const defaultBitDepth = ref.current!.bitDepth
47
+
48
+ await React.act(async () =>
49
+ root.render(
50
+ <EffectComposer ref={composerRef}>
51
+ <ColorDepth ref={ref} bits={4} />
52
+ </EffectComposer>
53
+ )
54
+ )
55
+ await flush()
56
+ expect(ref.current!.bitDepth).toBe(4)
57
+
58
+ await React.act(async () =>
59
+ root.render(
60
+ <EffectComposer ref={composerRef}>
61
+ <ColorDepth ref={ref} />
62
+ </EffectComposer>
63
+ )
64
+ )
65
+ await flush()
66
+
67
+ expect(ref.current!.bitDepth).toBe(defaultBitDepth)
68
+
69
+ await React.act(async () => root.render(null))
70
+ })
71
+ })
@@ -0,0 +1,130 @@
1
+ import { DepthOfFieldEffect, EffectComposer as EffectComposerImpl } from 'postprocessing'
2
+ import * as React from 'react'
3
+ import { Texture } from 'three'
4
+ import { describe, expect, it } from 'vitest'
5
+ import { EffectComposer } from '../EffectComposer'
6
+ import { DepthOfField } from '../effects/DepthOfField'
7
+ import { flush, root, waitForComposer } from './test-utils'
8
+
9
+ describe('DepthOfField', () => {
10
+ it('applies bokehScale live, without reconstructing the effect', async () => {
11
+ const composerRef = React.createRef<EffectComposerImpl>()
12
+ const ref = React.createRef<DepthOfFieldEffect>()
13
+
14
+ const render = (bokehScale: number) =>
15
+ root.render(
16
+ <EffectComposer ref={composerRef}>
17
+ <DepthOfField ref={ref} bokehScale={bokehScale} />
18
+ </EffectComposer>
19
+ )
20
+
21
+ await React.act(async () => render(1))
22
+ await waitForComposer(composerRef)
23
+ await flush()
24
+ const first = ref.current
25
+ expect(first!.bokehScale).toBe(1)
26
+
27
+ await React.act(async () => render(2))
28
+ await flush()
29
+
30
+ expect(ref.current).toBe(first)
31
+ expect(ref.current!.bokehScale).toBe(2)
32
+
33
+ await React.act(async () => root.render(null))
34
+ })
35
+
36
+ it('applies focusDistance live via the nested cocMaterial, without reconstructing the effect', async () => {
37
+ const composerRef = React.createRef<EffectComposerImpl>()
38
+ const ref = React.createRef<DepthOfFieldEffect>()
39
+
40
+ const render = (focusDistance: number) =>
41
+ root.render(
42
+ <EffectComposer ref={composerRef}>
43
+ <DepthOfField ref={ref} focusDistance={focusDistance} />
44
+ </EffectComposer>
45
+ )
46
+
47
+ await React.act(async () => render(0.1))
48
+ await waitForComposer(composerRef)
49
+ await flush()
50
+ const first = ref.current
51
+ expect(first!.cocMaterial.focusDistance).toBeCloseTo(0.1)
52
+
53
+ await React.act(async () => render(0.5))
54
+ await flush()
55
+
56
+ expect(ref.current).toBe(first)
57
+ expect(ref.current!.cocMaterial.focusDistance).toBeCloseTo(0.5)
58
+
59
+ await React.act(async () => root.render(null))
60
+ })
61
+
62
+ it('applies depthTexture live via setDepthTexture, without reconstructing, and resets on removal', async () => {
63
+ const composerRef = React.createRef<EffectComposerImpl>()
64
+ const ref = React.createRef<DepthOfFieldEffect>()
65
+ const textureA = new Texture()
66
+ const textureB = new Texture()
67
+ // cocMaterial.depthBuffer is write-only in postprocessing (setter, no
68
+ // getter) - the current value only reads back through its own uniform.
69
+ const currentDepthBuffer = () =>
70
+ (ref.current!.cocMaterial as unknown as { uniforms: { depthBuffer: { value: unknown } } }).uniforms.depthBuffer
71
+ .value
72
+
73
+ const render = (depthTexture?: { texture: Texture; packing: number }) =>
74
+ root.render(
75
+ <EffectComposer ref={composerRef}>
76
+ <DepthOfField ref={ref} depthTexture={depthTexture} />
77
+ </EffectComposer>
78
+ )
79
+
80
+ await React.act(async () => render())
81
+ await waitForComposer(composerRef)
82
+ await flush()
83
+ const first = ref.current
84
+
85
+ await React.act(async () => render({ texture: textureA, packing: 0 }))
86
+ await flush()
87
+ expect(ref.current).toBe(first)
88
+ expect(currentDepthBuffer()).toBe(textureA)
89
+
90
+ await React.act(async () => render({ texture: textureB, packing: 0 }))
91
+ await flush()
92
+ expect(ref.current).toBe(first)
93
+ expect(currentDepthBuffer()).toBe(textureB)
94
+
95
+ await React.act(async () => render())
96
+ await flush()
97
+ expect(ref.current).toBe(first)
98
+ // Reverts to no manually-provided depth texture (undefined), the state
99
+ // useLiveDefaults captured as this instance's default on first apply -
100
+ // not whatever EffectComposer's own depth-attribute auto-wiring later
101
+ // assigns, which runs separately and after this.
102
+ expect(currentDepthBuffer()).toBeUndefined()
103
+
104
+ await React.act(async () => root.render(null))
105
+ })
106
+
107
+ it('still reconstructs when resolutionScale (construction-only) changes', async () => {
108
+ const composerRef = React.createRef<EffectComposerImpl>()
109
+ const ref = React.createRef<DepthOfFieldEffect>()
110
+
111
+ const render = (resolutionScale: number) =>
112
+ root.render(
113
+ <EffectComposer ref={composerRef}>
114
+ <DepthOfField ref={ref} resolutionScale={resolutionScale} />
115
+ </EffectComposer>
116
+ )
117
+
118
+ await React.act(async () => render(0.5))
119
+ await waitForComposer(composerRef)
120
+ await flush()
121
+ const first = ref.current
122
+
123
+ await React.act(async () => render(1))
124
+ await flush()
125
+
126
+ expect(ref.current).not.toBe(first)
127
+
128
+ await React.act(async () => root.render(null))
129
+ })
130
+ })