Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/EffectGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Group>(null!)
const effectsRef = useRef<Effect[]>([])
Expand All @@ -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(() => {
Expand Down
38 changes: 37 additions & 1 deletion src/tests/EffectGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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<EffectComposerImpl>()
const cameraA = new PerspectiveCamera(50, 1, 0.1, 10)
const cameraB = new PerspectiveCamera(50, 1, 1.5, 500)

const render = (camera: PerspectiveCamera) =>
root.render(
<EffectComposer ref={composerRef} camera={camera}>
<EffectGroup>
<WrappedEffectA />
</EffectGroup>
</EffectComposer>
)

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)
})
})
Loading