Skip to content
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 48 additions & 24 deletions examples/webgpu_postprocessing_ao.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
<script type="module">

import * as THREE from 'three/webgpu';
import { pass, mrt, output, normalView, velocity } from 'three/tsl';
import { pass, mrt, output, metalness, normalView, blendColor } from 'three/tsl';
import { ao } from 'three/addons/tsl/display/GTAONode.js';
import { traa } from 'three/addons/tsl/display/TRAANode.js';
import { ssr } from 'three/addons/tsl/display/SSRNode.js';
import { smaa } from 'three/addons/tsl/display/SMAANode.js';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
Expand All @@ -42,16 +43,14 @@

let camera, scene, renderer, postProcessing, controls, stats;

let aoPass, traaPass, blendPassAO, scenePassColor;
let aoPass, ssrPass, blendPassAO;

const params = {
distanceExponent: 1,
distanceFallOff: 1,
radius: 0.25,
scale: 1,
thickness: 1,
denoised: false,
enabled: true,
denoiseRadius: 5,
lumaPhi: 5,
depthPhi: 5,
Expand All @@ -71,6 +70,8 @@
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
renderer.toneMapping = THREE.NeutralToneMapping;
renderer.toneMappingExposure = 3.0;
document.body.appendChild( renderer.domElement );

stats = new Stats();
Expand All @@ -91,10 +92,7 @@
controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( 0, 0.5, - 1 );
controls.update();
controls.enablePan = false;
controls.enableDamping = true;
controls.minDistance = 2;
controls.maxDistance = 8;

//

Expand All @@ -104,25 +102,25 @@
scenePass.setMRT( mrt( {
output: output,
normal: normalView,
velocity: velocity
metalness: metalness
} ) );

scenePassColor = scenePass.getTextureNode( 'output' );
const scenePassNormal = scenePass.getTextureNode( 'normal' );
const scenePassColor = scenePass.getTextureNode( 'output' );
const scenePassDepth = scenePass.getTextureNode( 'depth' );
const scenePassVelocity = scenePass.getTextureNode( 'velocity' );
const scenePassNormal = scenePass.getTextureNode( 'normal' );
const scenePassMetalness = scenePass.getTextureNode( 'metalness' );

// ao

aoPass = ao( scenePassDepth, scenePassNormal, camera );
aoPass.resolutionScale = 0.5; // running AO in half resolution is often sufficient
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would advice to revert this change as well. Half resolution AO is quite common and the performance lost running full resolution is usually not worth the quality gain.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is at 0.5:

Screen.Recording.2025-07-31.at.6.05.47.PM.mov

This is at 1.0:

Screen.Recording.2025-07-31.at.6.07.51.PM.mov

Maybe 0.25 with denoising could be better?

However, to me it's weird that the plant is producing a shadow like that in the window behind it.
The plant should be receiving light from the window and now casting a shadow in the window.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A TAA performs some sort of denoising so I found a separate denoise pass is not necessarily required.

Also not that the way we apply AO at the moment means we attenuate the entire scene. A more physically correct approach would compute AO before the beauty pass and then just attenuate the indirect light. This requires a more complex setup though.

Besides, as long as transparent object write into the depth buffer, they are affected by the SSAO computation. The given window is especially challenging since although light travels through it, it can receives shadows because of its dull/milky surface. I guess the issue could be mitigate somewhat with a directional light shining through the window but that would require AO only attenuates the indirect light.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see...

Another issue I noticed with TRAA is that it requires velocity and ...

When I tried to add metalness to mrt() I ran into this:

Screenshot 2025-07-31 at 7 40 31 PM

Copy link
Collaborator

@Mugen87 Mugen87 Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we have to start pack things a bit^^. We end up with too many too large color attachments.

blendPassAO = aoPass.getTextureNode().mul( scenePassColor );

// ssr

// traa
ssrPass = ssr( scenePassColor, scenePassDepth, scenePassNormal, scenePassMetalness, camera );
ssrPass.resolutionScale = 1.0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 is the default now so this line can be removed.


traaPass = traa( blendPassAO, scenePassDepth, scenePassVelocity, camera );

postProcessing.outputNode = traaPass;
postProcessing.outputNode = smaa( blendColor( blendPassAO, ssrPass ) );

//

Expand All @@ -139,19 +137,45 @@
model.position.set( 0, 1, 0 );
scene.add( model );

// Fix scene

const duvet = model.getObjectByName( 'Bedroom_Duvet_0' );
duvet.material.metalness = 0;

const carpet = model.getObjectByName( 'Bedroom_Carpet_0' );
carpet.material.metalness = 0;

const windows = model.getObjectByName( 'Bedroom_Windows_0' );
windows.material.emissive = new THREE.Color( 0xffffff );
windows.material.emissiveIntensity = 1.0;

model.traverse( ( o ) => {

// Transparent objects (e.g. loaded via GLTFLoader) might have "depthWrite" set to "false".
// This is wanted when rendering the beauty pass however it produces wrong results when computing
// AO since depth and normal data are out of sync. Computing normals from depth by not using MRT
// can mitigate the issue although the depth information (and thus the normals) are not correct in
// first place. Besides, normal estimation is computationally more expensive than just sampling a
// normal texture. So depending on your scene, consider to enable "depthWrite" for all transparent objects.
if ( o.material ) {

if ( o.material ) o.material.depthWrite = true;
if ( o.material.map ) o.material.map.anisotropy = 16;
if ( o.material.roughnessMap ) o.material.roughnessMap.anisotropy = 16;

// Remove emissive from all materials

o.material.emissive = new THREE.Color( 0x000000 );
o.material.emissiveMap = null;

// Transparent objects (e.g. loaded via GLTFLoader) might have "depthWrite" set to "false".
// This is wanted when rendering the beauty pass however it produces wrong results when computing
// AO since depth and normal data are out of sync. Computing normals from depth by not using MRT
// can mitigate the issue although the depth information (and thus the normals) are not correct in
// first place. Besides, normal estimation is computationally more expensive than just sampling a
// normal texture. So depending on your scene, consider to enable "depthWrite" for all transparent objects.

o.material.depthWrite = true;

}

} );

//

window.addEventListener( 'resize', onWindowResize );

//
Expand Down