@react-three/postprocessing 3.0.4 → 3.1.0
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.
- package/LICENSE +21 -21
- package/README.md +80 -81
- package/dist/EffectComposer.d.ts +14 -6
- package/dist/EffectGroup.d.ts +9 -0
- package/dist/Selection.d.ts +8 -8
- package/dist/createEffectComponent.d.ts +30 -0
- package/dist/effects/ASCII.d.ts +29 -7
- package/dist/effects/Autofocus.d.ts +8 -7
- package/dist/effects/Bloom.d.ts +10 -5
- package/dist/effects/BrightnessContrast.d.ts +15 -5
- package/dist/effects/ChromaticAberration.d.ts +30 -7
- package/dist/effects/ColorAverage.d.ts +7 -8
- package/dist/effects/ColorDepth.d.ts +8 -5
- package/dist/effects/Depth.d.ts +13 -5
- package/dist/effects/DepthOfField.d.ts +7 -18
- package/dist/effects/DotScreen.d.ts +15 -5
- package/dist/effects/FXAA.d.ts +11 -5
- package/dist/effects/Glitch.d.ts +17 -29
- package/dist/effects/GodRays.d.ts +7 -19
- package/dist/effects/Grid.d.ts +8 -8
- package/dist/effects/HueSaturation.d.ts +15 -5
- package/dist/effects/LUT.d.ts +4 -3
- package/dist/effects/LensFlare.d.ts +52 -11
- package/dist/effects/Noise.d.ts +8 -5
- package/dist/effects/Outline.d.ts +9 -25
- package/dist/effects/Pixelation.d.ts +3 -2
- package/dist/effects/Ramp.d.ts +58 -10
- package/dist/effects/SMAA.d.ts +10 -5
- package/dist/effects/SSAO.d.ts +7 -30
- package/dist/effects/ScanlineEffect.d.ts +13 -5
- package/dist/effects/SelectiveBloom.d.ts +5 -10
- package/dist/effects/Sepia.d.ts +13 -5
- package/dist/effects/ShockWave.d.ts +12 -5
- package/dist/effects/Texture.d.ts +7 -8
- package/dist/effects/TiltShift.d.ts +10 -5
- package/dist/effects/TiltShift2.d.ts +44 -8
- package/dist/effects/ToneMapping.d.ts +9 -7
- package/dist/effects/Vignette.d.ts +19 -5
- package/dist/effects/Water.d.ts +17 -5
- package/dist/index.d.ts +15 -11
- package/dist/index.js +1841 -45
- package/dist/index.js.map +1 -1
- package/dist/passes/DepthPicking.d.ts +11 -0
- package/dist/{effects → passes}/N8AO.d.ts +5 -2
- package/dist/util.d.ts +19 -17
- package/dist/wrapEffect.d.ts +12 -0
- package/package.json +74 -67
- package/src/EffectComposer.tsx +379 -200
- package/src/EffectGroup.tsx +74 -0
- package/src/Selection.tsx +86 -46
- package/src/createEffectComponent.tsx +71 -0
- package/src/effects/ASCII.tsx +183 -135
- package/src/effects/Autofocus.tsx +122 -153
- package/src/effects/Bloom.tsx +34 -6
- package/src/effects/BrightnessContrast.tsx +7 -4
- package/src/effects/ChromaticAberration.tsx +22 -5
- package/src/effects/ColorAverage.tsx +4 -15
- package/src/effects/ColorDepth.tsx +25 -4
- package/src/effects/Depth.tsx +6 -4
- package/src/effects/DepthOfField.tsx +109 -89
- package/src/effects/DotScreen.tsx +7 -4
- package/src/effects/FXAA.tsx +6 -4
- package/src/effects/Glitch.tsx +32 -40
- package/src/effects/GodRays.tsx +104 -16
- package/src/effects/Grid.tsx +27 -21
- package/src/effects/HueSaturation.tsx +7 -4
- package/src/effects/LUT.tsx +25 -26
- package/src/effects/LensFlare.tsx +765 -611
- package/src/effects/Noise.tsx +16 -4
- package/src/effects/Outline.tsx +84 -117
- package/src/effects/Pixelation.tsx +20 -15
- package/src/effects/Ramp.tsx +239 -150
- package/src/effects/SMAA.tsx +20 -4
- package/src/effects/SSAO.tsx +152 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +98 -126
- package/src/effects/Sepia.tsx +6 -4
- package/src/effects/ShockWave.tsx +32 -4
- package/src/effects/Texture.tsx +28 -23
- package/src/effects/TiltShift.tsx +32 -4
- package/src/effects/TiltShift2.tsx +156 -90
- package/src/effects/ToneMapping.tsx +19 -6
- package/src/effects/Vignette.tsx +7 -4
- package/src/effects/Water.tsx +50 -35
- package/src/index.ts +43 -40
- package/src/passes/DepthPicking.tsx +63 -0
- package/src/passes/N8AO.tsx +105 -0
- package/src/tests/Autofocus.test.tsx +38 -0
- package/src/tests/Bloom.test.tsx +76 -0
- package/src/tests/ChromaticAberration.test.tsx +81 -0
- package/src/tests/ColorDepth.test.tsx +71 -0
- package/src/tests/DepthOfField.test.tsx +130 -0
- package/src/tests/DepthPicking.test.tsx +250 -0
- package/src/tests/EffectComposer.test.tsx +1404 -0
- package/src/tests/EffectGroup.test.tsx +337 -0
- package/src/tests/Glitch.test.tsx +56 -0
- package/src/tests/GodRays.test.tsx +101 -0
- package/src/tests/Grid.test.tsx +34 -0
- package/src/tests/LUT.test.tsx +64 -0
- package/src/tests/N8AO.test.tsx +92 -0
- package/src/tests/Outline.test.tsx +211 -0
- package/src/tests/SSAO.test.tsx +140 -0
- package/src/tests/Selection.test.tsx +324 -0
- package/src/tests/SelectiveBloom.test.tsx +166 -0
- package/src/tests/ShockWave.test.tsx +95 -0
- package/src/tests/TiltShift.test.tsx +76 -0
- package/src/tests/createEffectComponent.test.tsx +286 -0
- package/src/tests/effects.smoke.test.tsx +238 -0
- package/src/tests/test-utils.tsx +96 -0
- package/src/tests/wrapEffect.test.tsx +209 -0
- package/src/util.tsx +221 -55
- package/src/wrapEffect.tsx +123 -0
- package/src/EffectComposer.test.tsx +0 -126
- package/src/effects/N8AO.tsx +0 -81
package/src/EffectComposer.tsx
CHANGED
|
@@ -1,200 +1,379 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
200
|
-
|
|
1
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
2
|
+
import {
|
|
3
|
+
CopyPass,
|
|
4
|
+
DepthDownsamplingPass,
|
|
5
|
+
Effect,
|
|
6
|
+
EffectAttribute,
|
|
7
|
+
EffectComposer as EffectComposerImpl,
|
|
8
|
+
EffectPass,
|
|
9
|
+
NormalPass,
|
|
10
|
+
Pass,
|
|
11
|
+
RenderPass,
|
|
12
|
+
} from 'postprocessing'
|
|
13
|
+
import {
|
|
14
|
+
createContext,
|
|
15
|
+
memo,
|
|
16
|
+
useEffect,
|
|
17
|
+
useImperativeHandle,
|
|
18
|
+
useLayoutEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useReducer,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
type ReactNode,
|
|
24
|
+
type Ref,
|
|
25
|
+
} from 'react'
|
|
26
|
+
import type { Camera, Group, Scene, TextureDataType, WebGLRenderer } from 'three'
|
|
27
|
+
import { HalfFloatType, NoToneMapping, Vector2 } from 'three'
|
|
28
|
+
import { readGroupChildren, updateIfChanged } from './util'
|
|
29
|
+
|
|
30
|
+
export const EffectComposerContext = /* @__PURE__ */ createContext<{
|
|
31
|
+
composer: EffectComposerImpl
|
|
32
|
+
normalPass: NormalPass | null
|
|
33
|
+
downSamplingPass: DepthDownsamplingPass | null
|
|
34
|
+
camera: Camera
|
|
35
|
+
scene: Scene
|
|
36
|
+
resolutionScale?: number
|
|
37
|
+
// A child's local state update doesn't re-render its parent - descendants
|
|
38
|
+
// that add/remove a bare Pass of their own (e.g. EffectGroup) call this
|
|
39
|
+
// to make the tree walk below notice.
|
|
40
|
+
requestRebuild: () => void
|
|
41
|
+
autoClear: boolean
|
|
42
|
+
}>(null!)
|
|
43
|
+
|
|
44
|
+
export type EffectComposerProps = {
|
|
45
|
+
enabled?: boolean
|
|
46
|
+
children: ReactNode
|
|
47
|
+
depthBuffer?: boolean
|
|
48
|
+
/** Only used for SSGI currently, leave it disabled for everything else unless it's needed */
|
|
49
|
+
enableNormalPass?: boolean
|
|
50
|
+
stencilBuffer?: boolean
|
|
51
|
+
autoClear?: boolean
|
|
52
|
+
autoRenderToScreen?: boolean
|
|
53
|
+
resolutionScale?: number
|
|
54
|
+
multisampling?: number
|
|
55
|
+
frameBufferType?: TextureDataType
|
|
56
|
+
renderPriority?: number
|
|
57
|
+
camera?: Camera
|
|
58
|
+
scene?: Scene
|
|
59
|
+
renderPass?: (scene: Scene, camera: Camera) => Pass
|
|
60
|
+
mergeMode?: 'auto' | 'all' | 'none'
|
|
61
|
+
ref?: Ref<EffectComposerImpl>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ComposerState = {
|
|
65
|
+
composer: EffectComposerImpl
|
|
66
|
+
normalPass: NormalPass | null
|
|
67
|
+
downSamplingPass: DepthDownsamplingPass | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isConvolution = (effect: Effect): boolean =>
|
|
71
|
+
(effect.getAttributes() & EffectAttribute.CONVOLUTION) === EffectAttribute.CONVOLUTION
|
|
72
|
+
|
|
73
|
+
// autoClear/toneMapping get force-set and never restored. Ref-counted per
|
|
74
|
+
// (renderer, property) since composers can share a renderer.
|
|
75
|
+
function createRendererPropertyGuard<K extends 'autoClear' | 'toneMapping'>(property: K) {
|
|
76
|
+
const refs = new WeakMap<
|
|
77
|
+
WebGLRenderer,
|
|
78
|
+
{ count: number; original: WebGLRenderer[K]; forcedValue: WebGLRenderer[K] }
|
|
79
|
+
>()
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
acquire(gl: WebGLRenderer, forcedValue: WebGLRenderer[K]): void {
|
|
83
|
+
const existing = refs.get(gl)
|
|
84
|
+
if (existing) {
|
|
85
|
+
existing.count++
|
|
86
|
+
existing.forcedValue = forcedValue
|
|
87
|
+
} else {
|
|
88
|
+
refs.set(gl, { count: 1, original: gl[property], forcedValue })
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
release(gl: WebGLRenderer): void {
|
|
92
|
+
const entry = refs.get(gl)
|
|
93
|
+
if (!entry) return
|
|
94
|
+
|
|
95
|
+
if (--entry.count <= 0) {
|
|
96
|
+
if (gl[property] === entry.forcedValue) {
|
|
97
|
+
gl[property] = entry.original
|
|
98
|
+
}
|
|
99
|
+
refs.delete(gl)
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const autoClearGuard = /* @__PURE__ */ createRendererPropertyGuard('autoClear')
|
|
106
|
+
const toneMappingGuard = /* @__PURE__ */ createRendererPropertyGuard('toneMapping')
|
|
107
|
+
|
|
108
|
+
// Scratch vector for gl.getSize() below - written then read synchronously
|
|
109
|
+
// within the same call, never held onto across a render/frame, so sharing
|
|
110
|
+
// one instance across every <EffectComposer> is safe (same reasoning as
|
|
111
|
+
// DEFAULT_SCREEN_RES in LensFlare.tsx: safe because nothing ever aliases it).
|
|
112
|
+
const glSize = /* @__PURE__ */ new Vector2()
|
|
113
|
+
|
|
114
|
+
// Stable identity for the `renderPass` default - it's in the creation
|
|
115
|
+
// effect's dependency array below (changing it recreates the composer,
|
|
116
|
+
// same as depthBuffer/multisampling), so a fresh arrow function every
|
|
117
|
+
// render here would recreate the composer on every render too.
|
|
118
|
+
const defaultRenderPass = (scene: Scene, camera: Camera): Pass => new RenderPass(scene, camera)
|
|
119
|
+
|
|
120
|
+
// Only passes buildPasses itself constructs - not a user's own bare Pass,
|
|
121
|
+
// which owns its own lifecycle.
|
|
122
|
+
const generatedPasses = /* @__PURE__ */ new WeakSet<Pass>()
|
|
123
|
+
|
|
124
|
+
// EffectGroup registers its pass here (see EffectGroup.tsx) so the rebuild
|
|
125
|
+
// effect below knows to add a shared trailing CopyPass.
|
|
126
|
+
export const groupPasses = /* @__PURE__ */ new WeakSet<Pass>()
|
|
127
|
+
|
|
128
|
+
// Not pass.dispose() - EffectPass.dispose() also disposes the effects it
|
|
129
|
+
// wraps, which have their own lifecycle. setEffects([]) detaches them first.
|
|
130
|
+
export function disposePassWithoutEffects(pass: Pass): void {
|
|
131
|
+
if (pass instanceof EffectPass) {
|
|
132
|
+
;(pass as unknown as { setEffects(effects: never[]): void }).setEffects([])
|
|
133
|
+
}
|
|
134
|
+
Pass.prototype.dispose.call(pass)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function disposeGeneratedPass(pass: Pass): void {
|
|
138
|
+
if (!generatedPasses.has(pass)) return
|
|
139
|
+
disposePassWithoutEffects(pass)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 'auto' (default): consecutive Effects share one EffectPass, same as vanilla
|
|
143
|
+
// postprocessing allows by hand - a run may contain at most one convolution
|
|
144
|
+
// Effect (e.g. DepthOfField), since the library throws if two land in the
|
|
145
|
+
// same pass. 'all' merges through that limit too, same no-guardrail
|
|
146
|
+
// treatment EffectGroup already gives its own children - multiple
|
|
147
|
+
// convolution Effects in one run will throw at render time. 'none' gives
|
|
148
|
+
// every Effect its own EffectPass.
|
|
149
|
+
function buildPasses(nodes: Array<Effect | Pass>, camera: Camera, mergeMode: 'auto' | 'all' | 'none'): Pass[] {
|
|
150
|
+
const passes: Pass[] = []
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
153
|
+
const node = nodes[i]
|
|
154
|
+
|
|
155
|
+
if (node instanceof Effect) {
|
|
156
|
+
const effects: Effect[] = [node]
|
|
157
|
+
let hasConvolution = isConvolution(node)
|
|
158
|
+
|
|
159
|
+
if (mergeMode !== 'none') {
|
|
160
|
+
let next: Effect | Pass | undefined
|
|
161
|
+
while ((next = nodes[i + 1]) instanceof Effect) {
|
|
162
|
+
const nextIsConvolution = isConvolution(next)
|
|
163
|
+
if (mergeMode === 'auto' && hasConvolution && nextIsConvolution) break
|
|
164
|
+
effects.push(next)
|
|
165
|
+
hasConvolution ||= nextIsConvolution
|
|
166
|
+
i++
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const pass = new EffectPass(camera, ...effects)
|
|
171
|
+
generatedPasses.add(pass)
|
|
172
|
+
passes.push(pass)
|
|
173
|
+
} else if (node instanceof Pass) {
|
|
174
|
+
passes.push(node)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return passes
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const EffectComposer = /* @__PURE__ */ memo(function EffectComposer({
|
|
182
|
+
children,
|
|
183
|
+
camera: _camera,
|
|
184
|
+
scene: _scene,
|
|
185
|
+
resolutionScale,
|
|
186
|
+
enabled = true,
|
|
187
|
+
renderPriority = 1,
|
|
188
|
+
autoClear = true,
|
|
189
|
+
autoRenderToScreen = true,
|
|
190
|
+
depthBuffer,
|
|
191
|
+
enableNormalPass,
|
|
192
|
+
stencilBuffer,
|
|
193
|
+
multisampling = 8,
|
|
194
|
+
frameBufferType = HalfFloatType,
|
|
195
|
+
renderPass = defaultRenderPass,
|
|
196
|
+
mergeMode = 'auto',
|
|
197
|
+
ref,
|
|
198
|
+
}: EffectComposerProps) {
|
|
199
|
+
const { gl, scene: defaultScene, camera: defaultCamera } = useThree()
|
|
200
|
+
const scene = _scene || defaultScene
|
|
201
|
+
const camera = _camera || defaultCamera
|
|
202
|
+
|
|
203
|
+
gl.getSize(glSize)
|
|
204
|
+
|
|
205
|
+
// useMemo can't own WebGL resources - React may discard it without cleanup.
|
|
206
|
+
const [composerState, setComposerState] = useState<ComposerState | null>(null)
|
|
207
|
+
|
|
208
|
+
// Dispatch is stable across renders, so it never destabilizes `state` below.
|
|
209
|
+
const [, requestRebuild] = useReducer((c: number) => c + 1, 0)
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
autoClearGuard.acquire(gl, false)
|
|
213
|
+
|
|
214
|
+
const effectComposer = new EffectComposerImpl(gl, { depthBuffer, stencilBuffer, multisampling, frameBufferType })
|
|
215
|
+
|
|
216
|
+
effectComposer.autoRenderToScreen = autoRenderToScreen
|
|
217
|
+
effectComposer.addPass(renderPass(scene, camera))
|
|
218
|
+
|
|
219
|
+
let normalPass: NormalPass | null = null
|
|
220
|
+
let downSamplingPass: DepthDownsamplingPass | null = null
|
|
221
|
+
|
|
222
|
+
if (enableNormalPass) {
|
|
223
|
+
normalPass = new NormalPass(scene, camera)
|
|
224
|
+
normalPass.enabled = false
|
|
225
|
+
effectComposer.addPass(normalPass)
|
|
226
|
+
|
|
227
|
+
if (resolutionScale !== undefined) {
|
|
228
|
+
downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale })
|
|
229
|
+
downSamplingPass.enabled = false
|
|
230
|
+
effectComposer.addPass(downSamplingPass)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
effectComposer.setSize(glSize.width, glSize.height)
|
|
235
|
+
|
|
236
|
+
setComposerState({ composer: effectComposer, normalPass, downSamplingPass })
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
// The rebuild effect below may not have detached its passes yet
|
|
240
|
+
// (composerState only updates next render) - without this, dispose()
|
|
241
|
+
// would kill effects the new composer is about to reuse.
|
|
242
|
+
for (const pass of effectComposer.passes) disposeGeneratedPass(pass)
|
|
243
|
+
effectComposer.dispose()
|
|
244
|
+
autoClearGuard.release(gl)
|
|
245
|
+
}
|
|
246
|
+
// `glSize` intentionally excluded: it's applied via the composer.setSize
|
|
247
|
+
// effect below, and shouldn't tear down/recreate the whole composer.
|
|
248
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
249
|
+
}, [
|
|
250
|
+
camera,
|
|
251
|
+
gl,
|
|
252
|
+
depthBuffer,
|
|
253
|
+
stencilBuffer,
|
|
254
|
+
multisampling,
|
|
255
|
+
frameBufferType,
|
|
256
|
+
autoRenderToScreen,
|
|
257
|
+
renderPass,
|
|
258
|
+
scene,
|
|
259
|
+
enableNormalPass,
|
|
260
|
+
resolutionScale,
|
|
261
|
+
])
|
|
262
|
+
|
|
263
|
+
// Last size actually applied to the composer, so the check below is a
|
|
264
|
+
// cheap no-op on frames where nothing changed.
|
|
265
|
+
const appliedSizeRef = useRef({ width: -1, height: -1 })
|
|
266
|
+
|
|
267
|
+
useFrame(
|
|
268
|
+
(_, delta) => {
|
|
269
|
+
if (!enabled || !composerState) return
|
|
270
|
+
const { composer } = composerState
|
|
271
|
+
|
|
272
|
+
gl.getSize(glSize)
|
|
273
|
+
if (glSize.width !== appliedSizeRef.current.width || glSize.height !== appliedSizeRef.current.height) {
|
|
274
|
+
composer.setSize(glSize.width, glSize.height)
|
|
275
|
+
appliedSizeRef.current.width = glSize.width
|
|
276
|
+
appliedSizeRef.current.height = glSize.height
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const currentAutoClear = gl.autoClear
|
|
280
|
+
gl.autoClear = autoClear
|
|
281
|
+
if (stencilBuffer && !autoClear) gl.clearStencil()
|
|
282
|
+
composer.render(delta)
|
|
283
|
+
gl.autoClear = currentAutoClear
|
|
284
|
+
},
|
|
285
|
+
enabled ? renderPriority : 0
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// Derived from the r3f scene graph (not tracked incrementally) so order
|
|
289
|
+
// always matches JSX, even through wrapper components or a reorder.
|
|
290
|
+
const group = useRef<Group>(null!)
|
|
291
|
+
const nodesRef = useRef<Array<Effect | Pass>>([])
|
|
292
|
+
const [nodesVersion, setNodesVersion] = useState(0)
|
|
293
|
+
|
|
294
|
+
// Runs every render, but only bumps nodesVersion (triggering the rebuild
|
|
295
|
+
// below) when the resolved node list actually changed.
|
|
296
|
+
useLayoutEffect(() => {
|
|
297
|
+
if (!composerState) return
|
|
298
|
+
const nodes = readGroupChildren(
|
|
299
|
+
group.current,
|
|
300
|
+
(object): object is Effect | Pass => object instanceof Effect || object instanceof Pass
|
|
301
|
+
)
|
|
302
|
+
if (updateIfChanged(nodesRef, nodes)) setNodesVersion((v) => v + 1)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// Only re-runs when nodesVersion/composerState/camera change - React's
|
|
306
|
+
// own dependency bailout, so create/cleanup pairing stays correct.
|
|
307
|
+
useLayoutEffect(() => {
|
|
308
|
+
if (!composerState) return
|
|
309
|
+
const { composer, normalPass, downSamplingPass } = composerState
|
|
310
|
+
|
|
311
|
+
const passes = buildPasses(nodesRef.current, camera, mergeMode)
|
|
312
|
+
|
|
313
|
+
// A toggleable pass (EffectGroup) sitting last would leave nothing on
|
|
314
|
+
// screen once disabled - postprocessing assigns renderToScreen by
|
|
315
|
+
// structural position, not `.enabled`. A shared trailing CopyPass
|
|
316
|
+
// (always enabled, always added last) fixes that for the whole chain.
|
|
317
|
+
if (passes.some((pass) => groupPasses.has(pass))) {
|
|
318
|
+
const trailingCopyPass = new CopyPass()
|
|
319
|
+
generatedPasses.add(trailingCopyPass)
|
|
320
|
+
passes.push(trailingCopyPass)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const pass of passes) composer.addPass(pass)
|
|
324
|
+
|
|
325
|
+
if (passes.length) {
|
|
326
|
+
if (normalPass) normalPass.enabled = true
|
|
327
|
+
if (downSamplingPass) downSamplingPass.enabled = true
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
for (const pass of passes) {
|
|
332
|
+
composer.removePass(pass)
|
|
333
|
+
disposeGeneratedPass(pass)
|
|
334
|
+
}
|
|
335
|
+
if (normalPass) normalPass.enabled = false
|
|
336
|
+
if (downSamplingPass) downSamplingPass.enabled = false
|
|
337
|
+
}
|
|
338
|
+
}, [composerState, nodesVersion, camera, mergeMode])
|
|
339
|
+
|
|
340
|
+
// Disable tone mapping because threejs disallows tonemapping on render targets
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
toneMappingGuard.acquire(gl, NoToneMapping)
|
|
343
|
+
gl.toneMapping = NoToneMapping
|
|
344
|
+
return () => {
|
|
345
|
+
toneMappingGuard.release(gl)
|
|
346
|
+
}
|
|
347
|
+
}, [gl])
|
|
348
|
+
|
|
349
|
+
// Memoize state, otherwise it would trigger all consumers on every render
|
|
350
|
+
const state = useMemo(
|
|
351
|
+
() =>
|
|
352
|
+
composerState
|
|
353
|
+
? {
|
|
354
|
+
composer: composerState.composer,
|
|
355
|
+
normalPass: composerState.normalPass,
|
|
356
|
+
downSamplingPass: composerState.downSamplingPass,
|
|
357
|
+
resolutionScale,
|
|
358
|
+
camera,
|
|
359
|
+
scene,
|
|
360
|
+
requestRebuild,
|
|
361
|
+
autoClear,
|
|
362
|
+
}
|
|
363
|
+
: null,
|
|
364
|
+
[composerState, resolutionScale, camera, scene, requestRebuild, autoClear]
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
// Expose the composer
|
|
368
|
+
useImperativeHandle(ref, () => composerState?.composer as EffectComposerImpl, [composerState])
|
|
369
|
+
|
|
370
|
+
// Wait until the composer exists before mounting children so they always
|
|
371
|
+
// see a valid composer instance via context.
|
|
372
|
+
if (!state) return null
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<EffectComposerContext.Provider value={state}>
|
|
376
|
+
<group ref={group}>{children}</group>
|
|
377
|
+
</EffectComposerContext.Provider>
|
|
378
|
+
)
|
|
379
|
+
})
|
|
@@ -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
|
+
}
|