Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6a16e04
Add createEffectComponent, a thin r3f-native effect factory
kvvasuu Aug 5, 2026
a3986a9
Rewrite EffectComposer's pass lifecycle for correctness and cost
kvvasuu Aug 5, 2026
e830758
Migrate simple effects to createEffectComponent
kvvasuu Aug 5, 2026
89dfefb
Migrate hand-rolled effects to useLiveDefaults
kvvasuu Aug 5, 2026
787c450
Simplify Autofocus's dispose handling, drop idempotency guard
kvvasuu Aug 5, 2026
5045ba4
Clean up comments in createEffectComponent
kvvasuu Aug 5, 2026
965b7fb
Rewrite EffectComposer's pass lifecycle for correctness and cost
kvvasuu Aug 5, 2026
8219d39
Migrate simple effects to createEffectComponent
kvvasuu Aug 5, 2026
8a776a9
Migrate hand-rolled effects to useLiveDefaults
kvvasuu Aug 5, 2026
a5c9785
Simplify Autofocus's dispose handling, drop idempotency guard
kvvasuu Aug 5, 2026
c8ad71c
Merge branch 'pr5/autofocus' of https://github.com/pmndrs/react-postp…
kvvasuu Aug 5, 2026
8884d64
Merge pull request #363 from pmndrs/pr1/foundation
kvvasuu Aug 9, 2026
8a767f2
Rewrite EffectComposer's pass lifecycle for correctness and cost
kvvasuu Aug 5, 2026
308b36f
Merge pull request #364 from pmndrs/pr2/effect-composer
kvvasuu Aug 9, 2026
54833ba
Migrate simple effects to createEffectComponent
kvvasuu Aug 5, 2026
ce9ec92
Merge pull request #366 from pmndrs/pr3/simple-effects
kvvasuu Aug 9, 2026
cc588f4
Migrate hand-rolled effects to useLiveDefaults
kvvasuu Aug 5, 2026
fca336d
Merge pull request #367 from pmndrs/pr4/hand-rolled-effects
kvvasuu Aug 9, 2026
3317962
Merge pull request #368 from pmndrs/pr5/autofocus
kvvasuu Aug 9, 2026
3279c9e
fix(SSAO): pierce worldProximityThreshold/Falloff through ssaoMaterial
kvvasuu Aug 9, 2026
b995a5f
Merge branch 'master' into v4
kvvasuu Aug 9, 2026
22c8e76
feat(EffectGroup): add explicit effect grouping with pass-level enabl…
kvvasuu Aug 13, 2026
12364ca
docs: add EffectGroup page
kvvasuu Aug 13, 2026
fe50084
fix(LensFlare): stop resetting screenRes uniform on every re-render
kvvasuu Aug 14, 2026
4405095
fix(Autofocus): stop ref prop from colliding with DepthOfField's own ref
kvvasuu Aug 14, 2026
5d1f6e5
fix(EffectComposer): size composer off gl.getSize(), checked every frame
kvvasuu Aug 14, 2026
b9a6be2
feat(EffectComposer): add autoRenderToScreen prop
kvvasuu Aug 14, 2026
41372e7
feat(EffectComposer): add renderPass prop for custom RenderPass factory
kvvasuu Aug 14, 2026
45da20b
feat(EffectComposer): add mergeMode prop to control effect pass merging
kvvasuu Aug 14, 2026
acd7688
fix(Outline): widen visibleEdgeColor/hiddenEdgeColor types to ColorRe…
kvvasuu Aug 14, 2026
de11744
build: disable minification, matching the rest of the ecosystem
kvvasuu Aug 14, 2026
35ff80c
build: stop leaking test-utils.d.ts into dist, typecheck all tests
kvvasuu Aug 14, 2026
6766c75
Merge branch 'master' into v4
kvvasuu Aug 15, 2026
777c26b
build: use import.meta.dirname instead of __dirname in vite.config.ts
kvvasuu Aug 15, 2026
0100604
docs: rewrite custom-effects guide for v4, promote to top-level page
kvvasuu Aug 15, 2026
6f54ab0
feat: add DepthPicking component and useDepthPicking hook - #288
kvvasuu Aug 15, 2026
1fe2f27
Merge pull request #372 from pmndrs/feat/depth-picking-pass
kvvasuu Aug 15, 2026
689754e
chore: switch from yarn to pnpm - #257
kvvasuu Aug 15, 2026
cff4243
Merge pull request #373 from pmndrs/chore/switch-to-pnpm
kvvasuu Aug 15, 2026
9da8d0c
Merge branch 'master' into v4
kvvasuu Aug 15, 2026
b45d7a5
fix: ReactThreeFiber.Vector3 for hand-rolled position props, drop dea…
kvvasuu Aug 16, 2026
140ff7d
feat: expose LensFlare's effect instance via ref
kvvasuu Aug 16, 2026
311328e
Merge branch 'master' into v4
kvvasuu Aug 16, 2026
76b9f89
docs: update effect pages for v4 API, move N8AO to passes
kvvasuu Aug 16, 2026
43a90c2
feat(Outline,GodRays): warn when autoClear={false} is missing
kvvasuu Aug 17, 2026
bf50602
Merge branch 'master' into v4
kvvasuu Aug 21, 2026
d954afc
chore: bump version to 3.1.0
kvvasuu Aug 23, 2026
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
11 changes: 6 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v3
with:
cache: 'yarn'
cache: 'pnpm'
- name: Install Dependencies
run: yarn install --frozen-lockfile
run: pnpm install --frozen-lockfile

- name: Check build health
run: yarn build
run: pnpm build

- name: Check for regressions
run: yarn eslint:ci
run: pnpm eslint:ci

- name: Run tests
run: yarn test
run: pnpm test
12 changes: 7 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ jobs:
- name: Check out repository
uses: actions/checkout@v6

- uses: pnpm/action-setup@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
package-manager-cache: false
cache: 'pnpm'

- name: Verify release version
env:
Expand All @@ -60,16 +62,16 @@ jobs:
fi

- name: Install dependencies
run: yarn install --frozen-lockfile
run: pnpm install --frozen-lockfile

- name: Lint
run: yarn eslint:ci
run: pnpm eslint:ci

- name: Test
run: yarn test
run: pnpm test

- name: Build
run: yarn build
run: pnpm build

- name: Publish package
env:
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ yarn-error.log
.size-snapshot.json
pnpm-debug.log
.parcel-cache
pnpm-lock.yaml
storybook-static

*.tgz
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Thanks for wanting to make a contribution and wanting to improve this library fo
## How to Contribute

1. Fork and clone the repo
2. Run `yarn install` to install dependencies
2. Run `pnpm install` to install dependencies
3. Create a branch for your PR with `git checkout -b pr-type/issue-number-your-branch-name`
4. Let's get cooking! 👨🏻‍🍳🥓

Expand Down
85 changes: 85 additions & 0 deletions docs/custom-effects.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: Custom effects
description: Wrapping your own effects, or postprocessing's, as components
nav: 0
---

Most effects from `postprocessing` are already wrapped by this library, but if you need one that isn't, or want to write your own, there are three ways to do it depending on what the effect's constructor needs.

## Zero-arg effects (recommended)

If the effect's constructor works with zero arguments (`new SomeEffect()`), `createEffectComponent` gives you a component with live-updating props for free - no `useMemo`/`useDispose` needed, and no reconstruction on every prop change. This is exactly how this library's own simple effects (`BrightnessContrast`, `ChromaticAberration`, `Noise`, ...) are built:

```jsx
import { SomeEffect } from 'postprocessing'
import { createEffectComponent } from '@react-three/postprocessing'

export const SomeEffectComponent = createEffectComponent(SomeEffect)
```

Every constructor option becomes a live prop that updates the existing instance in place, and `blendFunction`/`opacity` are supported automatically. `ref` resolves to the effect instance. Disposal is handled for you - unlike `<primitive>`, r3f disposes elements it constructed itself.

### Custom defaults

If you want different defaults than the class's own, or the constructor takes a positional argument instead of an options object, wrap it in a thin component - this is how this library's own `Pixelation` is built (its default `granularity` is `5`, not the class's own `30`):

```jsx
import { PixelationEffect } from 'postprocessing'
import { createEffectComponent } from '@react-three/postprocessing'

const PixelationImpl = createEffectComponent(PixelationEffect)

export function Pixelation({ granularity = 5, ...props }) {
return <PixelationImpl granularity={granularity} {...props} />
}
```

## Effects that need real constructor arguments

Effects whose constructor needs more than zero arguments - e.g. `OutlineEffect(scene, camera, options)` - can't use `createEffectComponent`: it relies on r3f's `extend()`, which always constructs via `new Effect()`. Build these by hand instead: `useMemo`/`useDispose` for construction, `useLiveDefaults` for props that should update the existing instance rather than reconstruct it.

```jsx
import { useMemo } from 'react'
import { SomeEffect } from 'postprocessing'
import { useDispose, useLiveDefaults } from '@react-three/postprocessing'

const LIVE_KEYS = ['someProp', 'anotherProp']

export function SomeEffectComponent({ requiredArg, someProp, anotherProp, ref }) {
const effect = useMemo(() => new SomeEffect(requiredArg), [requiredArg])

useLiveDefaults(effect, { someProp, anotherProp }, LIVE_KEYS)
useDispose(effect)

return <primitive ref={ref} object={effect} />
}
```

`LIVE_KEYS` should only list options that have a real setter on the class - check the effect's own source. `useLiveDefaults` resets a prop to the effect's constructor-time default when it's removed, and only calls the setter when the resolved value actually changed (some setters have side effects beyond storing the value). See `Outline.tsx` in this repo for a real example, including piercing into a nested property like `blendMode.blendFunction`.

## Writing a brand new effect

For effects that don't exist in `postprocessing` at all, extend `Effect` and wrap the result the same way as any other zero-arg effect:

```jsx
import { Effect } from 'postprocessing'
import { Uniform } from 'three'
import { createEffectComponent } from '@react-three/postprocessing'

const fragmentShader = `some_shader_code`

class MyCustomEffect extends Effect {
constructor({ param = 0.1 } = {}) {
super('MyCustomEffect', fragmentShader, {
uniforms: new Map([['param', new Uniform(param)]]),
})
}

update(renderer, inputBuffer, deltaTime) {
// read/write per-frame state on `this` (e.g. this.uniforms.get('param').value = ...),
// never on a module-level variable - that would be shared across every instance
}
}

export const MyCustomEffectComponent = createEffectComponent(MyCustomEffect)
```
6 changes: 6 additions & 0 deletions docs/effect-composer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ The `EffectComposer` must wrap all your effects. It will manage them for you.
enableNormalPass?: boolean
stencilBuffer?: boolean
autoClear?: boolean
/** Whether the last pass automatically renders to the screen. Set to `false` when rendering to a target of your own (e.g. via a trailing CopyPass) instead. */
autoRenderToScreen?: boolean
multisampling?: number
frameBufferType?: TextureDataType
/** For effects that support DepthDownsamplingPass */
resolutionScale?: number
renderPriority?: number
camera?: THREE.Camera
scene?: THREE.Scene
/** Constructs the initial pass that renders the scene - override to substitute your own RenderPass-like implementation. Defaults to `(scene, camera) => new RenderPass(scene, camera)`. */
renderPass?: (scene: THREE.Scene, camera: THREE.Camera) => Pass
/** Controls how effects are merged into EffectPass instances. `'auto'` (default) merges consecutive effects into one pass, keeping at most one convolution effect (e.g. DepthOfField) per pass - matching what postprocessing actually supports. `'all'` merges without that limit - only enable it if you've verified your specific combination works, since multiple convolution effects sharing a pass throws at render time. `'none'` disables merging entirely, giving every effect its own pass. */
mergeMode?: 'auto' | 'all' | 'none'
>
{/* your effects go here */}
</EffectComposer>
Expand Down
48 changes: 48 additions & 0 deletions docs/effect-group.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: EffectGroup
description: Explicit effect grouping with a pass-level enable toggle
nav: 0
---

`EffectComposer` groups effects into passes automatically: consecutive effects
merge into one `EffectPass`, based only on what happens to sit next to them in
JSX. `EffectGroup` lets you group specific effects into one pass explicitly,
regardless of what's around them, and gives that whole pass a cheap
`enabled` toggle - the same thing `Pass.enabled` gives you in vanilla
`postprocessing`, without reconstructing anything.

```jsx
<EffectGroup
enabled?: boolean // true
>
{/* effects to group go here */}
</EffectGroup>
```

```jsx
<EffectComposer>
<Bloom />
<EffectGroup enabled={showVignetteAndNoise}>
<Vignette />
<Noise />
</EffectGroup>
</EffectComposer>
```

`Bloom` still gets its own pass as usual. `Vignette` and `Noise` are merged
into one pass together, positioned exactly where `EffectGroup` sits in the
JSX, and toggling `enabled` turns that whole pass on or off without rebuilding
it - so it's cheap to flip every frame if you need to.

`ref` resolves to the underlying `EffectPass`:

```jsx
<EffectGroup ref={groupRef}>...</EffectGroup>
```

## Limitations

- `EffectGroup` only groups `postprocessing` `Effect`s - things that merge
into one shader pass. A handful of effects in this library (like `N8AO`)
are full standalone passes, not effects, and can't be grouped this way -
they take their own `enabled` prop instead.
2 changes: 1 addition & 1 deletion docs/effects/ascii.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ return (
| characters | String | ` .:,'-^=*+?!\|0#X%WM@` | The characters to sample from, ordered from "empty" to "dense". |
| fontSize | Number | 54 | The font size used to draw the character atlas. |
| cellSize | Number | 16 | The size of each character cell, in pixels. |
| color | String | '#ffffff' | The color of the characters. |
| color | ColorRepresentation | '#ffffff' | The color of the characters. |
| invert | Boolean | false | Inverts which characters map to bright vs dark pixels. |
2 changes: 1 addition & 1 deletion docs/effects/autofocus.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ nav: 1

An auto-focus effect, that extends `<DepthOfField>`.

Based on [ektogamat/AutoFocusDOF](https://github.com/ektogamat/AutoFocusDOF).
Based on [ektogamat/AutoFocusDOF](https://github.com/ektogamat/AutoFocusDOF). Built on `<DepthPicking>` and `useDepthPicking` internally - use those directly if you want a picked position for something other than `<DepthOfField>`'s own focus target.

```tsx
export type AutofocusProps = typeof DepthOfField & {
Expand Down
54 changes: 0 additions & 54 deletions docs/effects/custom-effects.mdx

This file was deleted.

2 changes: 2 additions & 0 deletions docs/effects/god-rays.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ nav: 1

The GodRays effect requires a mesh that will be used as an origin point for the rays. Refer to this [example](https://pmndrs.github.io/examples/take-control) for more details.

For correct occlusion by other objects, also set `<EffectComposer autoClear={false}>`.

```jsx
import { GodRays } from '@react-three/postprocessing'

Expand Down
40 changes: 22 additions & 18 deletions docs/effects/outline.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ nav: 1

An outline effect.

Requires `<EffectComposer autoClear={false}>` - without it, outlines don't render at all.

```jsx
import { Outline } from '@react-three/postprocessing'
import { BlendFunction, Resolution, KernelSize } from 'postprocessing'
Expand Down Expand Up @@ -37,21 +39,23 @@ return (

## Props

| Name | Type | Default | Description |
| ---------------- | ------------- | --------------------- | -------------------------------------------------------------------------- |
| selection | Objects | | Selection of objects that will be outlined |
| selectionLayer | Number | 10 | The selection layer |
| blendFunction | BlendFunction | BlendFunction.SCREEN | The blend function of this effect. |
| patternTexture | Texture | null | A pattern texture. |
| patternScale | Number | 1.0 | The pattern texture scale. |
| edgeStrength | Number | 1.0 | The edge strength. |
| pulseSpeed | Number | 0 | The pulse speed. A value of zero disables the pulse effect. |
| visibleEdgeColor | Number | 0xffffff | The color of visible edges. |
| hiddenEdgeColor | Number | 0x22090a | The color of hidden edges. |
| multisampling | Number | 0 | The number of samples used for multisample antialiasing. Requires WebGL 2. |
| resolutionScale | Number | 0.5 | The render resolution scale. |
| resolutionX | Number | Resolution.AUTO_SIZE | The render width. |
| resolutionY | Number | Resolution.AUTO_SIZE | The render height. |
| kernelSize | KernelSize | KernelSize.VERY_SMALL | The blur kernel size. |
| blur | Boolean | false | Whether the outline should be blurred. |
| xRay | Boolean | true | Whether occluded parts of selected objects should be visible. |
| Name | Type | Default | Description |
| ---------------- | -------------------- | ----------------------- | ------------------------------------------------------------------------------------- |
| selection | Objects | | Selection of objects that will be outlined |
| selectionLayer | Number | 10 | The selection layer |
| blendFunction | BlendFunction | BlendFunction.SCREEN | The blend function of this effect. |
| patternTexture | THREE.Texture | null | A pattern texture. |
| patternScale | Number | 1.0 | The pattern texture scale. |
| edgeStrength | Number | 1.0 | The edge strength. |
| pulseSpeed | Number | 0 | The pulse speed. A value of zero disables the pulse effect. |
| visibleEdgeColor | ColorRepresentation | 0xffffff | The color of visible edges. |
| hiddenEdgeColor | ColorRepresentation | 0x22090a | The color of hidden edges. |
| multisampling | Number | 0 | The number of samples used for multisample antialiasing. Requires WebGL 2. |
| resolutionScale | Number | 0.5 | The render resolution scale. Applied at construction only. |
| resolutionX | Number | Resolution.AUTO_SIZE | The render width. Applied at construction only. |
| resolutionY | Number | Resolution.AUTO_SIZE | The render height. Applied at construction only. |
| width | Number | Resizer.AUTO_SIZE | The render width. Unlike `resolutionX`, updates live without reconstructing the effect. |
| height | Number | Resizer.AUTO_SIZE | The render height. Unlike `resolutionY`, updates live without reconstructing the effect. |
| kernelSize | KernelSize | KernelSize.VERY_SMALL | The blur kernel size. |
| blur | Boolean | false | Whether the outline should be blurred. |
| xRay | Boolean | true | Whether occluded parts of selected objects should be visible. |
6 changes: 3 additions & 3 deletions docs/effects/shockwave.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ return (

## Props

| Name | Type | Default | Description |
| --------- | ------- | -------- | ------------------------------------- |
| position | Vector3 | (0,0,0) | The world position of the shockwave. |
| Name | Type | Default | Description |
| --------- | ----------------------------- | -------- | ------------------------------------- |
| position | Vector3 \| [x, y, z] \| Number | (0,0,0) | The world position of the shockwave. |
| speed | Number | 2.0 | The animation speed. |
| maxRadius | Number | 1.0 | The extent of the shockwave. |
| waveSize | Number | 0.2 | The wave size. |
Expand Down
Loading
Loading