pmndrs/postprocessing: a three.js post-processing library built around merged effect passes
A post processing library for three.js.
At a glance
- What is it?
- The pmndrs/postprocessing library replaces three.js pass chaining with a single EffectPass that merges effects into fewer render operations. Here is how the composer, the sRGB frame buffer trade-off and the HalfFloatType option fit together, and who should stay with three.js's own post-processing classes.
- Who is it for?
- Adopt pmndrs/postprocessing if you are building a three.js scene that needs several stacked fullscreen effects and you want them merged into one EffectPass rather than chained as separate render passes. Skip it if you need a post-processing stack for Unity, Unreal or a non-WebGL pipeline, since this library is tied to three.js and WebGLRenderer.
- Can I use it commercially?
- Yes. Zlib is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What pmndrs/postprocessing solves, and who it is for
three.js ships its own post-processing classes, and the usual pattern there is a chain: each effect is its own pass, each pass reads a render target and writes another. Stack six effects and you have six fullscreen render operations per frame. pmndrs/postprocessing is a separate library that keeps the same mental model (passes and effects) but changes the execution strategy. The README states that its EffectPass "automatically organizes and merges any given combination of effects", which the project presents as minimizing render operations and avoiding "the performance penalties of traditional pass chaining".
The audience is narrow and specific: JavaScript developers already rendering a scene with three.js who want bloom, depth of field, SSAO, tone mapping and similar fullscreen image manipulation on top of it. It is not a general image-processing toolkit, not a shader language, and not usable without three.js. The package.json lists three as a peer dependency, so the library is explicitly designed to sit alongside an existing three.js install rather than replace any part of it.
One detail worth noticing early: the README recommends creating the WebGLRenderer with antialias disabled, along with stencil and depth disabled and powerPreference set to high-performance. Antialiasing is then handled by an effect in the pipeline instead of by the context. That is a deliberate architectural choice, and it means adopting this library changes how you construct the renderer, not just what you add after rendering.
EffectComposer, RenderPass and EffectPass: the actual data flow
The pipeline has three moving parts. EffectComposer owns the render targets and drives the frame. RenderPass is conventionally the first pass: it clears the buffers and renders the scene, which is what gives the later effects something to work on. EffectPass takes a camera and one or more effects, and that is where the merging happens. The README's usage example adds a RenderPass and then a single EffectPass wrapping a BloomEffect, and calls composer.render() inside a requestAnimationFrame loop.
The merge is the whole point. Instead of one render target per effect, the EffectPass combines the effects you hand it, and the README notes that each effect can still choose its own blend function. So you get per-effect blending behaviour without paying for a separate fullscreen draw per effect.
There is a second optimization in the same section. All fullscreen render operations use a single triangle that fills the screen rather than a quad. The README's stated reason is that this harmonizes with modern GPU rasterization patterns and eliminates fragment calculations along the screen diagonal, which it says matters most for GPGPU passes and effects with complex fragment shaders. Both of these are design claims from the project's own documentation, not measurements I can reproduce here, but they are concrete enough to reason about: fewer draw calls and fewer shaded fragments per frame are the two levers being pulled.
The composer is also where frame buffer precision is configured, which is the next section's subject.
Installing it and rendering a first bloom pass
The library is published on npm as postprocessing, and three is a peer dependency, so both go in together. The README gives exactly one install command:
npm install three postprocessingAfter that, the renderer should be created with the attributes the README recommends for an optimal post-processing workflow. Note that antialias is off here; the pipeline handles that instead.
import { WebGLRenderer } from "three";
const renderer = new WebGLRenderer({
powerPreference: "high-performance",
antialias: false,
stencil: false,
depth: false
});With the renderer, scene and camera set up as usual in three.js, the composer takes over the render loop. The README's example adds a RenderPass first and an EffectPass carrying a BloomEffect second, then calls composer.render() every frame:
import { BloomEffect, EffectComposer, EffectPass, RenderPass } from "postprocessing";
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new EffectPass(camera, new BloomEffect()));
requestAnimationFrame(function render() {
requestAnimationFrame(render);
composer.render();
});What you should see is your scene rendered as before, with bloom applied on top. If the image looks wrong in the shadows rather than the highlights, the likely cause is frame buffer precision, covered below. The README also points readers at a demo, a StackBlitz sandbox and the generated documentation for the full effect list.
The sRGB frame buffer trade-off and when HalfFloatType is required
This is the part of the library most likely to bite you, and the README is unusually direct about it. Postprocessing stores intermediate results in UnsignedByteType sRGB frame buffers. The project describes this as a trade-off between hardware support, efficiency and quality, and explains why: linear results normally need at least 12 bits per color channel to avoid color degradation and banding. With low-precision sRGB buffers, colors clamp to the range 0.0 to 1.0 and the information loss shifts toward the darker part of the spectrum, which the README says produces noticeable banding in dark scenes.
The fix is to switch the composer to high-precision buffers, which the README calls the preferred option for HDR-like workflows on desktop devices:
import { HalfFloatType } from "three";
const composer = new EffectComposer(renderer, {
frameBufferType: HalfFloatType
});Read the phrasing carefully: desktop devices. The default is not an oversight, it is a compatibility decision, and moving to HalfFloatType is a decision you make per target. If your audience includes lower-end mobile hardware, you are choosing between banding in dark scenes and a buffer format that may not be available everywhere. The README does not offer a fallback strategy for that case.
Tone mapping interacts with the same setting. The README says renderer.toneMapping should be left at NoToneMapping and high-precision buffers enabled, otherwise colors are mapped into 0.0 to 1.0 at the start of the pipeline. Tone mapping should instead be a ToneMappingEffect at the end. There is one consequence the README calls out explicitly: because postprocessing operates on the full input image, tone mapping is applied to the clear color too, so a tone-mapped background will look different with and without postprocessing. The project's position is that the postprocessing result is the correct one.
Where the library is the wrong tool
The clearest limitation is scope. This is a three.js library. If your rendering stack is Unity, Unreal, a native Vulkan pipeline or a 2D canvas application, nothing here applies, and the effect names you may recognize (bloom, depth of field, SSAO) exist in those engines' own post-processing stacks with their own configuration. The shared vocabulary is not shared code.
The second limitation is precision, as described above. The default UnsignedByteType sRGB buffers clamp to 0.0 to 1.0, and the README itself says this causes banding in dark scenes. If your content is dark, or you are doing HDR-like work, the default path is not the one you want, and the recommended alternative is framed around desktop devices.
The third is that adoption changes your renderer setup. Antialias is turned off at the WebGLRenderer level and handled in the pipeline. If you have existing code that depends on context-level antialiasing, or a rendering setup you cannot easily restructure, the migration is more than adding an import.
Finally, the effect list is broad but bounded. Antialiasing, bloom, blur, color depth, color grading (including LUT), depth of field, glitch, god rays, pattern, pixelation, outline, shock wave, SSAO, texture and tone mapping are what the README enumerates. If you need an effect outside that set, the README points to the project wiki for writing custom effects and passes, which means writing shader code yourself rather than configuring something that already exists.
How it differs from three.js's own EffectComposer
The most relevant alternative is the post-processing support built into three.js itself. Both expose an EffectComposer, a RenderPass and an EffectPass, and the naming overlap is close enough that code can look interchangeable at a glance. The difference is in what happens when you add several effects.
In the three.js approach, the conventional pattern is one pass per effect, each writing to and reading from render targets. The pmndrs library's central claim is that its EffectPass merges any given combination of effects into fewer render operations, so stacking many effects does not multiply the cost the way pass chaining does. That is the architectural difference, and it is the reason to pick this library over the built-in path. Alongside it, the single-triangle fullscreen draw is a second, smaller optimization aimed at fragment-level cost.
The trade-off is dependency surface. Using three.js's own classes adds nothing to your dependency list. Using pmndrs/postprocessing adds a package that tracks three.js as a peer, and the library's own versioning is independent: the most recent releases in the repository are v6.39.5, v6.39.4 and v6.39.3. You are also accepting the library's opinions about renderer construction and frame buffer format, which the built-in path does not impose in the same way.
For a scene with one or two effects, the merge advantage is small and the built-in path is the simpler choice. The gap widens as the effect stack grows.
Maintenance, licence and upgrade cost
The repository is not archived, and the last push was on 2026-09-19. Release v6.39.5 is dated 2026-09-09, with v6.39.4 on 2026-07-27 and v6.39.3 on 2026-07-18 before it. The version numbers move in small increments, which is consistent with a library that is being maintained rather than one receiving occasional large rewrites.
For contributors rather than consumers, the toolchain is opinionated. package.json declares devEngines requiring pnpm in the range >=11 <12 and Node >=24, with onFail set to error, so a mismatched local environment fails rather than warns. The package is ESM-first ("type": "module") and ships both an ESM build and a CJS build with separate type declaration files for each, plus sideEffects set to false for tree shaking. The build script runs CSS, minified JS and type declaration steps in parallel.
Licensing is Zlib, which is a permissive licence, and the repository carries a LICENSE.md. Zlib is short and permissive in a way similar to MIT, but it is a distinct licence with its own attribution clause. If your organization has an approved-licence list, check that Zlib is on it before you ship; I am not in a position to give legal advice about your specific obligations, and the LICENSE.md file is the authoritative text.
Upgrade cost is mostly the peer dependency. Because three.js is a peer, a major three.js release can affect you independently of this library's own version, and the renderer construction and color space recommendations in the README are the areas most likely to need attention when either side moves.
Editorial conclusion
Adopt pmndrs/postprocessing if you are building a three.js scene that needs several stacked fullscreen effects and you want them merged into one EffectPass rather than chained as separate render passes. Skip it if you need a post-processing stack for Unity, Unreal or a non-WebGL pipeline, since this library is tied to three.js and WebGLRenderer. Before committing, verify three things in your own scene: whether UnsignedByteType sRGB frame buffers produce visible banding in your dark areas, whether HalfFloatType is available on your target devices, and that renderer.toneMapping is set to NoToneMapping with a ToneMappingEffect at the end of the pipeline instead.
Frequently asked questions
What is pmndrs/postprocessing?
It is a post-processing library for three.js, distributed on npm as postprocessing with three as a peer dependency. It introduces passes and effects, with an EffectComposer that manages and runs them.
How do I use postprocessing in a three.js scene?
Create an EffectComposer with your renderer, add a RenderPass with your scene and camera as the first pass, then add an EffectPass carrying your effects, and call composer.render() in your animation loop. The README recommends creating the WebGLRenderer with antialias, stencil and depth disabled.
What effects does pmndrs/postprocessing include?
The README lists antialiasing, bloom, blur, color depth, color grading (color average, sepia, brightness and contrast, hue and saturation, LUT), depth of field, glitch (chromatic aberration, noise), god rays, pattern (dot-screen, grid, scanline), pixelation, outline, shock wave, SSAO, texture and tone mapping.
Why does pmndrs/postprocessing show banding in dark scenes?
The library stores intermediate results in UnsignedByteType sRGB frame buffers by default, which clamp colors to the range 0.0 to 1.0 and shift information loss toward the darker spectrum. Passing frameBufferType: HalfFloatType to the EffectComposer is the README's recommended alternative for HDR-like workflows on desktop devices.
What is the difference between pmndrs/postprocessing and three.js's own post-processing?
The pmndrs library's EffectPass automatically organizes and merges any given combination of effects into fewer render operations, whereas the conventional three.js pattern chains one pass per effect. Using the built-in path adds no dependency, while this library adds a package that tracks three as a peer dependency.
What licence does pmndrs/postprocessing use?
The package.json declares the Zlib licence, and the repository includes a LICENSE.md file. Zlib is a permissive licence, but it is distinct from MIT, so check it against your organization's approved-licence list.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pmndrs-postprocessing)