Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGPURenderer: Apply fog before tonemapping and encoding #27850

Merged
merged 13 commits into from
Mar 13, 2024
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@
"webgpu_cubemap_dynamic",
"webgpu_cubemap_mix",
"webgpu_custom_fog",
"webgpu_custom_fog_background",
"webgpu_depth_texture",
"webgpu_equirectangular",
"webgpu_instance_mesh",
Expand Down
1 change: 1 addition & 0 deletions examples/jsm/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTexture
export { default as InstanceNode, instance } from './accessors/InstanceNode.js';
export { default as MaterialNode, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialSpecularStrength, materialReflectivity, materialRoughness, materialMetalness, materialNormal, materialClearcoat, materialClearcoatRoughness, materialClearcoatNormal, materialRotation, materialSheen, materialSheenRoughness, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialLineScale, materialLineDashSize, materialLineGapSize, materialLineWidth, materialLineDashOffset, materialPointWidth } from './accessors/MaterialNode.js';
export { default as MaterialReferenceNode, materialReference } from './accessors/MaterialReferenceNode.js';
export { default as RendererReferenceNode, rendererReference } from './accessors/RendererReferenceNode.js';
export { default as MorphNode, morphReference } from './accessors/MorphNode.js';
export { default as TextureBicubicNode, textureBicubic } from './accessors/TextureBicubicNode.js';
export { default as ModelNode, modelDirection, modelViewMatrix, modelNormalMatrix, modelWorldMatrix, modelPosition, modelViewPosition, modelScale } from './accessors/ModelNode.js';
Expand Down
29 changes: 29 additions & 0 deletions examples/jsm/nodes/accessors/RendererReferenceNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ReferenceNode from './ReferenceNode.js';
import { addNodeClass } from '../core/Node.js';
import { nodeObject } from '../shadernode/ShaderNode.js';

class RendererReferenceNode extends ReferenceNode {

constructor( property, inputType, renderer = null ) {

super( property, inputType, renderer );

this.renderer = renderer;

}

setReference( state ) {

this.reference = this.renderer !== null ? this.renderer : state.renderer;

return this.reference;

}

}

export default RendererReferenceNode;

export const rendererReference = ( name, type, renderer ) => nodeObject( new RendererReferenceNode( name, type, renderer ) );

addNodeClass( 'RendererReferenceNode', RendererReferenceNode );
18 changes: 17 additions & 1 deletion examples/jsm/nodes/display/PassNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class PassNode extends TempNode {
this._depthTextureNode = nodeObject( new PassTextureNode( this, depthTexture ) );

this._depthNode = null;
this._viewZNode = null;
this._cameraNear = uniform( 0 );
this._cameraFar = uniform( 0 );

Expand All @@ -91,14 +92,29 @@ class PassNode extends TempNode {

}

getViewZNode() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can do these just as getters? I.e. something like get viewZ(), get depth() instead of getViewZNode(), getDepthNode()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think it's a good idea move to getters, I just will add *Node suffix.


if ( this._viewZNode === null ) {

const cameraNear = this._cameraNear;
const cameraFar = this._cameraFar;

this._viewZNode = perspectiveDepthToViewZ( this._depthTextureNode, cameraNear, cameraFar );

}

return this._viewZNode;

}

getDepthNode() {

if ( this._depthNode === null ) {

const cameraNear = this._cameraNear;
const cameraFar = this._cameraFar;

this._depthNode = viewZToOrthographicDepth( perspectiveDepthToViewZ( this._depthTextureNode, cameraNear, cameraFar ), cameraNear, cameraFar );
this._depthNode = viewZToOrthographicDepth( this.getViewZNode(), cameraNear, cameraFar );

}

Expand Down
12 changes: 8 additions & 4 deletions examples/jsm/nodes/display/ToneMappingNode.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import TempNode from '../core/TempNode.js';
import { addNodeClass } from '../core/Node.js';
import { tslFn, nodeObject, float, mat3, vec3 } from '../shadernode/ShaderNode.js';

import { NoToneMapping, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping } from 'three';
import { addNodeElement, tslFn, nodeObject, float, mat3, vec3 } from '../shadernode/ShaderNode.js';
import { rendererReference } from '../accessors/RendererReferenceNode.js';
import { clamp, log2, max, pow } from '../math/MathNode.js';
import { mul } from '../math/OperatorNode.js';

import { NoToneMapping, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping } from 'three';

// exposure only
const LinearToneMappingNode = tslFn( ( { color, exposure } ) => {

Expand Down Expand Up @@ -127,7 +128,7 @@ const toneMappingLib = {

class ToneMappingNode extends TempNode {

constructor( toneMapping = NoToneMapping, exposureNode = float( 1 ), colorNode = null ) {
constructor( toneMapping = NoToneMapping, exposureNode = toneMappingExposure, colorNode = null ) {

super( 'vec3' );

Expand Down Expand Up @@ -180,5 +181,8 @@ class ToneMappingNode extends TempNode {
export default ToneMappingNode;

export const toneMapping = ( mapping, exposure, color ) => nodeObject( new ToneMappingNode( mapping, nodeObject( exposure ), nodeObject( color ) ) );
export const toneMappingExposure = rendererReference( 'toneMappingExposure', 'float' );

addNodeElement( 'toneMapping', ( color, mapping, exposure ) => toneMapping( mapping, exposure, color ) );
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this may be quite confusing...


addNodeClass( 'ToneMappingNode', ToneMappingNode );
9 changes: 4 additions & 5 deletions examples/jsm/nodes/fog/FogExp2Node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import FogNode from './FogNode.js';
import { positionView } from '../accessors/PositionNode.js';
import { addNodeClass } from '../core/Node.js';
import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';

Expand All @@ -15,12 +14,12 @@ class FogExp2Node extends FogNode {

}

setup() {
setup( builder ) {

const depthNode = positionView.z.negate();
const densityNode = this.densityNode;
const viewZ = this.getViewZNode( builder );
const density = this.densityNode;

return densityNode.mul( densityNode, depthNode, depthNode ).negate().exp().oneMinus();
return density.mul( density, viewZ, viewZ ).negate().exp().oneMinus();

}

Expand Down
6 changes: 3 additions & 3 deletions examples/jsm/nodes/fog/FogNode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Node, { addNodeClass } from '../core/Node.js';
import { mix } from '../math/MathNode.js';
import { positionView } from '../accessors/PositionNode.js';
import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';

class FogNode extends Node {
Expand All @@ -15,9 +15,9 @@ class FogNode extends Node {

}

mixAssign( outputNode ) {
getViewZNode( builder ) {

return mix( outputNode, this.colorNode, this );
return ( builder.context.viewZ || positionView.z ).negate();

}

Expand Down
7 changes: 4 additions & 3 deletions examples/jsm/nodes/fog/FogRangeNode.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import FogNode from './FogNode.js';
import { smoothstep } from '../math/MathNode.js';
import { positionView } from '../accessors/PositionNode.js';
import { addNodeClass } from '../core/Node.js';
import { addNodeElement, nodeProxy } from '../shadernode/ShaderNode.js';

Expand All @@ -17,9 +16,11 @@ class FogRangeNode extends FogNode {

}

setup() {
setup( builder ) {

return smoothstep( this.nearNode, this.farNode, positionView.z.negate() );
const viewZ = this.getViewZNode( builder );

return smoothstep( this.nearNode, this.farNode, viewZ );

}

Expand Down
16 changes: 8 additions & 8 deletions examples/jsm/nodes/materials/NodeMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,23 +380,23 @@ class NodeMaterial extends ShaderMaterial {

const renderer = builder.renderer;

// TONE MAPPING
// FOG

const toneMappingNode = builder.toneMappingNode;
if ( this.fog === true ) {

if ( this.toneMapped === true && toneMappingNode ) {
const fogNode = builder.fogNode;

outputNode = vec4( toneMappingNode.context( { color: outputNode.rgb } ), outputNode.a );
if ( fogNode ) outputNode = vec4( fogNode.mix( outputNode.rgb, fogNode.colorNode ), outputNode.a );

}

// FOG
// TONE MAPPING

if ( this.fog === true ) {
const toneMappingNode = builder.toneMappingNode;

const fogNode = builder.fogNode;
if ( this.toneMapped === true && toneMappingNode ) {

if ( fogNode ) outputNode = vec4( fogNode.mixAssign( outputNode.rgb ), outputNode.a );
outputNode = vec4( toneMappingNode.context( { color: outputNode.rgb } ), outputNode.a );

}

Expand Down
12 changes: 10 additions & 2 deletions examples/jsm/renderers/common/PostProcessing.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@ class PostProcessing {

}

async render() {
renderAsync() {

quadMesh.material.fragmentNode = this.outputNode;

await quadMesh.render( this.renderer );
return quadMesh.renderAsync( this.renderer );

}

render() {

quadMesh.material.fragmentNode = this.outputNode;

quadMesh.render( this.renderer );

}

Expand Down
2 changes: 1 addition & 1 deletion examples/jsm/renderers/common/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class Nodes extends DataMap {

if ( rendererData.toneMapping !== rendererToneMapping ) {

const rendererToneMappingNode = rendererData.rendererToneMappingNode || toneMapping( rendererToneMapping, reference( 'toneMappingExposure', 'float', renderer ) );
const rendererToneMappingNode = rendererData.rendererToneMappingNode || toneMapping( rendererToneMapping );
rendererToneMappingNode.toneMapping = rendererToneMapping;

rendererData.rendererToneMappingNode = rendererToneMappingNode;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 150 additions & 0 deletions examples/webgpu_custom_fog_background.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgpu - custom fog background</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>

#info {
background-color: #0066ff;
}

</style>
</head>
<body>

<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgpu custom fog background<br />
Battle Damaged Sci-fi Helmet by
<a href="https://sketchfab.com/theblueturtle_" target="_blank" rel="noopener">theblueturtle_</a><br />
<a href="https://hdrihaven.com/hdri/?h=royal_esplanade" target="_blank" rel="noopener">Royal Esplanade</a> by <a href="https://hdrihaven.com/" target="_blank" rel="noopener">HDRI Haven</a>
</div>

<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/",
"three/nodes": "./jsm/nodes/Nodes.js"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
import PostProcessing from 'three/addons/renderers/common/PostProcessing.js';

import { pass, color, rangeFog } from 'three/nodes';

import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

let camera, scene, renderer;
let postProcessing;

init();

function init() {

const container = document.createElement( 'div' );
document.body.appendChild( container );

camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
camera.position.set( - 1.8, 0.6, 2.7 );

scene = new THREE.Scene();

renderer = new WebGPURenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
//renderer.toneMapping = THREE.ACESFilmicToneMapping; // apply tone mapping in post processing
container.appendChild( renderer.domElement );

// post processing

// render scene pass ( the same of css )
const scenePass = pass( scene, camera );
const scenePassViewZ = scenePass.getViewZNode();

// background color
const backgroundColor = color( 0x0066ff );

// get fog factor from scene pass context
// equivalent to: scene.fog = new THREE.Fog( 0x0066ff, 2.7, 4 );
const fogFactor = rangeFog( null, 2.7, 4 ).context( { viewZ: scenePassViewZ } );

// tone mapping scene pass
const scenePassTM = scenePass.toneMapping( THREE.ACESFilmicToneMapping );

// mix fog from fog factor and background color
const compose = fogFactor.mix( scenePassTM, backgroundColor );

postProcessing = new PostProcessing( renderer );
postProcessing.outputNode = compose;

//

new RGBELoader()
.setPath( 'textures/equirectangular/' )
.load( 'royal_esplanade_1k.hdr', function ( texture ) {

texture.mapping = THREE.EquirectangularReflectionMapping;

scene.environment = texture;

// model

const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );
loader.load( 'DamagedHelmet.gltf', function ( gltf ) {

scene.add( gltf.scene );

render();

} );

} );

//

const controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 2;
controls.maxDistance = 5;
controls.target.set( 0, - 0.1, - 0.2 );
controls.update();
controls.addEventListener( 'change', render ); // use if there is no animation loop

window.addEventListener( 'resize', onWindowResize );

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

render();

}

//

function render() {

postProcessing.renderAsync();

}

</script>

</body>
</html>