three-gpu-pathtracer icon indicating copy to clipboard operation
three-gpu-pathtracer copied to clipboard

Add OIDN Denoiser.

Open DennisSmolek opened this issue 7 months ago • 23 comments

This PR Adds the OIDN Denoiser (Denoiser) into the core three-gpu-pathtracer.

It works, and looks decent but there are clear issues to resolve.

API Added

  • maxSamples Pathtracer now stops when reaching this number. Also is the number the system denoises at
  • enableDenoiser If we are using the denoiser (NOTE, the denosier actually initializes regardless of this for webGL compat)
  • weightsUrl Lets you bypass the jsDelivr URL for your own location of the TZAs. Also how you could pass your own weights in general
  • denoiserQuality Fast by default, Balanced is better. "high" is really heavy and not supported in the Denoiser yet
  • useAux Whether or not you want to use just the pathtraced input (fast, but bad) or with the Aux
  • renderAux If you want to view the aux inputs set this to albedo or normal null gets you the denoised output
  • cleanAux If you are 100% the aux inputs are clean, ANY noise will be projected into the output (NOTE, upstream fix may be needed)
  • externalAux If the user is supplying their own Aux Textures. disables automatic generation
  • denoiserDebugging Sets the denoiser to log more things including an output report.

State API Properties (read-only)

  • isDenoising If the denoiser is running
  • isDenoised if the current output is the denoised output

Methods

  • generateAux() runs the albedo/normals pass on the current scene. returns { albedo: THREE.Texture, normal: THREE.Texture }
  • setAuxTextures(albedoTexture, normalTexture) if the user wants to set the aux themselves, like in a pipeline or has a deferred setup. Deactiviates internal automatic creation (this.externalAux = true)
  • denoiseSample(inputTexture, albedoTexture, normalTexture) loads automatically or can be overridden letting you send to the denoiser directly to the DenoiserManager Not sure about exposing this, called internally by renderSample

Changes to Core

  • Not much was done to the core itself. The main thing is the addition of another fullScreenQuad pass that blends the pathtracer output with the denoised (or aux if you're debugging) outputs.
  • The second significant change is the addition of maxSamples and it's stopping the pathtracer when reached.
  • There were other changes to better support the denoiser, but nothing breaking.

One change was added to the BlendingMaterial to allow a conversion to happen to texture2. While we resolve the colorspace issues it may be useful.

Additions

  • AlbedoNormalPass: Generates Albedo & Normal textures based on the current scene
  • DenoiserManager: Holds all things related to the denoiser and interfaces with the Denoiser class directly

Flow With Denoising

Assuming Denoiser is enabled and all options default/set

  1. Pathtracer runs until maxSamples is hit
  2. Denoiser process started. Block future denoiseSample calls until finished
  3. Albedo and Normal Textures Generated (only when needed by the denoiser, not every frame)
  4. RawPathtracerTexture (pathtracer.target.texture), AlbedoTexture, and NormalTexture sent to the DenoiserManager.
  5. If the renderer size has changed, regenerate renderTargets and set Denoiser sizes.
  6. Render the RawPathtracer texture to a Quad with same tonemapping as default PT so resulting texture matches and within [0, 1]
  7. Extract raw WebGLTextures from internals of input textures (THREE.Textures)
  8. Set those Inputs on the denoiser
  9. Execute the denoiser, return denoisedWebGLTexture
  10. If not already created, create and load a ThreeJS texture that is properly initialized and setup within threejs internals outputTexture ( one of the many steps to get WebGL/Three to play nice)
  11. Merge the denoisedWebGLTexture with the outputTexture
  12. denoisedTexture is held in the class ready to be rendered
  13. denoiser marked finished, results say it is now denoised
  14. In the next renderSample call now that isDenoised is true, render using DenoiserManager.renderOutput()
  15. Quad material now set to BlendingMaterial
  16. Blend between previous PT Output (results of step 6) and the denoisedTexture (Note: if RenderAux is passed, will render the aux texture provided so you can see either the albedo or normal textures visually. Not sure the normals render how you'd expect, as they should be outputting in [-1, 1])

If reset is called it all drops back to normal, hides the outputs etc and starts over.

Known Issues

1. ColorSpaces/tonemapping/conversion: Something is clearly not setup right through the pipeline regarding colors. The denoiser DOES NOT CARE what colorspace you input. Whatever you input, it will output. Color and Albedo inputs should be in the same colorspace, and normals are expected to be in linear. The output looks correct but dark. converting makes it look flat. This will take tweaking and someone smarter than me with regards to color to follow the renderTargets (all setup with THREE.SRGBColorSpace and what we should convert/adjust

2. Normal Generation: I have a script setup to use worldspace or viewspace normals and output to [-1, 1]. I don't know if colorSpace/RT's might be effecting this (I don't think it matters). Also, I have added code to accept normalMaps on meshes so they can be used instead of the raw surface. I tried converting the NormalMaps to worldspace with tragic results. For the moment If an object has a map I use it.

Something very important to point out about normals and the denoiser. The denoiser does not actually use them for normal values or any kind of light calculations whatsoever. They can be in world/local space. The normal maps just help define edges and breaks in materials.

3. Albedo Generation: In The OIDN Docs they go into detail about the albedos. In general, most matte surfaces can use just the basic color output or textures. This isn't true for highly reflective surfaces (like the floor). Here albedo wants something different, you can read the docs and it says something to the effect that the reflected surface should have the albedo of whatever they are reflecting, or a full 1 value. It gets even weirder for reflections.

4. Edges with floor and background. Looking at original OIDN examples they do not expect black backgrounds or hard edges where soft edges blend with the original background. The normals/albedo read those as hard crisp edges or flat planes. So the gradient background gets flattened into a weird flat gradient and the floor has sharp edges. So with threejs backgrounds or envMaps we should generate something for both of these passes and include any floor/horizons.

The only one of these issues I see as a blocker is the colorspaces. Generating albedos and normals etc we can work on for a while and it would be fine to release improvements as updates. But gotta get closer on the color outputs IMO.

Other Changes

I updated the plain example (index) to include denoiser props and some other slight adjustments. I added a rawOutput canvas to the html which is very easy to setup when debugging to see exactly what the denoiser is outputting and confirm your inputs/outputs.

Notes

I tried commenting a lot to explain what is happening, and I realize my commits are terribly labeled. I was focusing on getting this added as the main thing and not making a lot of gradual changes.

DennisSmolek avatar Jul 24 '24 05:07 DennisSmolek