Render fewer pixels. Get a sharper image.
@pmndrs/upscaler is a temporal upscaler for the web. Render your scene at a fraction of
its resolution, and it rebuilds the full-resolution frame from the last several,
recovering detail that no single frame ever had and anti-aliasing it along the way. It's
the idea behind FSR 2/3, DLSS and XeSS, running in the browser on WebGPU.
What you do with the GPU time it saves is up to you: hold your frame rate on a laptop, or spend it on GI, reflections and heavier materials that wouldn't fit at native resolution.
pipeline.outputNode = upscaleScene(scene, camera);▶ See it live: showcases and 16 hands-on examples, each one a small, readable integration.
It started as a port of AMD's FidelityFX Super Resolution. The spatial EASU/RCAS shaders are faithful WGSL ports of AMD's originals, and the temporal path follows the FSR 2/3 architecture (credits). Then the web asked questions FSR never had to answer, and we measured our way past the port:
- Leaner than the source. We rebuilt source-faithful FSR 3.1.5 pass graphs and raced them against ours on the GPU. Where they cost 6–76% more for no visible gain, our fused passes stayed. Where the source's design earned its keep, as with the depth reconstruction that keeps history through camera motion, we adopted it in a leaner form. (Why we diverge)
- Still images that actually settle. A standing scene converges and stays put, instead of shimmering as each jitter phase re-snaps the history.
- HDR-safe sharpening. Bright highlights stay crisp, and the sharpening is capped so they never blow out into fireflies.
- Transparent canvases. Alpha is upscaled along with color, so the page shows through where it should.
- At home in a render pipeline. A native TSL node makes it one line in three's
RenderPipeline, and it composes with reduced-resolution SSGI, SSR and GTAO graphs. It also publishes its motion and disocclusion temporal guides so your own effects can reuse them instead of recomputing them. - Debuggable by design. Every stage has a debug view, there's an interactive bench, and the tuning decisions are backed by measurement scripts you can re-run.
Today it plugs into three.js WebGPURenderer (r186+). Underneath it's plain WebGPU:
hand-written WGSL compute passes dispatched on your renderer's GPUDevice, with no
three.js in the shaders. Bringing it to another WebGPU engine means writing an adapter,
not rewriting the upscaler; that's tracked in
#61. It needs a WebGPU-capable browser,
and there's no WebGL fallback.
npm install @pmndrs/upscaler threeWebGPU only: you need a WebGPU-capable browser (Chrome/Edge 113+) and three
r186+ (a peer dependency). r184/r185 still work but are deprecated. The TSL node
warns once and falls back to the pre-r186 render-pipeline hooks, and that fallback
will be removed. There is no WebGL fallback. See
Compatibility.
▶ Live demos: pmndrs.github.io/upscaler: 16 interactive examples, covering spatial vs temporal, the aliasing-torture scene, transparency and reactive masks, the composable and spatial-only TSL nodes, SSGI/SSR upscaled in one post graph, temporal guides, and transparent-canvas alpha.
The recommended integration is the TSL node. Make it the output of a
RenderPipeline, and it renders your scene at reduced resolution and upscales it
back, jitter and all:
import * as THREE from 'three/webgpu';
import { upscaleScene, QualityMode } from '@pmndrs/upscaler';
// The upscaler stays linear/HDR; presentation is the renderer's job.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.outputColorSpace = THREE.SRGBColorSpace;
scene.background = new THREE.Color(0x10141a); // or empty regions stay transparent (see Alpha)
const pipeline = new THREE.RenderPipeline(renderer);
pipeline.outputNode = upscaleScene(scene, camera, { quality: QualityMode.Quality });
renderer.setAnimationLoop(() => pipeline.render());That's the whole integration: no manual jitter, MRT or velocity wiring.
upscaleScene renders the scene in-graph as the upscaler's input, so the sub-pixel
jitter lands on it and you get real reconstruction rather than a smart blur.
| Integration | When |
|---|---|
upscaleScene(scene, camera) |
A scene, rendered through a RenderPipeline. |
upscale(color, depth, velocity, camera) |
A reduced-resolution effect graph (SSGI/SSR/GTAO) in the same pipeline. |
upscaleSpatial(color) |
Only a color texture: single-frame FSR1, no motion data. |
UpscalePass |
A plain render loop with no post-processing graph. |
Upscaler |
Your own render-target loop, split frames, or inputs the others don't expose. |
temporalGuides() / upscaler.guides |
Other temporal effects sharing the upscaler's motion, depth and disocclusion. |
Getting started walks through each one. Inputs and contracts specifies what color, depth, velocity, jitter, reactive masks and exposure must contain. Most integration bugs are contract bugs.
Every path upscales RGBA: the input's alpha is filtered and accumulated, not
replaced with 1.0, so a transparent canvas stays transparent through the upscale.
This is the same convention as three's FSR1Node. An opaque input comes out with
alpha exactly 1.
Coming from 0.2: earlier versions wrote alpha 1.0 everywhere. three's
WebGPURendererdefaults toalpha: trueand clears to alpha 0, so a scene with noscene.background(and no opaque clear color) presented throughUpscalePassor the TSL nodes now shows the page through its empty regions, exactly as three does without the upscaler. For the old look, setscene.backgroundorrenderer.setClearColor(color, 1), or create the renderer withalpha: false. Likewise, a post graph that scales the upscaledvec4by a scalar, such asupscale(...).mul(vignette), now scales alpha too. Multiply byvec4(vec3(vignette), 1)to darken color only. Details: Alpha.
- Getting started: the integration surfaces and runtime settings
- Inputs and contracts: color, depth, velocity, jitter, reactive, exposure, output, alpha
- Temporal guides: the published motion/disocclusion bundle and the split frame
- Debugging: debug views, symptoms, real-GPU verification
- Compatibility: three.js versions, WebGPU, limitations
- Architecture: the pass graph and internals, for contributors
- Design rationale vs FSR 3.1.5, and the full index
The pipeline is feature-complete and GPU-verified. It covers the spatial (FSR1)
and temporal paths, RGBA (alpha) passthrough, luminance-stability locks, auto-exposure
(plus external and host pre-exposure inputs), multi-scale shading-change detection,
reactive masks (explicit and auto-generated), RCAS with opt-in denoise, the imperative
UpscalePass, the composable TSL nodes, and the raw and linked-TSL temporal-guides
surfaces. A benchmarking program A/B-compared this implementation against
source-style FSR 3.1.5 pass graphs on the GPU. The measurements and the reasoning for
each divergence are in PARITY.md.
Deliberately not planned:
- Frame generation (the other half of "FSR3"). It needs swapchain-level frame pacing, which browsers don't expose.
- MSAA input. FSR's temporal path is the anti-aliaser; a multisampled input is redundant and can't bind to the compute passes.
- Perf-only micro-optimizations (
textureGathertap packing, f16 arithmetic, bind-group caching). Each adds correctness risk to a core path with no image-quality gain, so they wait until performance is an actual bottleneck on real content.
Bug reports, examples and PRs are welcome. Contributing covers the dev loop, the bench, testing on a real GPU, and how releases are cut.
- FidelityFX Super Resolution 2/3 (GPUOpen) — algorithm & source (MIT)
ffx_fsr1.h— EASU/RCAS reference the WGSL ports follow- "Filmic SMAA / temporal reprojection" (Jimenez, SIGGRAPH 2016) — Catmull-Rom history filtering
- "Temporal Reprojection Anti-Aliasing" (Playdead) — variance clipping
- three.js
TRAANode— jitter/velocity integration pattern this package mirrors
Built by Dennis Smolek. Maintained under the Poimandres collective.
Based on AMD's FidelityFX Super Resolution — this package ports its MIT-licensed EASU/RCAS shaders and follows the FSR2/3 temporal-upscaling architecture. "FSR" and "FidelityFX" are AMD's; this is an independent, unaffiliated implementation for three.js.
MIT — see LICENSE. The EASU/RCAS shaders derive from AMD's MIT-licensed FidelityFX Super Resolution; AMD's copyright notice is included in the license file.