Skip to content

Release/3.1.0 - #377

Merged
kvvasuu merged 47 commits into
masterfrom
release/3.1.0
Aug 23, 2026
Merged

Release/3.1.0#377
kvvasuu merged 47 commits into
masterfrom
release/3.1.0

Conversation

@kvvasuu

@kvvasuu kvvasuu commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Internal rewrite of how effects apply live prop changes - public props are unchanged except for one cleanup (see Breaking below), but a lot of previously-broken live updates now actually work.

Highlights

  • New EffectGroup component for grouping effects behind one cheap enabled toggle.
  • EffectComposer gains mergeMode, renderPass, and autoRenderToScreen for more control over pass composition.
  • createEffectComponent (the factory this library uses internally for simple effects) is now public, for wrapping your own effects the same way.
  • New DepthPicking component / useDepthPicking hook, split out of Autofocus into a reusable primitive.
  • Several effects (SSAO, DepthOfField, N8AO, LensFlare, Grid, Glitch, and more) had live prop updates that were silently no-ops -now fixed.
  • Outline/GodRays now warn when mounted without , which both silently require.
  • Switched from yarn to pnpm.

##Breaking

  • Pixelation/ShockWave no longer accept blendFunction/opacity - they never did anything (both are mainUv-only shaders with no color output to blend), so this is a no-op removal for anyone relying on real behavior.

Closes #68, closes #165, closes #182, closes #187 ,closes #257, closes #282, closes #285, closes #288, closes #292, closes #304, closes #323, closes #328, closes #336,

kvvasuu added 30 commits August 5, 2026 20:38
Registers a postprocessing effect class as an r3f intrinsic and lets r3f's own reconciler handle args-driven reconstruction, live prop application, and disposal - no custom accessor scanning or fingerprinting. Also adds useLiveDefaults, the equivalent live-prop mechanism for effects that need real constructor args and can't use r3f's native reset-on-removal.
Passes are now derived from the r3f scene graph and only rebuilt when the resolved node list actually changes, not on every render. Fixes real GPU-resource bugs found along the way: composer-level prop changes (multisampling etc.) could dispose effects still in use by the new composer, discarded EffectPass wrappers leaked their own material and kept a stale change listener on the effect they wrapped, and a user's own EffectPass rendered as a child could be mistaken for one we generated.
Covers every effect whose postprocessing class constructs with zero arguments (Bloom, Noise, Vignette, FXAA, and ~20 others) - live props update the existing instance instead of reconstructing on every change, construction-only options move to explicit args. Also fixes a few bugs these effects had on top of the migration: opacity typing on nine of them, ChromaticAberration's radialModulation/modulationOffset incorrectly required, ColorDepth's bits not resetting on removal.
Outline, SelectiveBloom, ShockWave, GodRays, DepthOfField, SSAO, LUT, and N8AO all need real constructor args (scene/camera/etc.), so they stay hand-built with useMemo, but now apply live props through useLiveDefaults instead of reconstructing on every change.

This is where nearly every real runtime bug from review surfaced: a first-apply bug where a still-correct value's setter fired anyway (Outline's multisampling disposing its render target before first use - the actual reason several of these didn't render at all), SSAO's color/fade/minRadiusScale/world* thresholds not resetting on removal, DepthOfField's depthTexture reconstructing instead of using the live setDepthTexture, and GodRays/N8AO not invalidating on live changes under frameloop="demand".
makeDisposeIdempotent guarded against depthPickingPass/copyPass getting disposed twice (once by the composer's own teardown, once by Autofocus's own cleanup) - unnecessary, since postprocessing/three dispose() is confirmed idempotent (event-fire or shallow property disposal, no internal state).
Removed redundant comments explaining ref behavior.
Passes are now derived from the r3f scene graph and only rebuilt when the resolved node list actually changes, not on every render. Fixes real GPU-resource bugs found along the way: composer-level prop changes (multisampling etc.) could dispose effects still in use by the new composer, discarded EffectPass wrappers leaked their own material and kept a stale change listener on the effect they wrapped, and a user's own EffectPass rendered as a child could be mistaken for one we generated.
Covers every effect whose postprocessing class constructs with zero arguments (Bloom, Noise, Vignette, FXAA, and ~20 others) - live props update the existing instance instead of reconstructing on every change, construction-only options move to explicit args. Also fixes a few bugs these effects had on top of the migration: opacity typing on nine of them, ChromaticAberration's radialModulation/modulationOffset incorrectly required, ColorDepth's bits not resetting on removal.
Outline, SelectiveBloom, ShockWave, GodRays, DepthOfField, SSAO, LUT, and N8AO all need real constructor args (scene/camera/etc.), so they stay hand-built with useMemo, but now apply live props through useLiveDefaults instead of reconstructing on every change.

This is where nearly every real runtime bug from review surfaced: a first-apply bug where a still-correct value's setter fired anyway (Outline's multisampling disposing its render target before first use - the actual reason several of these didn't render at all), SSAO's color/fade/minRadiusScale/world* thresholds not resetting on removal, DepthOfField's depthTexture reconstructing instead of using the live setDepthTexture, and GodRays/N8AO not invalidating on live changes under frameloop="demand".
makeDisposeIdempotent guarded against depthPickingPass/copyPass getting disposed twice (once by the composer's own teardown, once by Autofocus's own cleanup) - unnecessary, since postprocessing/three dispose() is confirmed idempotent (event-fire or shallow property disposal, no internal state).
Add createEffectComponent, a thin r3f-native effect factory
Passes are now derived from the r3f scene graph and only rebuilt when the resolved node list actually changes, not on every render. Fixes real GPU-resource bugs found along the way: composer-level prop changes (multisampling etc.) could dispose effects still in use by the new composer, discarded EffectPass wrappers leaked their own material and kept a stale change listener on the effect they wrapped, and a user's own EffectPass rendered as a child could be mistaken for one we generated.
Rewrite EffectComposer's pass lifecycle for correctness and cost
Covers every effect whose postprocessing class constructs with zero arguments (Bloom, Noise, Vignette, FXAA, and ~20 others) - live props update the existing instance instead of reconstructing on every change, construction-only options move to explicit args. Also fixes a few bugs these effects had on top of the migration: opacity typing on nine of them, ChromaticAberration's radialModulation/modulationOffset incorrectly required, ColorDepth's bits not resetting on removal.
Migrate simple effects to createEffectComponent
Outline, SelectiveBloom, ShockWave, GodRays, DepthOfField, SSAO, LUT, and N8AO all need real constructor args (scene/camera/etc.), so they stay hand-built with useMemo, but now apply live props through useLiveDefaults instead of reconstructing on every change.

This is where nearly every real runtime bug from review surfaced: a first-apply bug where a still-correct value's setter fired anyway (Outline's multisampling disposing its render target before first use - the actual reason several of these didn't render at all), SSAO's color/fade/minRadiusScale/world* thresholds not resetting on removal, DepthOfField's depthTexture reconstructing instead of using the live setDepthTexture, and GodRays/N8AO not invalidating on live changes under frameloop="demand".
Migrate hand-rolled effects to useLiveDefaults
Simplify Autofocus's dispose handling, drop idempotency guard
They only have real setters on ssaoMaterial, not SSAOEffect itself, so the live props were silent no-ops.
…e toggle

Groups effects into one EffectPass with a cheap enabled toggle, backed by a shared trailing CopyPass so disabling the last pass in a chain doesn't blank the canvas. Also fixes Autofocus's own passes racing that same mechanism, and gives N8AO a native enabled prop since it's a standalone Pass, not an Effect.
Documents the new grouping/enable-toggle API and its limitations (non-Effect passes like N8AO, convolution effects).
screenRes defaulted to a fresh Vector2(0,0) on each render, which r3f
copies in place onto the uniform whenever a parent re-renders (e.g. a
Leva control change) - wiping the viewport-synced value back to (0,0)
and producing a fully black frame from the resulting divide-by-zero,
until the next resize.
AutofocusProps intersected ComponentProps<typeof DepthOfField> (which
already declares its own ref) with a differently-typed ref of its own,
so TypeScript intersected the two ref types instead of the second
overriding the first - making any RefObject<AutofocusApi> a type error.
useEffect(() => composer.setSize(size.width, size.height), [composer, size])
used r3f's size, which drei's View overrides per-portal only on View's own
re-renders - not kept live, so a resize inside a View silently never
reaches it. gl.getSize() isn't portal-scoped, and checking it every frame
(cheap: no GPU sync, setSize itself only runs when the size actually
changed) means this can't be missed regardless of whether this component
ever re-renders. Fixes the composer being offset/wrong-sized inside View.
Mirrors postprocessing's own EffectComposer.autoRenderToScreen, set to
false to render to a target of your own (e.g. via a trailing CopyPass)
instead of the screen. Not a constructor option in postprocessing, but
only ever read inside addPass()/removePass(), so it's applied the same
way as depthBuffer/multisampling: changing it recreates the composer,
rather than trying to patch it onto already-added passes.

Closes #323
Lets consumers supply their own scene/camera render pass (e.g. with an
overrideMaterial), fixing #336. Closes #336.
'auto' (default) merges effects into as few passes as possible while
keeping at most one convolution effect per pass, matching what
postprocessing itself allows. 'all' drops that limit (same no-guardrail
contract as EffectGroup). 'none' gives every effect its own pass.
Fixes #304.
…presentation

Runtime already wrapped these in new Color(value), so string colors worked
regardless of what the number-only type said. Closes #187, #182.
kvvasuu added 17 commits August 14, 2026 21:53
@react-three/fiber, maath, and postprocessing itself all ship unminified -
the consumer's bundler minifies again anyway, and this keeps stack traces
and devtools readable.
tsconfig.json's exclude only matched *.test.* filenames, missing the
test-utils.tsx helper - it was excluded from typecheck (never actually
checked) yet still emitted into dist. Split into a build-only
tsconfig.build.json that excludes src/tests entirely, and dropped the
exclude from the base config so typecheck now covers every test file too.
# Conflicts:
#	docs/effects/outline.mdx
Silences Vite's configLoader: 'native' deprecation warning on every run.
Moved from docs/effects/ to docs/ alongside effect-composer/effect-group,
since it's a guide, not a single effect's reference page. Content was
still teaching the old manual useMemo+useDispose pattern with no mention
of createEffectComponent or useLiveDefaults - now leads with those.
Extracted from Autofocus into a standalone, reusable primitive for reading world-space positions off the depth buffer. Also splits DepthPicking and N8AO into a new src/passes directory, since neither wraps a postprocessing Effect.
feat: add DepthPicking component and useDepthPicking hook
Removes yarn.lock in favor of pnpm-lock.yaml, updates CI/release workflows and CONTRIBUTING.md, and pins the pnpm version via packageManager in package.json.
…d blend props

Add useVector3, fix ShockWave/LensFlare position props to accept tuples not just Vector3. Also fixes LensFlare's occlusion default (no raycast hit now correctly shows the flare instead of hiding it). Remove blendFunction/opacity from ShockWave and Pixelation - both are mainUv-only effects with no color output to blend.
LensFlare was the only effect not forwarding a ref to its underlying instance. Extract the merge-ref logic (local + caller ref) shared with createEffectComponent into useMergeRefs.
N8AO gained enabled, ASCII's color widened to ColorRepresentation, ShockWave's position now accepts tuples. Moved n8ao.mdx to docs/passes/ to match its v4 source location and reworded it to say "pass" instead of "effect".
Both effects render internal extra passes that need it to work correctly. Exposes the resolved autoClear prop via EffectComposerContext and documents the requirement in their docs pages.
Reserves 4.0.0 for the next breaking rework, likely driven by postprocessing v7.
@kvvasuu kvvasuu added the v3.1 label Aug 23, 2026
@kvvasuu
kvvasuu merged commit 3e1f680 into master Aug 23, 2026
1 check passed
@kvvasuu
kvvasuu deleted the release/3.1.0 branch August 23, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment