Procházet zdrojové kódy

Nodes: Add `FXAANode`. (#28859)

Michael Herzog před 1 rokem
rodič
revize
5481dd5731

+ 1 - 0
examples/files.json

@@ -375,6 +375,7 @@
 		"webgpu_postprocessing_anamorphic",
 		"webgpu_postprocessing_ao",
 		"webgpu_postprocessing_dof",
+		"webgpu_postprocessing_fxaa",
 		"webgpu_postprocessing_sobel",
 		"webgpu_postprocessing",
 		"webgpu_procedural_texture",

binární
examples/screenshots/webgpu_postprocessing_fxaa.jpg


+ 166 - 0
examples/webgpu_postprocessing_fxaa.html

@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - FXAA</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">
+	</head>
+
+	<body>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { pass, renderOutput } from 'three/tsl';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			const params = {
+				enabled: true,
+				animated: false
+			};
+
+			let camera, scene, renderer, clock, group;
+			let postProcessing;
+
+			init();
+
+			async function init() {
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 200 );
+				camera.position.z = 50;
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xffffff );
+
+				clock = new THREE.Clock();
+
+				//
+
+				const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d );
+				hemiLight.position.set( 0, 1000, 0 );
+				scene.add( hemiLight );
+
+				const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
+				dirLight.position.set( - 3000, 1000, - 1000 );
+				scene.add( dirLight );
+
+				//
+
+				group = new THREE.Group();
+
+				const geometry = new THREE.TetrahedronGeometry();
+				const material = new THREE.MeshStandardMaterial( { color: 0xf73232, flatShading: true } );
+
+				for ( let i = 0; i < 100; i ++ ) {
+
+					const mesh = new THREE.Mesh( geometry, material );
+
+					mesh.position.x = Math.random() * 50 - 25;
+					mesh.position.y = Math.random() * 50 - 25;
+					mesh.position.z = Math.random() * 50 - 25;
+
+					mesh.scale.setScalar( Math.random() * 2 + 1 );
+
+					mesh.rotation.x = Math.random() * Math.PI;
+					mesh.rotation.y = Math.random() * Math.PI;
+					mesh.rotation.z = Math.random() * Math.PI;
+
+					group.add( mesh );
+
+				}
+
+				scene.add( group );
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+
+				// post processing
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				// ignore default output color transform ( toneMapping and outputColorSpace )
+				// use renderOutput() for control the sequence
+
+				postProcessing.outputColorTransform = false;
+
+				// scene pass
+
+				const scenePass = pass( scene, camera );
+				const outputPass = renderOutput( scenePass );
+
+				// FXAA must be computed in sRGB color space (so after tone mapping and color space conversion)
+
+				const fxaaPass = outputPass.fxaa();
+				postProcessing.outputNode = fxaaPass;
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				//
+
+				const gui = new GUI();
+				gui.title( 'FXAA settings' );
+				gui.add( params, 'enabled' ).onChange( ( value ) => {
+			
+					if ( value === true ) {
+
+						postProcessing.outputNode = fxaaPass;
+
+					} else {
+
+						postProcessing.outputNode = outputPass;
+
+					}
+
+					postProcessing.needsUpdate = true;
+
+				} );
+				gui.add( params, 'animated' );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				const delta = clock.getDelta();
+
+				if ( params.animated === true ) {
+
+					group.rotation.y += delta * 0.1;
+
+				}
+
+				postProcessing.render();
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
src/nodes/Nodes.js

@@ -135,6 +135,7 @@ export { default as RGBShiftNode, rgbShift } from './display/RGBShiftNode.js';
 export { default as FilmNode, film } from './display/FilmNode.js';
 export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
 export { default as GTAONode, ao } from './display/GTAONode.js';
+export { default as FXAANode, fxaa } from './display/FXAANode.js';
 export { default as RenderOutputNode, renderOutput } from './display/RenderOutputNode.js';
 
 export { default as PassNode, pass, passTexture, depthPass } from './display/PassNode.js';

+ 327 - 0
src/nodes/display/FXAANode.js

@@ -0,0 +1,327 @@
+import TempNode from '../core/TempNode.js';
+import { uv } from '../accessors/UVNode.js';
+import { addNodeElement, tslFn, nodeObject, float, vec2, vec4, int, If } from '../shadernode/ShaderNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { uniform } from '../core/UniformNode.js';
+import { abs, max, min, mix, pow } from '../math/MathNode.js';
+import { sub } from '../math/OperatorNode.js';
+import { loop, Break } from '../utils/LoopNode.js';
+
+import { Vector2 } from '../../math/Vector2.js';
+
+class FXAANode extends TempNode {
+
+	constructor( textureNode ) {
+
+		super();
+
+		this.textureNode = textureNode;
+
+		this.updateBeforeType = NodeUpdateType.RENDER;
+
+		this._invSize = uniform( new Vector2() );
+
+	}
+
+	updateBefore() {
+
+		const map = this.textureNode.value;
+
+		this._invSize.value.set( 1 / map.image.width, 1 / map.image.height );
+
+	}
+
+	setup() {
+
+		const textureNode = this.textureNode.bias( - 100 );
+		const uvNode = textureNode.uvNode || uv();
+
+		// FXAA 3.11 implementation by NVIDIA, ported to WebGL by Agost Biro ([email protected])
+
+		//----------------------------------------------------------------------------------
+		// File:        es3-kepler\FXAA\assets\shaders/FXAA_DefaultES.frag
+		// SDK Version: v3.00
+		// Email:       [email protected]
+		// Site:        http://developer.nvidia.com/
+		//
+		// Copyright (c) 2014-2015, NVIDIA CORPORATION. All rights reserved.
+		//
+		// Redistribution and use in source and binary forms, with or without
+		// modification, are permitted provided that the following conditions
+		// are met:
+		//  * Redistributions of source code must retain the above copyright
+		//    notice, this list of conditions and the following disclaimer.
+		//  * Redistributions in binary form must reproduce the above copyright
+		//    notice, this list of conditions and the following disclaimer in the
+		//    documentation and/or other materials provided with the distribution.
+		//  * Neither the name of NVIDIA CORPORATION nor the names of its
+		//    contributors may be used to endorse or promote products derived
+		//    from this software without specific prior written permission.
+		//
+		// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
+		// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+		// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+		// PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+		// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+		// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+		// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+		// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+		// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+		// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+		// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+		//
+		//----------------------------------------------------------------------------------
+
+		const FxaaTexTop = ( p ) => textureNode.uv( p );
+		const FxaaTexOff = ( p, o, r ) => textureNode.uv( p.add( o.mul( r ) ) );
+
+		const NUM_SAMPLES = int( 5 );
+
+		const contrast = tslFn( ( [ a_immutable, b_immutable ] ) => {
+
+			// assumes colors have premultipliedAlpha, so that the calculated color contrast is scaled by alpha
+
+			const b = vec4( b_immutable ).toVar();
+			const a = vec4( a_immutable ).toVar();
+			const diff = vec4( abs( a.sub( b ) ) ).toVar();
+
+			return max( max( max( diff.r, diff.g ), diff.b ), diff.a );
+
+		} );
+
+		// FXAA3 QUALITY - PC
+
+		const FxaaPixelShader = tslFn( ( [ uv, fxaaQualityRcpFrame, fxaaQualityEdgeThreshold, fxaaQualityinvEdgeThreshold ] ) => {
+
+			const rgbaM = FxaaTexTop( uv ).toVar();
+			const rgbaS = FxaaTexOff( uv, vec2( 0.0, - 1.0 ), fxaaQualityRcpFrame.xy ).toVar();
+			const rgbaE = FxaaTexOff( uv, vec2( 1.0, 0.0 ), fxaaQualityRcpFrame.xy ).toVar();
+			const rgbaN = FxaaTexOff( uv, vec2( 0.0, 1.0 ), fxaaQualityRcpFrame.xy ).toVar();
+			const rgbaW = FxaaTexOff( uv, vec2( - 1.0, 0.0 ), fxaaQualityRcpFrame.xy ).toVar();
+			// . S .
+			// W M E
+			// . N .
+
+			const contrastN = contrast( rgbaM, rgbaN ).toVar();
+			const contrastS = contrast( rgbaM, rgbaS ).toVar();
+			const contrastE = contrast( rgbaM, rgbaE ).toVar();
+			const contrastW = contrast( rgbaM, rgbaW ).toVar();
+
+			const maxValue = max( contrastN, max( contrastS, max( contrastE, contrastW ) ) ).toVar();
+
+			// . 0 .
+			// 0 0 0
+			// . 0 .
+
+			If( maxValue.lessThan( fxaaQualityEdgeThreshold ), () => {
+
+				return rgbaM; // assuming define FXAA_DISCARD is always 0
+
+			} );
+
+			//
+
+			const relativeVContrast = sub( contrastN.add( contrastS ), ( contrastE.add( contrastW ) ) ).toVar();
+			relativeVContrast.mulAssign( fxaaQualityinvEdgeThreshold );
+
+			// 45 deg edge detection and corners of objects, aka V/H contrast is too similar
+
+			If( abs( relativeVContrast ).lessThan( 0.3 ), () => {
+
+				// locate the edge
+
+				const x = contrastE.greaterThan( contrastW ).cond( 1, - 1 ).toVar();
+				const y = contrastS.greaterThan( contrastN ).cond( 1, - 1 ).toVar();
+
+				const dirToEdge = vec2( x, y ).toVar();
+				// . 2 .      . 1 .
+				// 1 0 2  ~=  0 0 1
+				// . 1 .      . 0 .
+
+				// tap 2 pixels and see which ones are "outside" the edge, to
+				// determine if the edge is vertical or horizontal
+
+				const rgbaAlongH = FxaaTexOff( uv, vec2( dirToEdge.x, dirToEdge.y ), fxaaQualityRcpFrame.xy );
+				const matchAlongH = contrast( rgbaM, rgbaAlongH ).toVar();
+				// . 1 .
+				// 0 0 1
+				// . 0 H
+
+				const rgbaAlongV = FxaaTexOff( uv, vec2( dirToEdge.x.negate(), dirToEdge.y.negate() ), fxaaQualityRcpFrame.xy );
+				const matchAlongV = contrast( rgbaM, rgbaAlongV ).toVar();
+				// V 1 .
+				// 0 0 1
+				// . 0 .
+
+				relativeVContrast.assign( matchAlongV.sub( matchAlongH ) );
+				relativeVContrast.mulAssign( fxaaQualityinvEdgeThreshold );
+
+				If( abs( relativeVContrast ).lessThan( 0.3 ), () => { // 45 deg edge
+
+					// 1 1 .
+					// 0 0 1
+					// . 0 1
+
+					// do a simple blur
+					const sum = rgbaN.add( rgbaS ).add( rgbaE ).add( rgbaW );
+					return mix( rgbaM, sum.mul( 0.25 ), 0.4 );
+
+				} );
+
+			} );
+
+			const offNP = vec2().toVar();
+
+			If( relativeVContrast.lessThanEqual( 0 ), () => {
+
+				rgbaN.assign( rgbaW );
+				rgbaS.assign( rgbaE );
+
+				// . 0 .      1
+				// 1 0 1  ->  0
+				// . 0 .      1
+
+				offNP.x.assign( 0 );
+				offNP.y.assign( fxaaQualityRcpFrame.y );
+
+			 } ).else( () => {
+
+				offNP.x.assign( fxaaQualityRcpFrame.x );
+				offNP.y.assign( 0 );
+
+			 } );
+
+			const mn = contrast( rgbaM, rgbaN ).toVar();
+			const ms = contrast( rgbaM, rgbaS ).toVar();
+
+			If( mn.lessThanEqual( ms ), () => {
+
+				rgbaN.assign( rgbaS );
+
+			} );
+
+			const doneN = int( 0 ).toVar();
+			const doneP = int( 0 ).toVar();
+
+			const nDist = float( 0 ).toVar();
+			const pDist = float( 0 ).toVar();
+
+			const posN = vec2( uv ).toVar();
+			const posP = vec2( uv ).toVar();
+
+			const iterationsUsedN = int( 0 ).toVar();
+			const iterationsUsedP = int( 0 ).toVar();
+
+			loop( NUM_SAMPLES, ( { i } ) => {
+
+				const increment = i.add( 1 ).toVar();
+
+				If( doneN.equal( 0 ), () => {
+
+					nDist.addAssign( increment );
+					posN.assign( uv.add( offNP.mul( nDist ) ) );
+					const rgbaEndN = FxaaTexTop( posN.xy );
+
+					const nm = contrast( rgbaEndN, rgbaM ).toVar();
+					const nn = contrast( rgbaEndN, rgbaN ).toVar();
+
+					If( nm.greaterThan( nn ), () => {
+
+						doneN.assign( 1 );
+
+					} );
+
+					iterationsUsedN.assign( i );
+
+				} );
+
+				If( doneP.equal( 0 ), () => {
+
+					pDist.addAssign( increment );
+					posP.assign( uv.sub( offNP.mul( pDist ) ) );
+					const rgbaEndP = FxaaTexTop( posP.xy );
+
+					const pm = contrast( rgbaEndP, rgbaM ).toVar();
+					const pn = contrast( rgbaEndP, rgbaN ).toVar();
+
+					If( pm.greaterThan( pn ), () => {
+
+						doneP.assign( 1 );
+
+					} );
+
+					iterationsUsedP.assign( i );
+
+				} );
+
+				If( doneN.equal( 1 ).or( doneP.equal( 1 ) ), () => {
+
+					Break();
+
+				} );
+
+			} );
+
+			If( doneN.equal( 0 ).and( doneP.equal( 0 ) ), () => {
+
+				return rgbaM; // failed to find end of edge
+
+			} );
+
+			const distN = float( 1 ).toVar();
+			const distP = float( 1 ).toVar();
+
+			If( doneN.equal( 1 ), () => {
+
+				distN.assign( float( iterationsUsedN ).div( float( NUM_SAMPLES.sub( 1 ) ) ) );
+
+			} );
+
+			If( doneP.equal( 1 ), () => {
+
+				distP.assign( float( iterationsUsedP ).div( float( NUM_SAMPLES.sub( 1 ) ) ) );
+
+			} );
+
+			const dist = min( distN, distP );
+
+			// hacky way of reduces blurriness of mostly diagonal edges
+			// but reduces AA quality
+			dist.assign( pow( dist, 0.5 ) );
+			dist.assign( float( 1 ).sub( dist ) );
+
+			return mix( rgbaM, rgbaN, dist.mul( 0.5 ) );
+
+		} ).setLayout( {
+			name: 'FxaaPixelShader',
+			type: 'vec4',
+			inputs: [
+				{ name: 'uv', type: 'vec2' },
+				{ name: 'fxaaQualityRcpFrame', type: 'vec2' },
+				{ name: 'fxaaQualityEdgeThreshold', type: 'float' },
+				{ name: 'fxaaQualityinvEdgeThreshold', type: 'float' },
+			]
+		} );
+
+		const fxaa = tslFn( () => {
+
+			const edgeDetectionQuality = float( 0.2 );
+			const invEdgeDetectionQuality = float( 1 ).div( edgeDetectionQuality );
+
+			return FxaaPixelShader( uvNode, this._invSize, edgeDetectionQuality, invEdgeDetectionQuality );
+
+		} );
+
+		const outputNode = fxaa();
+
+		return outputNode;
+
+	}
+
+}
+
+export const fxaa = ( node ) => nodeObject( new FXAANode( nodeObject( node ).toTexture() ) );
+
+addNodeElement( 'fxaa', fxaa );
+
+export default FXAANode;