diff --git a/src/EffectGroup.tsx b/src/EffectGroup.tsx index a2a111c..10e29f5 100644 --- a/src/EffectGroup.tsx +++ b/src/EffectGroup.tsx @@ -18,7 +18,7 @@ export type EffectGroupProps = { // component's own JSX position, so EffectComposer's existing bare-Pass // passthrough places it correctly relative to sibling effects. export function EffectGroup({ enabled = true, children, ref }: EffectGroupProps) { - const { camera, requestRebuild } = use(EffectComposerContext) + const { camera, scene, requestRebuild } = use(EffectComposerContext) const group = useRef(null!) const effectsRef = useRef([]) @@ -42,6 +42,16 @@ export function EffectGroup({ enabled = true, children, ref }: EffectGroupProps) setPass(newPass) }) + // The pass snapshots camera near/far (and forwards mainCamera/mainScene to + // its effects) only at construction. EffectComposer's own passes are rebuilt + // when the composer camera or scene changes; this one is kept, so depth + // effects inside the group would keep reading the previous camera. + useLayoutEffect(() => { + if (!pass) return + pass.mainCamera = camera + pass.mainScene = scene + }, [pass, camera, scene]) + // useLayoutEffect (not useEffect) so `enabled` is applied before // EffectComposer's own effect adds this pass to the composer. useLayoutEffect(() => { diff --git a/src/tests/EffectGroup.test.tsx b/src/tests/EffectGroup.test.tsx index e091b9c..3293634 100644 --- a/src/tests/EffectGroup.test.tsx +++ b/src/tests/EffectGroup.test.tsx @@ -1,10 +1,11 @@ import { CopyPass, Effect, EffectComposer as EffectComposerImpl, EffectPass, RenderPass } from 'postprocessing' import * as React from 'react' +import { PerspectiveCamera } from 'three' import { afterEach, describe, expect, it, vi } from 'vitest' import { EffectComposer } from '../EffectComposer' import { EffectGroup } from '../EffectGroup' import { wrapEffect } from '../wrapEffect' -import { EFFECT_SHADER, flush, root, strict, waitForComposer } from './test-utils' +import { EFFECT_SHADER, flush, root, strict, waitForComposer, waitForNewComposer } from './test-utils' class EffectA extends Effect { constructor() { @@ -334,4 +335,39 @@ describe('EffectGroup', () => { expect(composer.passes.filter((p) => p instanceof EffectPass)).toHaveLength(1) }) + + it("copies the EffectComposer's camera onto the pass when that camera changes", async () => { + const composerRef = React.createRef() + const cameraA = new PerspectiveCamera(50, 1, 0.1, 10) + const cameraB = new PerspectiveCamera(50, 1, 1.5, 500) + + const render = (camera: PerspectiveCamera) => + root.render( + + + + + + ) + + await React.act(async () => render(cameraA)) + const firstComposer = await waitForComposer(composerRef) + const pass = await waitForGroupPass(firstComposer) + const material = () => + pass.fullscreenMaterial as unknown as { + uniforms: { cameraNear: { value: number }; cameraFar: { value: number } } + } + + expect(material().uniforms.cameraNear.value).toBeCloseTo(cameraA.near) + expect(material().uniforms.cameraFar.value).toBeCloseTo(cameraA.far) + + await React.act(async () => render(cameraB)) + const secondComposer = await waitForNewComposer(composerRef, firstComposer) + await waitUntil(() => secondComposer.passes.includes(pass)) + + // Same pass — EffectGroup does not rebuild it just because the camera changed. + expect(secondComposer.passes).toContain(pass) + expect(material().uniforms.cameraNear.value).toBeCloseTo(cameraB.near) + expect(material().uniforms.cameraFar.value).toBeCloseTo(cameraB.far) + }) })