Open-source project
gkjohnson/three-gpu-pathtracer avatar
gkjohnson/three-gpu-pathtracer

three-gpu-pathtracer converges one sample per frame and needs WebGPU to do it

Path tracing renderer and utilities for three.js built on top of three-mesh-bvh.

1,920 stars189 forksJavaScriptMIT

At a glance

What is it?
three-gpu-pathtracer is a path tracing renderer for three.js built on three-mesh-bvh and WebGPU compute shaders, where each animation frame adds a single sample and the image sharpens over time. Its constraint list is short and specific: WebGPU, two materials, and multiple importance sampling.
Who is it for?
Adopt three-gpu-pathtracer if you are already building with three.js on WebGPU and you need physically based renders with area lights, depth of field and HDR environments rather than a real-time rasterised approximation. Do not adopt it if any of your scene uses materials other than MeshStandardMaterial or MeshPhysicalMaterial, or if you need transparent backgrounds together with upscaling, since the project lists both as limitations.
Can I use it commercially?
Yes. MIT 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 11 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 October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

renderSample in a requestAnimationFrame loop is the whole model

The integration is short enough to read in full, and the shape of it explains the library. You construct a WebGPU renderer, initialise it, wrap it in a `WebGPUPathTracer`, hand it the scene and camera, and then call `renderSample()` once per animation frame:

js
import * as THREE from 'three/webgpu';
import { WebGPUPathTracer } from 'three-gpu-pathtracer/webgpu';

renderer = new THREE.WebGPURenderer();
renderer.toneMapping = THREE.ACESFilmicToneMapping;
await renderer.init();

pathTracer = new WebGPUPathTracer( renderer );
pathTracer.setScene( scene, camera );

animate();

function animate() {
  requestAnimationFrame( animate );
  pathTracer.renderSample();
}

That is a progressive path tracer, not a real-time one. One sample per frame, accumulated, so the image starts noisy and resolves. Nothing in the sample code resets the accumulation buffer, which is what makes it converge instead of flicker.

The detail that catches people is the import path. The renderer comes from `three/webgpu`, not from `three`, and the path tracer comes from `three-gpu-pathtracer/webgpu` rather than the package root. The package exports map confirms this: the root `.` entry points at `src/index.js`, the `./webgpu` subpath at `src/webgpu/index.js`, and there is an additional `./src/*` subpath that exposes internals directly.

`renderer.init()` is awaited before anything else. That is an asynchronous step the WebGPU renderer needs, and code that constructs a path tracer before it resolves will not have a device to dispatch compute shaders against.

Setting `renderer.toneMapping = THREE.ACESFilmicToneMapping` is part of the sample rather than an extra, since a path tracer accumulates raw radiance and the tone mapping step is what turns that into a displayable image.

The gotchas list is short, and each entry closes a door

There is a dedicated gotchas section, and it is the most useful part of the README for deciding whether this fits. It is five lines long:

The project requires WebGPU. That is not a preference; it is the first line.

SpotLights, DirectionalLights and PointLights are only supported with MIS, where MIS is multiple importance sampling. SpotLights, DirectionalLights, and PointLights are gated on MIS.

Only MeshStandardMaterial and MeshPhysicalMaterial are supported. Any other material in your scene is either ignored or wrong, and there is no fallback.

Emissive materials are supported but do not take advantage of MIS.

Upscaling does not currently work with a transparent background.

Read together, these describe a renderer with a narrow but coherent domain. It does physically based rendering of standard materials under a specific set of light types, on a device with WebGPU. The upside is that everything inside that domain is done properly. The downside is that any existing three.js scene you hoped to drop in needs auditing material by material.

The transparent background interaction is the subtlest of the five. If your page design depends on a path traced render compositing over a CSS background, and you also want the denoise-and-upscale path for performance, you cannot have both. Denoising and upscaling have their own example page in the repository, so the feature exists; the combination is what is excluded.

Blurring the environment map is a convergence trade, stated plainly

There is one tuning knob the README explains in enough detail to be worth quoting, because it is the kind of decision usually buried in a comment.

Using a pre-blurred environment map helps improve frame convergence time, at the cost of sharp environment reflections. If performance is a concern, multiple importance sampling can be disabled and a blurred environment map used instead. Those two suggestions are coupled: dropping MIS and blurring the environment are two faces of the same trade.

js
import { BlurredEnvMapGenerator } from 'three-gpu-pathtracer/webgpu';

const envMap = await new HDRLoader().loadAsync( envMapUrl );
const generator = new BlurredEnvMapGenerator( renderer );
const blurredEnvMap = await generator.generate( envMap, 0.35 );
generator.dispose();

scene.environment = blurredEnvMap;

The `0.35` argument is the blur amount, and it is the single parameter standing between a mirror-sharp reflection and a soft one. The generator is disposable, so the pattern is generate, dispose, assign, which means you can regenerate at different blur levels to compare without leaking GPU resources.

This is the right place to look for how the project thinks about performance. It is not hiding a quality setting. It is saying that every sample of a sharp environment map costs more, that you can pay for sharpness or you can pay for frames, and that the environment map is the cheapest place to buy frames back.

The HDR demo and the equirect demo in the example list cover the environment handling, and the interior scene with equirect rendering is specifically about getting an environment to light a room.

What the examples directory tells you about scope

The example folder is the best available map of what the renderer actually does, since the README links to hosted demos and the repository holds the source for each of them.

The named demos cover basic glTF setup, basic primitive geometry, physically based materials, an interior scene with equirect rendering, depth of field, an HDR image, area light support, spot light support, and denoising and upscaling. Two test scenes are worth calling out because they are for verification rather than for showing off: a material test orb, and model viewer fidelity scene comparisons, which is a comparison against a model viewer rendering.

The example files list goes further than the README does. Alongside the demo pages there is `furnace_test.html` and `furnace.js`, a furnace test being the standard way of isolating material response from scene lighting. There is `lkg.html`, a look, a lighter-weight variant. There is `fog.html`, which is notable because fog is not in the supported features list. There is `graphing.html`, `dynamicScene.html` and `extraScenarios.json`.

The tools category has one entry, an animation rendering demo, and the devDependencies explain how that is built. `mediabunny` for media encoding, `pixelmatch` and `pngjs` for image comparison, `puppeteer` for driving a browser, `oidn-web` for denoising, and `@monogrid/gainmap-js` plus `@pmndrs/upscaler` for the upscaling path. The presence of puppeteer alongside pixelmatch and pngjs says the repository runs image comparisons as part of its own checks rather than leaving visual regression to reviewers.

`@lookingglass/webxr` is also a devDependency, which points at the project viewing its own output in a headset at some point in its tooling.

Version 0.0.27 and what the peer dependency floor implies

The published version is 0.0.27, with 0.0.25 on 2026-09-28, 0.0.26 on 2026-09-29 and 0.0.27 on 2026-10-08, and the last push was on 2026-10-12. That is a fast cadence, and the version numbers are the giveaway that pre-1.0 discipline applies: a patch release in this project is not a compatibility promise, since 0.0.x signals that interfaces may change without the semver ceremony.

The peer dependencies are where an upgrade can actually break you. They are `three: >=0.185.0` and `three-mesh-bvh: >=0.9.15`, with a third entry truncated past `xat`. The devDependencies pin `three` at `^0.185.1` and `three-mesh-bvh` at `^0.9.15`, so the project tracks three.js releases closely rather than lagging behind them.

That coupling is inherent to the design. This is not a standalone renderer you drop next to your app; it depends on three.js types and on a specific BVH library version, and it uses `three/webgpu`, a build of three.js that only some bundler and browser combinations handle. A project pinned to an older three.js cannot use this.

The package is ESM first, with `"type": "module"`, a `module` entry at `src/index.js` and a `main` fallback at `build/index.umd.cjs`. The published `files` are `src/*` and `build/*`, so consumers get the untranspiled source, which is normal for a graphics library that ships raw shader strings. The `sideEffects` field marks `./src/webgpu/index.js` and the shims in `./src/webgpu/shims/*.js`, which tells a bundler not to tree-shake the WebGPU entry point or its compatibility shims away.

The licence is MIT, documentation is generated into a docs site from `src/webgpu/API.md` with jsdoc2md, and the repository carries an .eslintrc.json, CHANGELOG.md and CONTRIBUTING.md.

Where it sits against rasterised three.js and against a native path tracer

A path tracer samples paths through the scene and integrates over many samples, which is why reflections, refraction, global illumination and soft shadows fall out of the algorithm rather than being approximated in.

The practical consequence for a three.js developer is the loop you saw at the start. `renderSample()` once per frame is not real-time. It converges. For a product that needs 60 frames per second of interactive navigation this is the wrong tool entirely, and no amount of sample optimisation changes that.

The other comparison is against a native path tracer such as blender/cycles, which the README lists among its resources along with Raytracing in One Weekend, the PBR Book, a GLSL path tracer and the OpenPBR specification. The animation rendering demo is included among the project's examples.

That is a genuinely different position in the pipeline. Offline quality with an interactive authoring loop, at the cost of an API surface limited to two material types and a light set gated on MIS.

Editorial conclusion

Adopt three-gpu-pathtracer if you are already building with three.js on WebGPU and you need physically based renders with area lights, depth of field and HDR environments rather than a real-time rasterised approximation. Do not adopt it if any of your scene uses materials other than MeshStandardMaterial or MeshPhysicalMaterial, or if you need transparent backgrounds together with upscaling, since the project lists both as limitations. Verify first that your `three` version satisfies the peer requirement of `>=0.185.0` and that `three-mesh-bvh` is at `>=0.9.15`, then read the gotchas list before designing a scene, since the MIS-only support for spot, directional and point lights constrains your lighting setup.

Frequently asked questions

What does three-gpu-pathtracer require to run?

It requires WebGPU. The peer dependencies are `three: >=0.185.0` and `three-mesh-bvh: >=0.9.15`, and you import the renderer from `three/webgpu` and the path tracer from `three-gpu-pathtracer/webgpu`.

How do I render with three-gpu-pathtracer?

Create a WebGPURenderer, `await renderer.init()`, wrap it in `new WebGPUPathTracer( renderer )`, call `pathTracer.setScene( scene, camera )`, then call `pathTracer.renderSample()` inside a requestAnimationFrame loop. One sample per frame accumulates into a progressively resolved image.

Which materials and lights does three-gpu-pathtracer support?

Only MeshStandardMaterial and MeshPhysicalMaterial. SpotLights, DirectionalLights and PointLights are only supported with multiple importance sampling, and emissive materials are supported but do not take advantage of MIS.

How can I speed up convergence in three-gpu-pathtracer?

Use a pre-blurred environment map. Generate it with `BlurredEnvMapGenerator`, for example `generator.generate( envMap, 0.35 )`, which improves frame convergence at the cost of sharp environment reflections. The README also notes that disabling multiple importance sampling and using a blurred environment map is the alternative when performance matters.

Does upscaling work with a transparent background in three-gpu-pathtracer?

No. The gotchas list states that upscaling does not currently work with a transparent background. Denoising and upscaling are supported otherwise, with a dedicated example.

Official sources

  1. gkjohnson/three-gpu-pathtracer on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gkjohnson-three-gpu-pathtracer.svg)](https://hysenlabs.com/projects/gkjohnson-three-gpu-pathtracer)