@react-three/postprocessing 3.0.5 → 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/README.md +6 -7
- package/dist/EffectComposer.d.ts +9 -2
- package/dist/EffectGroup.d.ts +9 -0
- package/dist/createEffectComponent.d.ts +30 -0
- package/dist/effects/ASCII.d.ts +28 -7
- package/dist/effects/Autofocus.d.ts +1 -1
- package/dist/effects/Bloom.d.ts +9 -1
- package/dist/effects/BrightnessContrast.d.ts +14 -1
- package/dist/effects/ChromaticAberration.d.ts +26 -3
- package/dist/effects/ColorAverage.d.ts +7 -7
- package/dist/effects/ColorDepth.d.ts +7 -1
- package/dist/effects/Depth.d.ts +12 -1
- package/dist/effects/DotScreen.d.ts +14 -1
- package/dist/effects/FXAA.d.ts +10 -1
- package/dist/effects/Glitch.d.ts +16 -11
- package/dist/effects/GodRays.d.ts +1 -1
- package/dist/effects/Grid.d.ts +7 -6
- package/dist/effects/HueSaturation.d.ts +14 -1
- package/dist/effects/LUT.d.ts +1 -1
- package/dist/effects/LensFlare.d.ts +46 -5
- package/dist/effects/Noise.d.ts +7 -1
- package/dist/effects/Outline.d.ts +6 -3
- package/dist/effects/Pixelation.d.ts +1 -1
- package/dist/effects/Ramp.d.ts +58 -6
- package/dist/effects/SMAA.d.ts +9 -1
- package/dist/effects/SSAO.d.ts +1 -1
- package/dist/effects/ScanlineEffect.d.ts +12 -1
- package/dist/effects/SelectiveBloom.d.ts +1 -1
- package/dist/effects/Sepia.d.ts +12 -1
- package/dist/effects/ShockWave.d.ts +11 -1
- package/dist/effects/Texture.d.ts +4 -4
- package/dist/effects/TiltShift.d.ts +9 -1
- package/dist/effects/TiltShift2.d.ts +44 -4
- package/dist/effects/ToneMapping.d.ts +9 -3
- package/dist/effects/Vignette.d.ts +18 -1
- package/dist/effects/Water.d.ts +17 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2205 -713
- package/dist/index.js.map +1 -1
- package/dist/passes/DepthPicking.d.ts +11 -0
- package/dist/{effects → passes}/N8AO.d.ts +2 -1
- package/dist/util.d.ts +9 -15
- package/dist/wrapEffect.d.ts +3 -3
- package/package.json +4 -3
- package/src/EffectComposer.tsx +143 -42
- package/src/EffectGroup.tsx +74 -0
- package/src/createEffectComponent.tsx +71 -0
- package/src/effects/ASCII.tsx +75 -28
- package/src/effects/Autofocus.tsx +24 -77
- package/src/effects/Bloom.tsx +32 -4
- package/src/effects/BrightnessContrast.tsx +5 -2
- package/src/effects/ChromaticAberration.tsx +13 -21
- package/src/effects/ColorAverage.tsx +3 -16
- package/src/effects/ColorDepth.tsx +23 -2
- package/src/effects/Depth.tsx +4 -2
- package/src/effects/DepthOfField.tsx +48 -26
- package/src/effects/DotScreen.tsx +5 -2
- package/src/effects/FXAA.tsx +4 -2
- package/src/effects/Glitch.tsx +26 -31
- package/src/effects/GodRays.tsx +90 -6
- package/src/effects/Grid.tsx +17 -18
- package/src/effects/HueSaturation.tsx +5 -2
- package/src/effects/LUT.tsx +9 -11
- package/src/effects/LensFlare.tsx +209 -55
- package/src/effects/Noise.tsx +14 -2
- package/src/effects/Outline.tsx +55 -56
- package/src/effects/Pixelation.tsx +11 -8
- package/src/effects/Ramp.tsx +96 -7
- package/src/effects/SMAA.tsx +18 -2
- package/src/effects/SSAO.tsx +130 -22
- package/src/effects/ScanlineEffect.tsx +6 -6
- package/src/effects/SelectiveBloom.tsx +26 -44
- package/src/effects/Sepia.tsx +4 -2
- package/src/effects/ShockWave.tsx +30 -2
- package/src/effects/Texture.tsx +10 -9
- package/src/effects/TiltShift.tsx +30 -2
- package/src/effects/TiltShift2.tsx +72 -6
- package/src/effects/ToneMapping.tsx +16 -3
- package/src/effects/Vignette.tsx +5 -2
- package/src/effects/Water.tsx +21 -6
- package/src/index.ts +4 -2
- package/src/passes/DepthPicking.tsx +63 -0
- package/src/{effects → passes}/N8AO.tsx +28 -4
- package/src/tests/Autofocus.test.tsx +38 -0
- package/src/tests/Bloom.test.tsx +76 -0
- package/src/tests/ChromaticAberration.test.tsx +50 -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 +501 -8
- 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 +122 -0
- package/src/tests/SSAO.test.tsx +140 -0
- package/src/tests/SelectiveBloom.test.tsx +48 -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 +46 -42
- package/src/util.tsx +148 -23
- package/src/wrapEffect.tsx +3 -2
- package/dist/tests/test-utils.d.ts +0 -12
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ColorAverageEffect,
|
|
4
4
|
DepthDownsamplingPass,
|
|
5
5
|
Effect,
|
|
6
|
+
EffectAttribute,
|
|
6
7
|
EffectComposer as EffectComposerImpl,
|
|
7
8
|
EffectPass,
|
|
8
9
|
NormalPass,
|
|
@@ -34,9 +35,23 @@ class EffectC extends Effect {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
class ConvolutionEffect extends Effect {
|
|
39
|
+
constructor() {
|
|
40
|
+
super('ConvolutionEffect', EFFECT_SHADER, { attributes: EffectAttribute.CONVOLUTION })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ConvolutionEffectTwo extends Effect {
|
|
45
|
+
constructor() {
|
|
46
|
+
super('ConvolutionEffectTwo', EFFECT_SHADER, { attributes: EffectAttribute.CONVOLUTION })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
const WrappedEffectA = wrapEffect(EffectA)
|
|
38
51
|
const WrappedEffectB = wrapEffect(EffectB)
|
|
39
52
|
const WrappedEffectC = wrapEffect(EffectC)
|
|
53
|
+
const WrappedConvolutionEffect = wrapEffect(ConvolutionEffect)
|
|
54
|
+
const WrappedConvolutionEffectTwo = wrapEffect(ConvolutionEffectTwo)
|
|
40
55
|
|
|
41
56
|
afterEach(async () => {
|
|
42
57
|
await React.act(async () => {
|
|
@@ -299,6 +314,285 @@ describe('EffectComposer', () => {
|
|
|
299
314
|
})
|
|
300
315
|
})
|
|
301
316
|
|
|
317
|
+
describe('autoRenderToScreen', () => {
|
|
318
|
+
it('defaults to true, with the last pass rendering to screen', async () => {
|
|
319
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
320
|
+
|
|
321
|
+
await React.act(async () =>
|
|
322
|
+
root.render(
|
|
323
|
+
<EffectComposer ref={ref}>
|
|
324
|
+
<WrappedEffectA />
|
|
325
|
+
</EffectComposer>
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
const composer = await waitForComposer(ref)
|
|
330
|
+
expect(composer.autoRenderToScreen).toBe(true)
|
|
331
|
+
expect(composer.passes.at(-1)?.renderToScreen).toBe(true)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('applies false at mount, with no pass rendering to screen', async () => {
|
|
335
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
336
|
+
|
|
337
|
+
await React.act(async () =>
|
|
338
|
+
root.render(
|
|
339
|
+
<EffectComposer ref={ref} autoRenderToScreen={false}>
|
|
340
|
+
<WrappedEffectA />
|
|
341
|
+
</EffectComposer>
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
const composer = await waitForComposer(ref)
|
|
346
|
+
expect(composer.autoRenderToScreen).toBe(false)
|
|
347
|
+
expect(composer.passes.at(-1)?.renderToScreen).toBe(false)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Not a constructor option in postprocessing itself, but the only place
|
|
351
|
+
// it's actually read is inside addPass() - deciding whether *that* call
|
|
352
|
+
// also assigns renderToScreen to the pass going in. Setting it after
|
|
353
|
+
// passes already exist doesn't revisit them, so treating this as a
|
|
354
|
+
// live/reactive prop (mutate the flag, leave existing passes alone)
|
|
355
|
+
// would leave a composer that was built with autoRenderToScreen: false
|
|
356
|
+
// permanently unable to render to screen again, even after the prop
|
|
357
|
+
// flips back to true - every existing pass already has renderToScreen:
|
|
358
|
+
// false baked in from when it was added, and nothing revisits it.
|
|
359
|
+
// Recreating the composer (same treatment as depthBuffer/multisampling/
|
|
360
|
+
// etc. above) sidesteps that entirely: every pass is freshly added
|
|
361
|
+
// through addPass() with the current flag already in effect.
|
|
362
|
+
it('recreates the composer when autoRenderToScreen changes, with the new last pass matching it', async () => {
|
|
363
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
364
|
+
|
|
365
|
+
await React.act(async () =>
|
|
366
|
+
root.render(
|
|
367
|
+
<EffectComposer ref={ref} autoRenderToScreen={false}>
|
|
368
|
+
<WrappedEffectA />
|
|
369
|
+
</EffectComposer>
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
const first = await waitForComposer(ref)
|
|
374
|
+
expect(first.passes.at(-1)?.renderToScreen).toBe(false)
|
|
375
|
+
|
|
376
|
+
await React.act(async () =>
|
|
377
|
+
root.render(
|
|
378
|
+
<EffectComposer ref={ref} autoRenderToScreen={true}>
|
|
379
|
+
<WrappedEffectA />
|
|
380
|
+
</EffectComposer>
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
const second = await waitForNewComposer(ref, first)
|
|
385
|
+
expect(second.autoRenderToScreen).toBe(true)
|
|
386
|
+
expect(second.passes.at(-1)?.renderToScreen).toBe(true)
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
describe('renderPass', () => {
|
|
391
|
+
it('adds a plain RenderPass as the first pass by default', async () => {
|
|
392
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
393
|
+
|
|
394
|
+
await React.act(async () =>
|
|
395
|
+
root.render(
|
|
396
|
+
<EffectComposer ref={ref}>
|
|
397
|
+
<WrappedEffectA />
|
|
398
|
+
</EffectComposer>
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
const composer = await waitForComposer(ref)
|
|
403
|
+
expect(composer.passes[0]).toBeInstanceOf(RenderPass)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('uses a custom factory instead of the default RenderPass, called with the resolved scene/camera', async () => {
|
|
407
|
+
class CustomRenderPass extends RenderPass {}
|
|
408
|
+
const scene = new THREE.Scene()
|
|
409
|
+
const camera = new THREE.PerspectiveCamera()
|
|
410
|
+
const factory = vi.fn((s: THREE.Scene, c: THREE.Camera) => new CustomRenderPass(s, c))
|
|
411
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
412
|
+
|
|
413
|
+
await React.act(async () =>
|
|
414
|
+
root.render(
|
|
415
|
+
<EffectComposer ref={ref} scene={scene} camera={camera} renderPass={factory}>
|
|
416
|
+
<WrappedEffectA />
|
|
417
|
+
</EffectComposer>
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
const composer = await waitForComposer(ref)
|
|
422
|
+
expect(composer.passes[0]).toBeInstanceOf(CustomRenderPass)
|
|
423
|
+
expect(factory).toHaveBeenCalledWith(scene, camera)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// Not a postprocessing constructor option (it's this library's own prop),
|
|
427
|
+
// but the pass it produces is only ever added once, at construction -
|
|
428
|
+
// same treatment as depthBuffer/multisampling/autoRenderToScreen above.
|
|
429
|
+
it('recreates the composer when renderPass changes', async () => {
|
|
430
|
+
class CustomRenderPass extends RenderPass {}
|
|
431
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
432
|
+
|
|
433
|
+
await React.act(async () =>
|
|
434
|
+
root.render(
|
|
435
|
+
<EffectComposer ref={ref}>
|
|
436
|
+
<WrappedEffectA />
|
|
437
|
+
</EffectComposer>
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
const first = await waitForComposer(ref)
|
|
442
|
+
expect(first.passes[0]).toBeInstanceOf(RenderPass)
|
|
443
|
+
expect(first.passes[0]).not.toBeInstanceOf(CustomRenderPass)
|
|
444
|
+
|
|
445
|
+
await React.act(async () =>
|
|
446
|
+
root.render(
|
|
447
|
+
<EffectComposer ref={ref} renderPass={(s, c) => new CustomRenderPass(s, c)}>
|
|
448
|
+
<WrappedEffectA />
|
|
449
|
+
</EffectComposer>
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
const second = await waitForNewComposer(ref, first)
|
|
454
|
+
expect(second.passes[0]).toBeInstanceOf(CustomRenderPass)
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
describe('mergeMode', () => {
|
|
459
|
+
it("'auto' (default) merges a convolution effect with its non-convolution neighbors into one EffectPass", async () => {
|
|
460
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
461
|
+
|
|
462
|
+
await React.act(async () =>
|
|
463
|
+
root.render(
|
|
464
|
+
<EffectComposer ref={ref}>
|
|
465
|
+
<WrappedEffectA />
|
|
466
|
+
<WrappedConvolutionEffect />
|
|
467
|
+
<WrappedEffectB />
|
|
468
|
+
<WrappedEffectC />
|
|
469
|
+
</EffectComposer>
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
const composer = await waitForComposer(ref)
|
|
474
|
+
const effectPasses = composer.passes.filter((pass) => pass instanceof EffectPass)
|
|
475
|
+
expect(effectPasses).toHaveLength(1)
|
|
476
|
+
// @ts-expect-error - `effects` isn't part of the public Pass typing
|
|
477
|
+
expect(effectPasses[0].effects).toHaveLength(4)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it("'auto' still splits into separate passes rather than merging two convolution effects together", async () => {
|
|
481
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
482
|
+
|
|
483
|
+
await React.act(async () =>
|
|
484
|
+
root.render(
|
|
485
|
+
<EffectComposer ref={ref}>
|
|
486
|
+
<WrappedEffectA />
|
|
487
|
+
<WrappedConvolutionEffect />
|
|
488
|
+
<WrappedConvolutionEffectTwo />
|
|
489
|
+
<WrappedEffectB />
|
|
490
|
+
</EffectComposer>
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
const composer = await waitForComposer(ref)
|
|
495
|
+
const effectPasses = composer.passes.filter((pass) => pass instanceof EffectPass)
|
|
496
|
+
// [A, ConvolutionEffect] | [ConvolutionEffectTwo, B] - never both convolutions in one pass
|
|
497
|
+
expect(effectPasses).toHaveLength(2)
|
|
498
|
+
// @ts-expect-error - `effects` isn't part of the public Pass typing
|
|
499
|
+
const firstEffects = effectPasses[0].effects as Effect[]
|
|
500
|
+
// @ts-expect-error - `effects` isn't part of the public Pass typing
|
|
501
|
+
const secondEffects = effectPasses[1].effects as Effect[]
|
|
502
|
+
expect(firstEffects.filter((e) => e instanceof ConvolutionEffect || e instanceof ConvolutionEffectTwo)).toHaveLength(1)
|
|
503
|
+
expect(secondEffects.filter((e) => e instanceof ConvolutionEffect || e instanceof ConvolutionEffectTwo)).toHaveLength(1)
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it("'all' merges even a single convolution effect with neighbors, same as 'auto'", async () => {
|
|
507
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
508
|
+
|
|
509
|
+
await React.act(async () =>
|
|
510
|
+
root.render(
|
|
511
|
+
<EffectComposer ref={ref} mergeMode="all">
|
|
512
|
+
<WrappedEffectA />
|
|
513
|
+
<WrappedConvolutionEffect />
|
|
514
|
+
<WrappedEffectB />
|
|
515
|
+
</EffectComposer>
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
const composer = await waitForComposer(ref)
|
|
520
|
+
const effectPasses = composer.passes.filter((pass) => pass instanceof EffectPass)
|
|
521
|
+
expect(effectPasses).toHaveLength(1)
|
|
522
|
+
// @ts-expect-error - `effects` isn't part of the public Pass typing
|
|
523
|
+
expect(effectPasses[0].effects).toHaveLength(3)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
// postprocessing itself throws when two convolution effects share a
|
|
527
|
+
// pass - 'all' doesn't guard against that (same no-guardrail contract as
|
|
528
|
+
// EffectGroup), unlike 'auto' which keeps them apart specifically to
|
|
529
|
+
// avoid this.
|
|
530
|
+
it("'all' throws at render time if that removes 'auto'-only protection against merging two convolution effects", async () => {
|
|
531
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
532
|
+
|
|
533
|
+
await expect(
|
|
534
|
+
React.act(async () =>
|
|
535
|
+
root.render(
|
|
536
|
+
<EffectComposer ref={ref} mergeMode="all">
|
|
537
|
+
<WrappedEffectA />
|
|
538
|
+
<WrappedConvolutionEffect />
|
|
539
|
+
<WrappedConvolutionEffectTwo />
|
|
540
|
+
<WrappedEffectB />
|
|
541
|
+
</EffectComposer>
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
).rejects.toThrow('Convolution effects cannot be merged')
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it("'none' gives every effect its own EffectPass, even non-convolution ones", async () => {
|
|
548
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
549
|
+
|
|
550
|
+
await React.act(async () =>
|
|
551
|
+
root.render(
|
|
552
|
+
<EffectComposer ref={ref} mergeMode="none">
|
|
553
|
+
<WrappedEffectA />
|
|
554
|
+
<WrappedEffectB />
|
|
555
|
+
<WrappedEffectC />
|
|
556
|
+
</EffectComposer>
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
const composer = await waitForComposer(ref)
|
|
561
|
+
expect(composer.passes.filter((pass) => pass instanceof EffectPass)).toHaveLength(3)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('rebuilds passes (without recreating the composer) when mergeMode changes', async () => {
|
|
565
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
566
|
+
|
|
567
|
+
await React.act(async () =>
|
|
568
|
+
root.render(
|
|
569
|
+
<EffectComposer ref={ref} mergeMode="none">
|
|
570
|
+
<WrappedEffectA />
|
|
571
|
+
<WrappedConvolutionEffect />
|
|
572
|
+
<WrappedEffectB />
|
|
573
|
+
</EffectComposer>
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
const composer = await waitForComposer(ref)
|
|
578
|
+
expect(composer.passes.filter((pass) => pass instanceof EffectPass)).toHaveLength(3)
|
|
579
|
+
|
|
580
|
+
await React.act(async () =>
|
|
581
|
+
root.render(
|
|
582
|
+
<EffectComposer ref={ref} mergeMode="auto">
|
|
583
|
+
<WrappedEffectA />
|
|
584
|
+
<WrappedConvolutionEffect />
|
|
585
|
+
<WrappedEffectB />
|
|
586
|
+
</EffectComposer>
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
await flush()
|
|
590
|
+
|
|
591
|
+
expect(await waitForComposer(ref)).toBe(composer)
|
|
592
|
+
expect(composer.passes.filter((pass) => pass instanceof EffectPass)).toHaveLength(1)
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
|
|
302
596
|
describe('cleanup and disposal', () => {
|
|
303
597
|
it('unregisters effects and clears the ref on full unmount', async () => {
|
|
304
598
|
const ref = React.createRef<EffectComposerImpl>()
|
|
@@ -380,6 +674,128 @@ describe('EffectComposer', () => {
|
|
|
380
674
|
disposeSpy.mockRestore()
|
|
381
675
|
})
|
|
382
676
|
|
|
677
|
+
it('disposes a discarded EffectPass wrapper\'s own material on rebuild, without disposing the effects it wrapped', async () => {
|
|
678
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
679
|
+
|
|
680
|
+
await React.act(async () => root.render(<EffectComposer ref={ref}><WrappedEffectA /></EffectComposer>))
|
|
681
|
+
const composer = await waitForComposer(ref)
|
|
682
|
+
await waitForEffects(ref, 1)
|
|
683
|
+
|
|
684
|
+
const firstPass = composer.passes.find((p) => p instanceof EffectPass) as EffectPass
|
|
685
|
+
const materialDisposeSpy = vi.spyOn(firstPass.fullscreenMaterial, 'dispose')
|
|
686
|
+
const effectDisposeSpy = vi.spyOn(EffectA.prototype, 'dispose')
|
|
687
|
+
|
|
688
|
+
// Changing the node list forces a rebuild: buildPasses always
|
|
689
|
+
// constructs a brand new EffectPass, discarding the old wrapper.
|
|
690
|
+
await React.act(async () =>
|
|
691
|
+
root.render(
|
|
692
|
+
<EffectComposer ref={ref}>
|
|
693
|
+
<WrappedEffectA />
|
|
694
|
+
<WrappedEffectB />
|
|
695
|
+
</EffectComposer>
|
|
696
|
+
)
|
|
697
|
+
)
|
|
698
|
+
await flush()
|
|
699
|
+
|
|
700
|
+
const secondPass = composer.passes.find((p) => p instanceof EffectPass) as EffectPass
|
|
701
|
+
expect(secondPass).not.toBe(firstPass)
|
|
702
|
+
expect(materialDisposeSpy).toHaveBeenCalledTimes(1)
|
|
703
|
+
expect(effectDisposeSpy).not.toHaveBeenCalled()
|
|
704
|
+
|
|
705
|
+
materialDisposeSpy.mockRestore()
|
|
706
|
+
effectDisposeSpy.mockRestore()
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('detaches a discarded EffectPass\'s change listener from the effect it wrapped, so it no longer reacts to it', async () => {
|
|
710
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
711
|
+
const effectRef = React.createRef<EffectA>()
|
|
712
|
+
|
|
713
|
+
await React.act(async () => root.render(<EffectComposer ref={ref}><WrappedEffectA ref={effectRef} /></EffectComposer>))
|
|
714
|
+
const composer = await waitForComposer(ref)
|
|
715
|
+
await waitForEffects(ref, 1)
|
|
716
|
+
|
|
717
|
+
const firstPass = composer.passes.find((p) => p instanceof EffectPass) as EffectPass
|
|
718
|
+
const recompileSpy = vi.spyOn(firstPass, 'recompile')
|
|
719
|
+
|
|
720
|
+
await React.act(async () =>
|
|
721
|
+
root.render(
|
|
722
|
+
<EffectComposer ref={ref}>
|
|
723
|
+
<WrappedEffectA ref={effectRef} />
|
|
724
|
+
<WrappedEffectB />
|
|
725
|
+
</EffectComposer>
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
await flush()
|
|
729
|
+
|
|
730
|
+
const secondPass = composer.passes.find((p) => p instanceof EffectPass) as EffectPass
|
|
731
|
+
expect(secondPass).not.toBe(firstPass)
|
|
732
|
+
|
|
733
|
+
// The same effect instance survived the rebuild - firing its own
|
|
734
|
+
// 'change' event should only reach whatever pass currently wraps it,
|
|
735
|
+
// not the discarded one still listening from before.
|
|
736
|
+
effectRef.current!.dispatchEvent({ type: 'change' })
|
|
737
|
+
|
|
738
|
+
expect(recompileSpy).not.toHaveBeenCalled()
|
|
739
|
+
|
|
740
|
+
recompileSpy.mockRestore()
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
it('leaves a user-provided EffectPass (rendered directly as a child) untouched across a rebuild', async () => {
|
|
744
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
745
|
+
const camera = new THREE.PerspectiveCamera()
|
|
746
|
+
const userEffect = new EffectC()
|
|
747
|
+
const userPass = new EffectPass(camera, userEffect)
|
|
748
|
+
|
|
749
|
+
await React.act(async () =>
|
|
750
|
+
root.render(
|
|
751
|
+
<EffectComposer ref={ref}>
|
|
752
|
+
<WrappedEffectA />
|
|
753
|
+
<primitive object={userPass} />
|
|
754
|
+
</EffectComposer>
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
const composer = await waitForComposer(ref)
|
|
758
|
+
await waitForEffects(ref, 1)
|
|
759
|
+
expect(composer.passes).toContain(userPass)
|
|
760
|
+
|
|
761
|
+
// Forces a rebuild (node list changes) - buildPasses only ever
|
|
762
|
+
// constructs a *new* EffectPass for Effect children; userPass is
|
|
763
|
+
// passed through unchanged via the plain-Pass branch.
|
|
764
|
+
await React.act(async () =>
|
|
765
|
+
root.render(
|
|
766
|
+
<EffectComposer ref={ref}>
|
|
767
|
+
<WrappedEffectA />
|
|
768
|
+
<WrappedEffectB />
|
|
769
|
+
<primitive object={userPass} />
|
|
770
|
+
</EffectComposer>
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
await flush()
|
|
774
|
+
|
|
775
|
+
expect(composer.passes).toContain(userPass)
|
|
776
|
+
// @ts-expect-error - `effects` isn't part of the public Pass typing
|
|
777
|
+
expect(userPass.effects).toEqual([userEffect])
|
|
778
|
+
|
|
779
|
+
await React.act(async () => root.render(null))
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
it('disposes the final EffectPass wrapper\'s material on full unmount too (composer.dispose has nothing left to dispose by then)', async () => {
|
|
783
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
784
|
+
|
|
785
|
+
await React.act(async () => root.render(<EffectComposer ref={ref}><WrappedEffectA /></EffectComposer>))
|
|
786
|
+
const composer = await waitForComposer(ref)
|
|
787
|
+
await waitForEffects(ref, 1)
|
|
788
|
+
|
|
789
|
+
const pass = composer.passes.find((p) => p instanceof EffectPass) as EffectPass
|
|
790
|
+
const materialDisposeSpy = vi.spyOn(pass.fullscreenMaterial, 'dispose')
|
|
791
|
+
|
|
792
|
+
await React.act(async () => root.render(null))
|
|
793
|
+
|
|
794
|
+
expect(materialDisposeSpy).toHaveBeenCalled()
|
|
795
|
+
|
|
796
|
+
materialDisposeSpy.mockRestore()
|
|
797
|
+
})
|
|
798
|
+
|
|
383
799
|
it('disposes exactly as many composers as it constructs, across repeated prop changes', async () => {
|
|
384
800
|
const ref = React.createRef<EffectComposerImpl>()
|
|
385
801
|
const disposeSpy = vi.spyOn(EffectComposerImpl.prototype, 'dispose')
|
|
@@ -405,6 +821,42 @@ describe('EffectComposer', () => {
|
|
|
405
821
|
disposeSpy.mockRestore()
|
|
406
822
|
})
|
|
407
823
|
|
|
824
|
+
it('does not dispose a still-in-use effect when a composer-level prop (multisampling) recreates the composer', async () => {
|
|
825
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
826
|
+
const effectRef = React.createRef<EffectA>()
|
|
827
|
+
|
|
828
|
+
await React.act(async () =>
|
|
829
|
+
root.render(
|
|
830
|
+
<EffectComposer ref={ref} multisampling={0}>
|
|
831
|
+
<WrappedEffectA ref={effectRef} />
|
|
832
|
+
</EffectComposer>
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
const firstComposer = await waitForComposer(ref)
|
|
836
|
+
await waitForEffects(ref, 1)
|
|
837
|
+
const effect = effectRef.current
|
|
838
|
+
expect(effect).toBeTruthy()
|
|
839
|
+
|
|
840
|
+
const effectDisposeSpy = vi.spyOn(EffectA.prototype, 'dispose')
|
|
841
|
+
|
|
842
|
+
await React.act(async () =>
|
|
843
|
+
root.render(
|
|
844
|
+
<EffectComposer ref={ref} multisampling={4}>
|
|
845
|
+
<WrappedEffectA ref={effectRef} />
|
|
846
|
+
</EffectComposer>
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
const secondComposer = await waitForNewComposer(ref, firstComposer)
|
|
850
|
+
await flush()
|
|
851
|
+
|
|
852
|
+
expect(secondComposer).not.toBe(firstComposer)
|
|
853
|
+
expect(effectRef.current).toBe(effect)
|
|
854
|
+
expect(effectDisposeSpy).not.toHaveBeenCalled()
|
|
855
|
+
expect(secondComposer.passes.some((p) => p instanceof EffectPass)).toBe(true)
|
|
856
|
+
|
|
857
|
+
effectDisposeSpy.mockRestore()
|
|
858
|
+
})
|
|
859
|
+
|
|
408
860
|
it('disposes a hand-constructed effect exactly once on unmount', async () => {
|
|
409
861
|
const disposeSpy = vi.spyOn(ColorAverageEffect.prototype, 'dispose')
|
|
410
862
|
const ref = React.createRef<EffectComposerImpl>()
|
|
@@ -425,7 +877,7 @@ describe('EffectComposer', () => {
|
|
|
425
877
|
disposeSpy.mockRestore()
|
|
426
878
|
})
|
|
427
879
|
|
|
428
|
-
it('
|
|
880
|
+
it('keeps a single ColorAverage instance across repeated blendFunction changes and disposes it exactly once (blendFunction is live, not construction-only)', async () => {
|
|
429
881
|
const disposeSpy = vi.spyOn(ColorAverageEffect.prototype, 'dispose')
|
|
430
882
|
const ref = React.createRef<ColorAverageEffect>()
|
|
431
883
|
const seenInstances = new Set<ColorAverageEffect>()
|
|
@@ -446,15 +898,16 @@ describe('EffectComposer', () => {
|
|
|
446
898
|
|
|
447
899
|
await React.act(async () => root.render(null))
|
|
448
900
|
|
|
449
|
-
expect(seenInstances.size).toBe(
|
|
450
|
-
expect(disposeSpy).toHaveBeenCalledTimes(
|
|
901
|
+
expect(seenInstances.size).toBe(1)
|
|
902
|
+
expect(disposeSpy).toHaveBeenCalledTimes(1)
|
|
451
903
|
} finally {
|
|
452
904
|
disposeSpy.mockRestore()
|
|
453
905
|
}
|
|
454
906
|
})
|
|
455
907
|
|
|
456
|
-
it('
|
|
908
|
+
it('disposes every ColorAverage instance seen, even across StrictMode\'s mount/cleanup/mount cycle', async () => {
|
|
457
909
|
const disposedNodes: ColorAverageEffect[] = []
|
|
910
|
+
const seenInstances = new Set<ColorAverageEffect>()
|
|
458
911
|
const disposeSpy = vi.spyOn(ColorAverageEffect.prototype, 'dispose').mockImplementation(function (
|
|
459
912
|
this: ColorAverageEffect
|
|
460
913
|
) {
|
|
@@ -474,11 +927,17 @@ describe('EffectComposer', () => {
|
|
|
474
927
|
)
|
|
475
928
|
)
|
|
476
929
|
await flush()
|
|
930
|
+
if (ref.current) seenInstances.add(ref.current)
|
|
477
931
|
}
|
|
478
932
|
await React.act(async () => root.render(null))
|
|
479
933
|
|
|
480
|
-
|
|
481
|
-
|
|
934
|
+
// dispose() is idempotent (just event-firing / shallow property
|
|
935
|
+
// disposal, no internal state), so StrictMode calling it more than
|
|
936
|
+
// once per instance is fine - this only checks nothing leaked.
|
|
937
|
+
const disposedSet = new Set(disposedNodes)
|
|
938
|
+
for (const instance of seenInstances) {
|
|
939
|
+
expect(disposedSet.has(instance)).toBe(true)
|
|
940
|
+
}
|
|
482
941
|
} finally {
|
|
483
942
|
disposeSpy.mockRestore()
|
|
484
943
|
}
|
|
@@ -883,7 +1342,7 @@ describe('EffectComposer', () => {
|
|
|
883
1342
|
})
|
|
884
1343
|
|
|
885
1344
|
describe('performance characteristics (documented, not enforced)', () => {
|
|
886
|
-
it('rebuilds the EffectPass
|
|
1345
|
+
it('rebuilds the EffectPass at most twice when mounting many effects at once', async () => {
|
|
887
1346
|
const addPassSpy = vi.spyOn(EffectComposerImpl.prototype, 'addPass')
|
|
888
1347
|
|
|
889
1348
|
const ref = React.createRef<EffectComposerImpl>()
|
|
@@ -903,9 +1362,43 @@ describe('EffectComposer', () => {
|
|
|
903
1362
|
|
|
904
1363
|
const effectPassAddCalls = addPassSpy.mock.calls.filter(([pass]) => pass instanceof EffectPass).length
|
|
905
1364
|
|
|
906
|
-
|
|
1365
|
+
// The node-list change detector and the pass-building effect settle
|
|
1366
|
+
// over two synchronous layout-effect passes on first mount (detect
|
|
1367
|
+
// change -> bump a version -> rebuild once more) - a one-time cost,
|
|
1368
|
+
// not a per-render one. See the "does not rebuild on unrelated
|
|
1369
|
+
// re-renders" test below for the actual guarantee this trades for.
|
|
1370
|
+
expect(effectPassAddCalls).toBeLessThanOrEqual(2)
|
|
1371
|
+
|
|
1372
|
+
addPassSpy.mockRestore()
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
it('does not rebuild the EffectPass (or re-run EffectPass.initialize) on unrelated re-renders', async () => {
|
|
1376
|
+
const ref = React.createRef<EffectComposerImpl>()
|
|
1377
|
+
|
|
1378
|
+
const render = (tick: number) =>
|
|
1379
|
+
root.render(
|
|
1380
|
+
<EffectComposer ref={ref}>
|
|
1381
|
+
<WrappedEffectA />
|
|
1382
|
+
<group name={`tick-${tick}`} />
|
|
1383
|
+
</EffectComposer>
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
await React.act(async () => render(0))
|
|
1387
|
+
await waitForEffects(ref, 1)
|
|
1388
|
+
|
|
1389
|
+
const addPassSpy = vi.spyOn(EffectComposerImpl.prototype, 'addPass')
|
|
1390
|
+
const initializeSpy = vi.spyOn(EffectPass.prototype, 'initialize')
|
|
1391
|
+
|
|
1392
|
+
for (let t = 1; t <= 5; t++) {
|
|
1393
|
+
await React.act(async () => render(t))
|
|
1394
|
+
await flush()
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
expect(addPassSpy).not.toHaveBeenCalled()
|
|
1398
|
+
expect(initializeSpy).not.toHaveBeenCalled()
|
|
907
1399
|
|
|
908
1400
|
addPassSpy.mockRestore()
|
|
1401
|
+
initializeSpy.mockRestore()
|
|
909
1402
|
})
|
|
910
1403
|
})
|
|
911
1404
|
})
|