Runs Open Image Denoise over a path traced image. Pass one to "WebGPUPathTracer.setDenoiser", or drive it directly with "denoise".
"initUNetFromURL" and the weights are passed in rather than imported so neither the library nor the network files become a dependency.
import { initUNetFromURL } from 'oidn-web';
pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );Weights come from the oidn-weights repository, where the "_small" and "_large" variants trade quality against download size and per tile cost.
import { ACESFilmicToneMapping, Color, EquirectangularReflectionMapping } from 'three/webgpu';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
import { createFloor } from './floor.js';
import { initUNetFromURL } from 'oidn-web';
import { WebGPUPathTracer, OIDNDenoiser, RANDOM_SOBOL } from 'three-gpu-pathtracer/webgpu';
// scene, camera and renderer are initialized here
const URL = 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/nasa-m2020/Perseverance.glb';
const ENV_URL = 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/hdri/chinese_garden_1k.hdr';
const WEIGHTS_URL = 'https://raw.githubusercontent.com/gkjohnson/three-gpu-pathtracer/v0.0.25/example/src/denoise/rt_hdr_alb_nrm.tza';
const { scene: model } = await new GLTFLoader()
.setDRACOLoader( new DRACOLoader() )
.loadAsync( URL );
const environment = await new HDRLoader().loadAsync( ENV_URL );
environment.mapping = EquirectangularReflectionMapping;
scene.environment = environment;
scene.background = new Color( 0xeeeeee );
renderer.toneMapping = ACESFilmicToneMapping;
model.position.z = - 0.32;
scene.add( model, createFloor() );
camera.position.set( - 3.2, 2.2, 4.6 );
controls.target.set( 0, 0.9, 0 );
controls.update();
const pathTracer = new WebGPUPathTracer( renderer );
// mobile GPUs are slower: fewer paths per frame keep frames fast, and fewer pixels converge sooner
if ( matchMedia( '(pointer: coarse)' ).matches ) {
pathTracer.frameBudget = 50000;
renderer.setPixelRatio( window.devicePixelRatio / 2 );
}
pathTracer.maxSamples = 6;
pathTracer.setRandom( RANDOM_SOBOL );
pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl: WEIGHTS_URL } ) );
pathTracer.setScene( scene, camera );
controls.addEventListener( 'change', () => pathTracer.updateCamera() );
window.addEventListener( 'resize', () => pathTracer.updateCamera() );
renderer.setAnimationLoop( () => pathTracer.renderSample() );Properties
texture: ExternalTexture | null
The denoised result, or null until the first tile has been produced.
complete: boolean
Whether a pass has finished. Stays true until reset.
running: boolean
Whether a pass is running. The work is spread over several frames.
Methods
new OIDNDenoiser(
options: {
initUNetFromURL: function,
// Weights for the guided model.
auxWeightsUrl: string,
// Weights for the color only model, needed only when `useAuxiliaryBuffers` is `false`.
colorWeightsUrl?: string,
useAuxiliaryBuffers?: boolean,
maxTileSize?: number | null,
// `false` pins every tile to `maxTileSize`. An object tunes the adaptive sizing.
dynamicTile?: DynamicTileSetting | null
}
)Every field below can also be assigned after construction.
init( renderer: WebGPURenderer ): voidsetScene( scene: Scene, camera: Camera ): voidupdate(
// The path traced result, in linear HDR.
target: Texture
): Texture | nullRenders the auxiliary buffers and starts a pass. Safe to call every frame.
async denoise(
// The path traced result, in linear HDR.
color: Texture,
albedo: Texture | null = null,
normal: Texture | null = null
): voidRuns a pass unless one is already running or finished. The optional albedo and normal buffers guide the filter and select the guided model. Both hold [0,1] values, with normals mapped so a flat normal is (0.5, 0.5, 1).
reset(): voidDrops any running pass and the last result. Call it whenever the image changes, such as when the camera moves.