@react-three/postprocessing 3.0.4 → 3.0.5
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 +81 -81
- package/dist/EffectComposer.d.ts +7 -6
- package/dist/Selection.d.ts +8 -8
- package/dist/effects/ASCII.d.ts +5 -4
- package/dist/effects/Autofocus.d.ts +8 -7
- package/dist/effects/Bloom.d.ts +2 -5
- package/dist/effects/BrightnessContrast.d.ts +2 -5
- package/dist/effects/ChromaticAberration.d.ts +7 -7
- package/dist/effects/ColorAverage.d.ts +7 -8
- package/dist/effects/ColorDepth.d.ts +2 -5
- package/dist/effects/Depth.d.ts +2 -5
- package/dist/effects/DepthOfField.d.ts +7 -18
- package/dist/effects/DotScreen.d.ts +2 -5
- package/dist/effects/FXAA.d.ts +2 -5
- package/dist/effects/Glitch.d.ts +4 -21
- package/dist/effects/GodRays.d.ts +7 -19
- package/dist/effects/Grid.d.ts +6 -7
- package/dist/effects/HueSaturation.d.ts +2 -5
- package/dist/effects/LUT.d.ts +4 -3
- package/dist/effects/LensFlare.d.ts +8 -8
- package/dist/effects/N8AO.d.ts +4 -2
- package/dist/effects/Noise.d.ts +2 -5
- package/dist/effects/Outline.d.ts +4 -23
- package/dist/effects/Pixelation.d.ts +3 -2
- package/dist/effects/Ramp.d.ts +1 -5
- package/dist/effects/SMAA.d.ts +2 -5
- package/dist/effects/SSAO.d.ts +7 -30
- package/dist/effects/ScanlineEffect.d.ts +2 -5
- package/dist/effects/SelectiveBloom.d.ts +5 -10
- package/dist/effects/Sepia.d.ts +2 -5
- package/dist/effects/ShockWave.d.ts +2 -5
- package/dist/effects/Texture.d.ts +7 -8
- package/dist/effects/TiltShift.d.ts +2 -5
- package/dist/effects/TiltShift2.d.ts +1 -5
- package/dist/effects/ToneMapping.d.ts +2 -6
- package/dist/effects/Vignette.d.ts +2 -5
- package/dist/effects/Water.d.ts +1 -5
- package/dist/index.d.ts +11 -10
- package/dist/index.js +855 -551
- package/dist/index.js.map +1 -1
- package/dist/tests/test-utils.d.ts +12 -0
- package/dist/util.d.ts +25 -17
- package/dist/wrapEffect.d.ts +12 -0
- package/package.json +73 -67
- package/src/EffectComposer.tsx +278 -200
- package/src/Selection.tsx +86 -46
- package/src/effects/ASCII.tsx +136 -135
- package/src/effects/Autofocus.tsx +175 -153
- package/src/effects/Bloom.tsx +6 -6
- package/src/effects/BrightnessContrast.tsx +4 -4
- package/src/effects/ChromaticAberration.tsx +30 -5
- package/src/effects/ColorAverage.tsx +17 -15
- package/src/effects/ColorDepth.tsx +4 -4
- package/src/effects/Depth.tsx +4 -4
- package/src/effects/DepthOfField.tsx +87 -89
- package/src/effects/DotScreen.tsx +4 -4
- package/src/effects/FXAA.tsx +4 -4
- package/src/effects/Glitch.tsx +37 -40
- package/src/effects/GodRays.tsx +20 -16
- package/src/effects/Grid.tsx +28 -21
- package/src/effects/HueSaturation.tsx +4 -4
- package/src/effects/LUT.tsx +27 -26
- package/src/effects/LensFlare.tsx +611 -611
- package/src/effects/N8AO.tsx +81 -81
- package/src/effects/Noise.tsx +4 -4
- package/src/effects/Outline.tsx +85 -117
- package/src/effects/Pixelation.tsx +17 -15
- package/src/effects/Ramp.tsx +150 -150
- package/src/effects/SMAA.tsx +4 -4
- package/src/effects/SSAO.tsx +44 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +116 -126
- package/src/effects/Sepia.tsx +4 -4
- package/src/effects/ShockWave.tsx +4 -4
- package/src/effects/Texture.tsx +27 -23
- package/src/effects/TiltShift.tsx +4 -4
- package/src/effects/TiltShift2.tsx +90 -90
- package/src/effects/ToneMapping.tsx +6 -6
- package/src/effects/Vignette.tsx +4 -4
- package/src/effects/Water.tsx +35 -35
- package/src/index.ts +41 -40
- package/src/tests/ChromaticAberration.test.tsx +31 -0
- package/src/tests/EffectComposer.test.tsx +911 -0
- package/src/tests/Outline.test.tsx +89 -0
- package/src/tests/Selection.test.tsx +324 -0
- package/src/tests/SelectiveBloom.test.tsx +118 -0
- package/src/tests/effects.smoke.test.tsx +234 -0
- package/src/tests/test-utils.tsx +96 -0
- package/src/tests/wrapEffect.test.tsx +209 -0
- package/src/util.tsx +96 -55
- package/src/wrapEffect.tsx +122 -0
- package/src/EffectComposer.test.tsx +0 -126
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlendFunction,
|
|
3
|
+
ColorAverageEffect,
|
|
4
|
+
DepthDownsamplingPass,
|
|
5
|
+
Effect,
|
|
6
|
+
EffectComposer as EffectComposerImpl,
|
|
7
|
+
EffectPass,
|
|
8
|
+
NormalPass,
|
|
9
|
+
RenderPass,
|
|
10
|
+
} from 'postprocessing'
|
|
11
|
+
import * as React from 'react'
|
|
12
|
+
import * as THREE from 'three'
|
|
13
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
14
|
+
import { EffectComposer } from '../EffectComposer'
|
|
15
|
+
import { ColorAverage } from '../effects/ColorAverage'
|
|
16
|
+
import { wrapEffect } from '../wrapEffect'
|
|
17
|
+
import { EFFECT_SHADER, flush, root, strict, waitForComposer, waitForEffects, waitForNewComposer } from './test-utils'
|
|
18
|
+
|
|
19
|
+
class EffectA extends Effect {
|
|
20
|
+
constructor() {
|
|
21
|
+
super('EffectA', EFFECT_SHADER)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class EffectB extends Effect {
|
|
26
|
+
constructor() {
|
|
27
|
+
super('EffectB', EFFECT_SHADER)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class EffectC extends Effect {
|
|
32
|
+
constructor() {
|
|
33
|
+
super('EffectC', EFFECT_SHADER)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const WrappedEffectA = wrapEffect(EffectA)
|
|
38
|
+
const WrappedEffectB = wrapEffect(EffectB)
|
|
39
|
+
const WrappedEffectC = wrapEffect(EffectC)
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
await React.act(async () => {
|
|
43
|
+
root.render(null)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('EffectComposer', () => {
|
|
48
|
+
describe('registration and composition', () => {
|
|
49
|
+
it('merges wrapped effects into a single EffectPass', async () => {
|
|
50
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
51
|
+
|
|
52
|
+
await React.act(async () =>
|
|
53
|
+
root.render(
|
|
54
|
+
<EffectComposer ref={ref}>
|
|
55
|
+
<WrappedEffectA />
|
|
56
|
+
<WrappedEffectB />
|
|
57
|
+
<WrappedEffectC />
|
|
58
|
+
</EffectComposer>
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const composer = await waitForComposer(ref)
|
|
63
|
+
|
|
64
|
+
expect(composer.passes).toHaveLength(2)
|
|
65
|
+
expect(composer.passes[0]).toBeInstanceOf(RenderPass)
|
|
66
|
+
expect(composer.passes[1]).toBeInstanceOf(EffectPass)
|
|
67
|
+
|
|
68
|
+
const effects = await waitForEffects(ref, 3)
|
|
69
|
+
|
|
70
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
71
|
+
expect(effects[1]).toBeInstanceOf(EffectB)
|
|
72
|
+
expect(effects[2]).toBeInstanceOf(EffectC)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('preserves registration order on initial mount', async () => {
|
|
76
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
77
|
+
|
|
78
|
+
await React.act(async () =>
|
|
79
|
+
root.render(
|
|
80
|
+
<EffectComposer ref={ref}>
|
|
81
|
+
<WrappedEffectC />
|
|
82
|
+
<WrappedEffectA />
|
|
83
|
+
<WrappedEffectB />
|
|
84
|
+
<ColorAverage />
|
|
85
|
+
</EffectComposer>
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const effects = await waitForEffects(ref, 4)
|
|
90
|
+
|
|
91
|
+
expect(effects[0]).toBeInstanceOf(EffectC)
|
|
92
|
+
expect(effects[1]).toBeInstanceOf(EffectA)
|
|
93
|
+
expect(effects[2]).toBeInstanceOf(EffectB)
|
|
94
|
+
expect(effects[3]).toBeInstanceOf(ColorAverageEffect)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('keeps JSX registration order after the child list changes', async () => {
|
|
98
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
99
|
+
|
|
100
|
+
await React.act(async () =>
|
|
101
|
+
root.render(
|
|
102
|
+
<EffectComposer ref={ref}>
|
|
103
|
+
<WrappedEffectC />
|
|
104
|
+
<WrappedEffectA />
|
|
105
|
+
</EffectComposer>
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
let effects = await waitForEffects(ref, 2)
|
|
110
|
+
|
|
111
|
+
expect(effects[0]).toBeInstanceOf(EffectC)
|
|
112
|
+
expect(effects[1]).toBeInstanceOf(EffectA)
|
|
113
|
+
|
|
114
|
+
await React.act(async () =>
|
|
115
|
+
root.render(
|
|
116
|
+
<EffectComposer ref={ref}>
|
|
117
|
+
<WrappedEffectB />
|
|
118
|
+
<WrappedEffectC />
|
|
119
|
+
</EffectComposer>
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
effects = await waitForEffects(ref, 2)
|
|
124
|
+
|
|
125
|
+
expect(effects[0]).toBeInstanceOf(EffectB)
|
|
126
|
+
expect(effects[1]).toBeInstanceOf(EffectC)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('reorders effects when children swap positions in JSX, identity preserved via key', async () => {
|
|
130
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
131
|
+
|
|
132
|
+
await React.act(async () =>
|
|
133
|
+
root.render(
|
|
134
|
+
<EffectComposer ref={ref}>
|
|
135
|
+
<WrappedEffectA key="a" />
|
|
136
|
+
<WrappedEffectB key="b" />
|
|
137
|
+
</EffectComposer>
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
let effects = await waitForEffects(ref, 2)
|
|
142
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
143
|
+
expect(effects[1]).toBeInstanceOf(EffectB)
|
|
144
|
+
|
|
145
|
+
const [firstA, firstB] = effects
|
|
146
|
+
|
|
147
|
+
await React.act(async () =>
|
|
148
|
+
root.render(
|
|
149
|
+
<EffectComposer ref={ref}>
|
|
150
|
+
<WrappedEffectB key="b" />
|
|
151
|
+
<WrappedEffectA key="a" />
|
|
152
|
+
</EffectComposer>
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
effects = await waitForEffects(ref, 2)
|
|
157
|
+
|
|
158
|
+
expect(effects[0]).toBeInstanceOf(EffectB)
|
|
159
|
+
expect(effects[1]).toBeInstanceOf(EffectA)
|
|
160
|
+
// same underlying instances, just reordered — not a remount
|
|
161
|
+
expect(effects[0]).toBe(firstB)
|
|
162
|
+
expect(effects[1]).toBe(firstA)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('keeps an effect in its slot when its instance is recreated via a prop-driven arg change', async () => {
|
|
166
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
167
|
+
|
|
168
|
+
class MiddleEffect extends Effect {
|
|
169
|
+
value: number
|
|
170
|
+
constructor({ value = 0 }: { value?: number } = {}) {
|
|
171
|
+
super('MiddleEffect', EFFECT_SHADER)
|
|
172
|
+
this.value = value
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const WrappedMiddle = wrapEffect(MiddleEffect)
|
|
176
|
+
|
|
177
|
+
await React.act(async () =>
|
|
178
|
+
root.render(
|
|
179
|
+
<EffectComposer ref={ref}>
|
|
180
|
+
<WrappedEffectA />
|
|
181
|
+
<WrappedMiddle value={1} />
|
|
182
|
+
<WrappedEffectC />
|
|
183
|
+
</EffectComposer>
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
let effects = await waitForEffects(ref, 3)
|
|
188
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
189
|
+
expect(effects[1]).toBeInstanceOf(MiddleEffect)
|
|
190
|
+
expect(effects[2]).toBeInstanceOf(EffectC)
|
|
191
|
+
|
|
192
|
+
const firstMiddle = effects[1]
|
|
193
|
+
|
|
194
|
+
await React.act(async () =>
|
|
195
|
+
root.render(
|
|
196
|
+
<EffectComposer ref={ref}>
|
|
197
|
+
<WrappedEffectA />
|
|
198
|
+
<WrappedMiddle value={2} />
|
|
199
|
+
<WrappedEffectC />
|
|
200
|
+
</EffectComposer>
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
effects = await waitForEffects(ref, 3)
|
|
205
|
+
|
|
206
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
207
|
+
expect(effects[1]).toBeInstanceOf(MiddleEffect)
|
|
208
|
+
expect(effects[1]).not.toBe(firstMiddle) // recreated, new instance
|
|
209
|
+
expect((effects[1] as InstanceType<typeof MiddleEffect>).value).toBe(2)
|
|
210
|
+
expect(effects[2]).toBeInstanceOf(EffectC)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('removes only the unmounted effect', async () => {
|
|
214
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
215
|
+
|
|
216
|
+
await React.act(async () =>
|
|
217
|
+
root.render(
|
|
218
|
+
<EffectComposer ref={ref}>
|
|
219
|
+
<WrappedEffectA />
|
|
220
|
+
<WrappedEffectB />
|
|
221
|
+
</EffectComposer>
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
await waitForEffects(ref, 2)
|
|
226
|
+
|
|
227
|
+
await React.act(async () =>
|
|
228
|
+
root.render(
|
|
229
|
+
<EffectComposer ref={ref}>
|
|
230
|
+
<WrappedEffectB />
|
|
231
|
+
</EffectComposer>
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const effects = await waitForEffects(ref, 1)
|
|
236
|
+
|
|
237
|
+
expect(effects[0]).toBeInstanceOf(EffectB)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('accepts conditionally-rendered children via `condition && <Effect/>` without a type cast', async () => {
|
|
241
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
242
|
+
|
|
243
|
+
// Boxed behind a function so TS can't literal-narrow `show` to `false`
|
|
244
|
+
// at the call site below — that would make `show && <WrappedEffectA/>`
|
|
245
|
+
// type as just `false` instead of `boolean | Element`, silently
|
|
246
|
+
// defeating the point of this test (a real regression to the old,
|
|
247
|
+
// too-narrow `children: JSX.Element | JSX.Element[]` type wouldn't
|
|
248
|
+
// get caught by tsc).
|
|
249
|
+
const shouldShow = (value: boolean): boolean => value
|
|
250
|
+
|
|
251
|
+
await React.act(async () =>
|
|
252
|
+
root.render(<EffectComposer ref={ref}>{shouldShow(false) && <WrappedEffectA />}</EffectComposer>)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
await flush()
|
|
256
|
+
|
|
257
|
+
expect(ref.current!.passes.filter((p) => p instanceof EffectPass)).toHaveLength(0)
|
|
258
|
+
|
|
259
|
+
await React.act(async () =>
|
|
260
|
+
root.render(<EffectComposer ref={ref}>{shouldShow(true) && <WrappedEffectA />}</EffectComposer>)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
const effects = await waitForEffects(ref, 1)
|
|
264
|
+
|
|
265
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('recreates the composer when the camera changes and keeps effects', async () => {
|
|
269
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
270
|
+
|
|
271
|
+
const cameraA = new THREE.PerspectiveCamera()
|
|
272
|
+
const cameraB = new THREE.PerspectiveCamera()
|
|
273
|
+
|
|
274
|
+
await React.act(async () =>
|
|
275
|
+
root.render(
|
|
276
|
+
<EffectComposer ref={ref} camera={cameraA}>
|
|
277
|
+
<WrappedEffectA />
|
|
278
|
+
</EffectComposer>
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const first = await waitForComposer(ref)
|
|
283
|
+
|
|
284
|
+
await React.act(async () =>
|
|
285
|
+
root.render(
|
|
286
|
+
<EffectComposer ref={ref} camera={cameraB}>
|
|
287
|
+
<WrappedEffectA />
|
|
288
|
+
</EffectComposer>
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
const second = await waitForComposer(ref)
|
|
293
|
+
|
|
294
|
+
expect(second).not.toBe(first)
|
|
295
|
+
|
|
296
|
+
const effects = await waitForEffects(ref, 1)
|
|
297
|
+
|
|
298
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe('cleanup and disposal', () => {
|
|
303
|
+
it('unregisters effects and clears the ref on full unmount', async () => {
|
|
304
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
305
|
+
|
|
306
|
+
await React.act(async () =>
|
|
307
|
+
root.render(
|
|
308
|
+
<EffectComposer ref={ref}>
|
|
309
|
+
<WrappedEffectA />
|
|
310
|
+
</EffectComposer>
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
await waitForComposer(ref)
|
|
315
|
+
expect(ref.current!.passes).toHaveLength(2)
|
|
316
|
+
|
|
317
|
+
await React.act(async () => root.render(null))
|
|
318
|
+
|
|
319
|
+
expect(ref.current).toBe(null)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('does not leak composer instances in StrictMode', async () => {
|
|
323
|
+
const disposeSpy = vi.spyOn(EffectComposerImpl.prototype, 'dispose')
|
|
324
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
325
|
+
|
|
326
|
+
await React.act(async () =>
|
|
327
|
+
root.render(
|
|
328
|
+
strict(
|
|
329
|
+
<EffectComposer ref={ref}>
|
|
330
|
+
<WrappedEffectA />
|
|
331
|
+
</EffectComposer>
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
await waitForComposer(ref)
|
|
337
|
+
|
|
338
|
+
await React.act(async () => {
|
|
339
|
+
root.render(null)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
expect(disposeSpy).toHaveBeenCalled()
|
|
343
|
+
|
|
344
|
+
disposeSpy.mockRestore()
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('removes passes before disposing the composer, not after', async () => {
|
|
348
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
349
|
+
|
|
350
|
+
const removePassSpy = vi.spyOn(EffectComposerImpl.prototype, 'removePass')
|
|
351
|
+
const disposeSpy = vi.spyOn(EffectComposerImpl.prototype, 'dispose')
|
|
352
|
+
|
|
353
|
+
await React.act(async () =>
|
|
354
|
+
root.render(
|
|
355
|
+
<EffectComposer ref={ref}>
|
|
356
|
+
<WrappedEffectA />
|
|
357
|
+
<WrappedEffectB />
|
|
358
|
+
</EffectComposer>
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
await waitForEffects(ref, 2)
|
|
363
|
+
|
|
364
|
+
removePassSpy.mockClear()
|
|
365
|
+
disposeSpy.mockClear()
|
|
366
|
+
|
|
367
|
+
await React.act(async () => {
|
|
368
|
+
root.render(null)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
expect(removePassSpy).toHaveBeenCalled()
|
|
372
|
+
expect(disposeSpy).toHaveBeenCalled()
|
|
373
|
+
|
|
374
|
+
const lastRemovePassOrder = Math.max(...removePassSpy.mock.invocationCallOrder)
|
|
375
|
+
const disposeOrder = disposeSpy.mock.invocationCallOrder[0]
|
|
376
|
+
|
|
377
|
+
expect(lastRemovePassOrder).toBeLessThan(disposeOrder)
|
|
378
|
+
|
|
379
|
+
removePassSpy.mockRestore()
|
|
380
|
+
disposeSpy.mockRestore()
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('disposes exactly as many composers as it constructs, across repeated prop changes', async () => {
|
|
384
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
385
|
+
const disposeSpy = vi.spyOn(EffectComposerImpl.prototype, 'dispose')
|
|
386
|
+
const seenInstances = new Set<EffectComposerImpl>()
|
|
387
|
+
const cycles = 20
|
|
388
|
+
|
|
389
|
+
for (let i = 0; i < cycles; i++) {
|
|
390
|
+
await React.act(async () =>
|
|
391
|
+
root.render(
|
|
392
|
+
<EffectComposer ref={ref} multisampling={i % 2 === 0 ? 0 : 16}>
|
|
393
|
+
<WrappedEffectA />
|
|
394
|
+
</EffectComposer>
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
seenInstances.add(await waitForComposer(ref))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await React.act(async () => root.render(null))
|
|
401
|
+
|
|
402
|
+
expect(seenInstances.size).toBe(cycles)
|
|
403
|
+
expect(disposeSpy).toHaveBeenCalledTimes(cycles)
|
|
404
|
+
|
|
405
|
+
disposeSpy.mockRestore()
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('disposes a hand-constructed effect exactly once on unmount', async () => {
|
|
409
|
+
const disposeSpy = vi.spyOn(ColorAverageEffect.prototype, 'dispose')
|
|
410
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
411
|
+
|
|
412
|
+
await React.act(async () =>
|
|
413
|
+
root.render(
|
|
414
|
+
<EffectComposer ref={ref}>
|
|
415
|
+
<WrappedEffectA />
|
|
416
|
+
<ColorAverage />
|
|
417
|
+
</EffectComposer>
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
await waitForComposer(ref)
|
|
422
|
+
await React.act(async () => root.render(null))
|
|
423
|
+
|
|
424
|
+
expect(disposeSpy).toHaveBeenCalledTimes(1)
|
|
425
|
+
disposeSpy.mockRestore()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('disposes exactly as many ColorAverage instances as it constructs, across repeated prop changes', async () => {
|
|
429
|
+
const disposeSpy = vi.spyOn(ColorAverageEffect.prototype, 'dispose')
|
|
430
|
+
const ref = React.createRef<ColorAverageEffect>()
|
|
431
|
+
const seenInstances = new Set<ColorAverageEffect>()
|
|
432
|
+
const cycles = 20
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
for (let i = 0; i < cycles; i++) {
|
|
436
|
+
await React.act(async () =>
|
|
437
|
+
root.render(
|
|
438
|
+
<EffectComposer>
|
|
439
|
+
<ColorAverage ref={ref} blendFunction={i % 2 === 0 ? BlendFunction.NORMAL : BlendFunction.ADD} />
|
|
440
|
+
</EffectComposer>
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
await flush()
|
|
444
|
+
if (ref.current) seenInstances.add(ref.current)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
await React.act(async () => root.render(null))
|
|
448
|
+
|
|
449
|
+
expect(seenInstances.size).toBe(cycles)
|
|
450
|
+
expect(disposeSpy).toHaveBeenCalledTimes(cycles)
|
|
451
|
+
} finally {
|
|
452
|
+
disposeSpy.mockRestore()
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('never disposes the same ColorAverage instance twice, even in StrictMode', async () => {
|
|
457
|
+
const disposedNodes: ColorAverageEffect[] = []
|
|
458
|
+
const disposeSpy = vi.spyOn(ColorAverageEffect.prototype, 'dispose').mockImplementation(function (
|
|
459
|
+
this: ColorAverageEffect
|
|
460
|
+
) {
|
|
461
|
+
disposedNodes.push(this)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const ref = React.createRef<ColorAverageEffect>()
|
|
466
|
+
for (let i = 0; i < 20; i++) {
|
|
467
|
+
await React.act(async () =>
|
|
468
|
+
root.render(
|
|
469
|
+
strict(
|
|
470
|
+
<EffectComposer>
|
|
471
|
+
<ColorAverage ref={ref} blendFunction={i % 2 === 0 ? BlendFunction.NORMAL : BlendFunction.ADD} />
|
|
472
|
+
</EffectComposer>
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
await flush()
|
|
477
|
+
}
|
|
478
|
+
await React.act(async () => root.render(null))
|
|
479
|
+
|
|
480
|
+
const uniqueDisposed = new Set(disposedNodes)
|
|
481
|
+
expect(uniqueDisposed.size).toBe(disposedNodes.length)
|
|
482
|
+
} finally {
|
|
483
|
+
disposeSpy.mockRestore()
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
describe('renderer state restoration', () => {
|
|
489
|
+
it('restores autoClear to its previous value once the composer is fully unmounted', async () => {
|
|
490
|
+
const gl = root.render(null).getState().gl
|
|
491
|
+
gl.autoClear = true // known baseline, independent of whatever earlier tests in this file left behind
|
|
492
|
+
|
|
493
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
494
|
+
|
|
495
|
+
await React.act(async () =>
|
|
496
|
+
root.render(
|
|
497
|
+
<EffectComposer ref={ref}>
|
|
498
|
+
<WrappedEffectA />
|
|
499
|
+
</EffectComposer>
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
await waitForComposer(ref)
|
|
504
|
+
expect(gl.autoClear).toBe(false)
|
|
505
|
+
|
|
506
|
+
await React.act(async () => root.render(null))
|
|
507
|
+
|
|
508
|
+
expect(gl.autoClear).toBe(true)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('keeps autoClear disabled while a sibling composer on the same renderer is still mounted, regardless of unmount order', async () => {
|
|
512
|
+
const gl = root.render(null).getState().gl
|
|
513
|
+
gl.autoClear = true
|
|
514
|
+
|
|
515
|
+
const refA = React.createRef<EffectComposerImpl>()
|
|
516
|
+
const refB = React.createRef<EffectComposerImpl>()
|
|
517
|
+
|
|
518
|
+
await React.act(async () =>
|
|
519
|
+
root.render(
|
|
520
|
+
<>
|
|
521
|
+
<EffectComposer key="a" ref={refA}>
|
|
522
|
+
<WrappedEffectA />
|
|
523
|
+
</EffectComposer>
|
|
524
|
+
<EffectComposer key="b" ref={refB}>
|
|
525
|
+
<WrappedEffectB />
|
|
526
|
+
</EffectComposer>
|
|
527
|
+
</>
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
await waitForComposer(refA)
|
|
532
|
+
await waitForComposer(refB)
|
|
533
|
+
expect(gl.autoClear).toBe(false)
|
|
534
|
+
|
|
535
|
+
// Unmount the first composer only (key "a" drops out of the tree) -
|
|
536
|
+
// the second is still relying on autoClear staying off, so this must
|
|
537
|
+
// not restore it yet. Keying both is essential here: without it,
|
|
538
|
+
// React would match by position and reuse "a"'s instance in place
|
|
539
|
+
// (just updating its props to "b"'s), unmounting the wrong one.
|
|
540
|
+
await React.act(async () =>
|
|
541
|
+
root.render(
|
|
542
|
+
<EffectComposer key="b" ref={refB}>
|
|
543
|
+
<WrappedEffectB />
|
|
544
|
+
</EffectComposer>
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
expect(gl.autoClear).toBe(false)
|
|
549
|
+
|
|
550
|
+
// Now the last one goes too - only now should it actually restore.
|
|
551
|
+
await React.act(async () => root.render(null))
|
|
552
|
+
|
|
553
|
+
expect(gl.autoClear).toBe(true)
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('restores toneMapping to its previous value once the composer is fully unmounted', async () => {
|
|
557
|
+
const gl = root.render(null).getState().gl
|
|
558
|
+
gl.toneMapping = THREE.ACESFilmicToneMapping // known baseline, distinct from NoToneMapping
|
|
559
|
+
|
|
560
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
561
|
+
|
|
562
|
+
await React.act(async () =>
|
|
563
|
+
root.render(
|
|
564
|
+
<EffectComposer ref={ref}>
|
|
565
|
+
<WrappedEffectA />
|
|
566
|
+
</EffectComposer>
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
await waitForComposer(ref)
|
|
571
|
+
expect(gl.toneMapping).toBe(THREE.NoToneMapping)
|
|
572
|
+
|
|
573
|
+
await React.act(async () => root.render(null))
|
|
574
|
+
|
|
575
|
+
expect(gl.toneMapping).toBe(THREE.ACESFilmicToneMapping)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('keeps toneMapping disabled while a sibling composer on the same renderer is still mounted, regardless of unmount order', async () => {
|
|
579
|
+
const gl = root.render(null).getState().gl
|
|
580
|
+
gl.toneMapping = THREE.ACESFilmicToneMapping
|
|
581
|
+
|
|
582
|
+
const refA = React.createRef<EffectComposerImpl>()
|
|
583
|
+
const refB = React.createRef<EffectComposerImpl>()
|
|
584
|
+
|
|
585
|
+
await React.act(async () =>
|
|
586
|
+
root.render(
|
|
587
|
+
<>
|
|
588
|
+
<EffectComposer key="a" ref={refA}>
|
|
589
|
+
<WrappedEffectA />
|
|
590
|
+
</EffectComposer>
|
|
591
|
+
<EffectComposer key="b" ref={refB}>
|
|
592
|
+
<WrappedEffectB />
|
|
593
|
+
</EffectComposer>
|
|
594
|
+
</>
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
await waitForComposer(refA)
|
|
599
|
+
await waitForComposer(refB)
|
|
600
|
+
expect(gl.toneMapping).toBe(THREE.NoToneMapping)
|
|
601
|
+
|
|
602
|
+
// Unmount the first composer only - the second still relies on
|
|
603
|
+
// toneMapping staying off, so this must not restore it yet.
|
|
604
|
+
await React.act(async () =>
|
|
605
|
+
root.render(
|
|
606
|
+
<EffectComposer key="b" ref={refB}>
|
|
607
|
+
<WrappedEffectB />
|
|
608
|
+
</EffectComposer>
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
expect(gl.toneMapping).toBe(THREE.NoToneMapping)
|
|
613
|
+
|
|
614
|
+
// Now the last one goes too - only now should it actually restore.
|
|
615
|
+
await React.act(async () => root.render(null))
|
|
616
|
+
|
|
617
|
+
expect(gl.toneMapping).toBe(THREE.ACESFilmicToneMapping)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('does not clobber a manual autoClear change made while the composer was mounted', async () => {
|
|
621
|
+
const gl = root.render(null).getState().gl
|
|
622
|
+
// Pre-mount baseline is false (not the usual true) specifically so
|
|
623
|
+
// it differs from the manual override below - autoClear only has two
|
|
624
|
+
// states, so this is the only way to make an unconditional restore-
|
|
625
|
+
// to-original and a "preserve the manual change" outcome observably
|
|
626
|
+
// different from each other.
|
|
627
|
+
gl.autoClear = false
|
|
628
|
+
|
|
629
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
630
|
+
|
|
631
|
+
await React.act(async () =>
|
|
632
|
+
root.render(
|
|
633
|
+
<EffectComposer ref={ref}>
|
|
634
|
+
<WrappedEffectA />
|
|
635
|
+
</EffectComposer>
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
await waitForComposer(ref)
|
|
640
|
+
expect(gl.autoClear).toBe(false)
|
|
641
|
+
|
|
642
|
+
// Something outside this component takes manual control while the
|
|
643
|
+
// composer happens to still be mounted - a deliberate, more current
|
|
644
|
+
// intent than whatever we captured before mount.
|
|
645
|
+
gl.autoClear = true
|
|
646
|
+
|
|
647
|
+
await React.act(async () => root.render(null))
|
|
648
|
+
|
|
649
|
+
// Must stay what the manual override set it to, not get silently
|
|
650
|
+
// reset back to the pre-mount value we originally captured (false).
|
|
651
|
+
expect(gl.autoClear).toBe(true)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('does not clobber a manual toneMapping change made while the composer was mounted', async () => {
|
|
655
|
+
const gl = root.render(null).getState().gl
|
|
656
|
+
gl.toneMapping = THREE.ACESFilmicToneMapping
|
|
657
|
+
|
|
658
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
659
|
+
|
|
660
|
+
await React.act(async () =>
|
|
661
|
+
root.render(
|
|
662
|
+
<EffectComposer ref={ref}>
|
|
663
|
+
<WrappedEffectA />
|
|
664
|
+
</EffectComposer>
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
await waitForComposer(ref)
|
|
669
|
+
expect(gl.toneMapping).toBe(THREE.NoToneMapping)
|
|
670
|
+
|
|
671
|
+
gl.toneMapping = THREE.ReinhardToneMapping
|
|
672
|
+
|
|
673
|
+
await React.act(async () => root.render(null))
|
|
674
|
+
|
|
675
|
+
expect(gl.toneMapping).toBe(THREE.ReinhardToneMapping)
|
|
676
|
+
})
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
describe('StrictMode', () => {
|
|
680
|
+
it('preserves effects after the StrictMode fake-unmount/remount cycle', async () => {
|
|
681
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
682
|
+
|
|
683
|
+
await React.act(async () =>
|
|
684
|
+
root.render(
|
|
685
|
+
strict(
|
|
686
|
+
<EffectComposer ref={ref}>
|
|
687
|
+
<WrappedEffectA />
|
|
688
|
+
</EffectComposer>
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
const firstComposer = await waitForComposer(ref)
|
|
694
|
+
const effects = await waitForEffects(ref, 1)
|
|
695
|
+
|
|
696
|
+
expect(firstComposer).toBeTruthy()
|
|
697
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
698
|
+
|
|
699
|
+
await flush()
|
|
700
|
+
|
|
701
|
+
const secondComposer = ref.current
|
|
702
|
+
|
|
703
|
+
expect(secondComposer).toBeTruthy()
|
|
704
|
+
expect(secondComposer!.passes).toHaveLength(2)
|
|
705
|
+
|
|
706
|
+
const secondEffects = await waitForEffects(ref, 1)
|
|
707
|
+
|
|
708
|
+
expect(secondEffects[0]).toBeInstanceOf(EffectA)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
it('never creates duplicate EffectPass instances, including when effects are reduced', async () => {
|
|
712
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
713
|
+
|
|
714
|
+
await React.act(async () =>
|
|
715
|
+
root.render(
|
|
716
|
+
strict(
|
|
717
|
+
<EffectComposer ref={ref}>
|
|
718
|
+
<WrappedEffectA />
|
|
719
|
+
<WrappedEffectB />
|
|
720
|
+
<WrappedEffectC />
|
|
721
|
+
</EffectComposer>
|
|
722
|
+
)
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
const composer = await waitForComposer(ref)
|
|
727
|
+
const effects = await waitForEffects(ref, 3)
|
|
728
|
+
|
|
729
|
+
expect(effects[0]).toBeInstanceOf(EffectA)
|
|
730
|
+
expect(effects[1]).toBeInstanceOf(EffectB)
|
|
731
|
+
expect(effects[2]).toBeInstanceOf(EffectC)
|
|
732
|
+
expect(composer.passes.filter((p) => p instanceof EffectPass)).toHaveLength(1)
|
|
733
|
+
|
|
734
|
+
await React.act(async () =>
|
|
735
|
+
root.render(
|
|
736
|
+
strict(
|
|
737
|
+
<EffectComposer ref={ref}>
|
|
738
|
+
<WrappedEffectA />
|
|
739
|
+
</EffectComposer>
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
const reduced = await waitForEffects(ref, 1)
|
|
745
|
+
|
|
746
|
+
expect(reduced[0]).toBeInstanceOf(EffectA)
|
|
747
|
+
expect(composer.passes.filter((p) => p instanceof EffectPass)).toHaveLength(1)
|
|
748
|
+
|
|
749
|
+
await React.act(async () => root.render(null))
|
|
750
|
+
expect(ref.current).toBe(null)
|
|
751
|
+
})
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
describe('composer recreation', () => {
|
|
755
|
+
it('already has its EffectPass the instant the composer reference changes', async () => {
|
|
756
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
757
|
+
|
|
758
|
+
await React.act(async () =>
|
|
759
|
+
root.render(
|
|
760
|
+
<EffectComposer ref={ref} multisampling={4}>
|
|
761
|
+
<WrappedEffectA />
|
|
762
|
+
</EffectComposer>
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
const firstComposer = await waitForComposer(ref)
|
|
767
|
+
await waitForEffects(ref, 1)
|
|
768
|
+
|
|
769
|
+
await React.act(async () =>
|
|
770
|
+
root.render(
|
|
771
|
+
<EffectComposer ref={ref} multisampling={8}>
|
|
772
|
+
<WrappedEffectA />
|
|
773
|
+
</EffectComposer>
|
|
774
|
+
)
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
const secondComposer = await waitForNewComposer(ref, firstComposer)
|
|
778
|
+
|
|
779
|
+
expect(secondComposer.passes.filter((p) => p instanceof EffectPass)).toHaveLength(1)
|
|
780
|
+
// @ts-expect-error
|
|
781
|
+
expect(secondComposer.passes.find((p) => p instanceof EffectPass)!.effects).toHaveLength(1)
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('preserves registration order across recreation with multiple effects', async () => {
|
|
785
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
786
|
+
|
|
787
|
+
await React.act(async () =>
|
|
788
|
+
root.render(
|
|
789
|
+
<EffectComposer ref={ref} multisampling={4}>
|
|
790
|
+
<WrappedEffectC />
|
|
791
|
+
<WrappedEffectA />
|
|
792
|
+
<WrappedEffectB />
|
|
793
|
+
</EffectComposer>
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
const firstComposer = await waitForComposer(ref)
|
|
798
|
+
await waitForEffects(ref, 3)
|
|
799
|
+
|
|
800
|
+
await React.act(async () =>
|
|
801
|
+
root.render(
|
|
802
|
+
<EffectComposer ref={ref} multisampling={8}>
|
|
803
|
+
<WrappedEffectC />
|
|
804
|
+
<WrappedEffectA />
|
|
805
|
+
<WrappedEffectB />
|
|
806
|
+
</EffectComposer>
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
const secondComposer = await waitForNewComposer(ref, firstComposer)
|
|
811
|
+
// @ts-expect-error
|
|
812
|
+
const effects = secondComposer.passes.find((p) => p instanceof EffectPass)!.effects
|
|
813
|
+
|
|
814
|
+
expect(effects).toHaveLength(3)
|
|
815
|
+
expect(effects[0]).toBeInstanceOf(EffectC)
|
|
816
|
+
expect(effects[1]).toBeInstanceOf(EffectA)
|
|
817
|
+
expect(effects[2]).toBeInstanceOf(EffectB)
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
it('enables normalPass/downSamplingPass synchronously on the new composer when effects are already registered', async () => {
|
|
821
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
822
|
+
|
|
823
|
+
await React.act(async () =>
|
|
824
|
+
root.render(
|
|
825
|
+
<EffectComposer ref={ref} multisampling={4} enableNormalPass resolutionScale={0.5}>
|
|
826
|
+
<WrappedEffectA />
|
|
827
|
+
</EffectComposer>
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
const firstComposer = await waitForComposer(ref)
|
|
832
|
+
await waitForEffects(ref, 1)
|
|
833
|
+
|
|
834
|
+
await React.act(async () =>
|
|
835
|
+
root.render(
|
|
836
|
+
<EffectComposer ref={ref} multisampling={8} enableNormalPass resolutionScale={0.5}>
|
|
837
|
+
<WrappedEffectA />
|
|
838
|
+
</EffectComposer>
|
|
839
|
+
)
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
const secondComposer = await waitForNewComposer(ref, firstComposer)
|
|
843
|
+
|
|
844
|
+
const normalPass = secondComposer.passes.find((p) => p instanceof NormalPass)
|
|
845
|
+
const downSamplingPass = secondComposer.passes.find((p) => p instanceof DepthDownsamplingPass)
|
|
846
|
+
|
|
847
|
+
expect(normalPass).toBeTruthy()
|
|
848
|
+
expect(downSamplingPass).toBeTruthy()
|
|
849
|
+
expect(normalPass!.enabled).toBe(true)
|
|
850
|
+
expect(downSamplingPass!.enabled).toBe(true)
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('leaves normalPass/downSamplingPass disabled when the composer is recreated before any effect has ever registered', async () => {
|
|
854
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
855
|
+
|
|
856
|
+
await React.act(async () =>
|
|
857
|
+
root.render(
|
|
858
|
+
<EffectComposer ref={ref} multisampling={4} enableNormalPass resolutionScale={0.5}>
|
|
859
|
+
<></>
|
|
860
|
+
</EffectComposer>
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
const firstComposer = await waitForComposer(ref)
|
|
865
|
+
|
|
866
|
+
await React.act(async () =>
|
|
867
|
+
root.render(
|
|
868
|
+
<EffectComposer ref={ref} multisampling={8} enableNormalPass resolutionScale={0.5}>
|
|
869
|
+
<></>
|
|
870
|
+
</EffectComposer>
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
const secondComposer = await waitForNewComposer(ref, firstComposer)
|
|
875
|
+
|
|
876
|
+
const normalPass = secondComposer.passes.find((p) => p instanceof NormalPass)
|
|
877
|
+
const downSamplingPass = secondComposer.passes.find((p) => p instanceof DepthDownsamplingPass)
|
|
878
|
+
|
|
879
|
+
expect(normalPass!.enabled).toBe(false)
|
|
880
|
+
expect(downSamplingPass!.enabled).toBe(false)
|
|
881
|
+
expect(secondComposer.passes.some((p) => p instanceof EffectPass)).toBe(false)
|
|
882
|
+
})
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
describe('performance characteristics (documented, not enforced)', () => {
|
|
886
|
+
it('rebuilds the EffectPass once per registration when mounting many effects at once', async () => {
|
|
887
|
+
const addPassSpy = vi.spyOn(EffectComposerImpl.prototype, 'addPass')
|
|
888
|
+
|
|
889
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
890
|
+
const effectCount = 44
|
|
891
|
+
|
|
892
|
+
await React.act(async () =>
|
|
893
|
+
root.render(
|
|
894
|
+
<EffectComposer ref={ref}>
|
|
895
|
+
{Array.from({ length: effectCount }, (_, i) => (
|
|
896
|
+
<WrappedEffectA key={i} />
|
|
897
|
+
))}
|
|
898
|
+
</EffectComposer>
|
|
899
|
+
)
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
await waitForEffects(ref, effectCount)
|
|
903
|
+
|
|
904
|
+
const effectPassAddCalls = addPassSpy.mock.calls.filter(([pass]) => pass instanceof EffectPass).length
|
|
905
|
+
|
|
906
|
+
expect(effectPassAddCalls).toBe(1)
|
|
907
|
+
|
|
908
|
+
addPassSpy.mockRestore()
|
|
909
|
+
})
|
|
910
|
+
})
|
|
911
|
+
})
|