Browse Source

examples: post-processing material AO (#27475)

* examples: post-processing material AO

* examples: postprocessing material - use gl_FragCoord.xy for map lookup

* Examples: postprocessing material ao: show diffuse pass on both sides

* Examples: postprocessing material a - simplify example

* Examples: postprocessing material ao - simplify enironment texture
Gernot Steinegger 1 year ago
parent
commit
bad533f2a9

+ 1 - 0
docs/examples/en/postprocessing/EffectComposer.html

@@ -41,6 +41,7 @@
 			[example:webgl_postprocessing_godrays postprocessing godrays]<br />
 			[example:webgl_postprocessing_gtao postprocessing gtao]<br />
 			[example:webgl_postprocessing_masking postprocessing masking]<br />
+			[example:webgl_postprocessing_material_ao postprocessing material ao]<br />
 			[example:webgl_postprocessing_outline postprocessing outline]<br />
 			[example:webgl_postprocessing_pixel postprocessing pixelate]<br />
 			[example:webgl_postprocessing_procedural postprocessing procedural]<br />

+ 1 - 0
docs/examples/zh/postprocessing/EffectComposer.html

@@ -40,6 +40,7 @@
 			[example:webgl_postprocessing_godrays postprocessing godrays]<br />
 			[example:webgl_postprocessing_gtao postprocessing gtao]<br />
 			[example:webgl_postprocessing_masking postprocessing masking]<br />
+			[example:webgl_postprocessing_material_ao postprocessing material ao]<br />
 			[example:webgl_postprocessing_outline postprocessing outline]<br />
 			[example:webgl_postprocessing_pixel postprocessing pixelate]<br />
 			[example:webgl_postprocessing_procedural postprocessing procedural]<br />

+ 1 - 0
examples/files.json

@@ -249,6 +249,7 @@
 		"webgl_postprocessing_gtao",
 		"webgl_postprocessing_rgb_halftone",
 		"webgl_postprocessing_masking",
+		"webgl_postprocessing_material_ao",
 		"webgl_postprocessing_ssaa",
 		"webgl_postprocessing_outline",
 		"webgl_postprocessing_pixel",

+ 144 - 0
examples/jsm/materials/MeshPostProcessingMaterial.js

@@ -0,0 +1,144 @@
+import { MeshPhysicalMaterial } from 'three';
+
+/**
+ * The aim of this mesh material is to use information from a post processing pass in the diffuse color pass.
+ * This material is based on the MeshPhysicalMaterial.
+ *
+ * In the current state, only the information of a screen space AO pass can be used in the material.
+ * Actually, the output of any screen space AO (SSAO, GTAO) can be used,
+ * as it is only necessary to provide the AO in one color channel of a texture,
+ * however the AO pass must be rendered prior to the color pass,
+ * which makes the post-processing pass somewhat of a pre-processing pass.
+ * Fot this purpose a new map (`aoPassMap`) is added to the material.
+ * The value of the map is used the same way as the `aoMap` value.
+ *
+ * Motivation to use the outputs AO pass directly in the material:
+ * The incident light of a fragment is composed of ambient light, direct light and indirect light
+ * Ambient Occlusion only occludes ambient light and environment light, but not direct light.
+ * Direct light is only occluded by geometry that casts shadows.
+ * And of course the emitted light should not be darkened by ambient occlusion either.
+ * This cannot be achieved if the AO post processing pass is simply blended with the diffuse render pass.
+ *
+ * Further extension work might be to use the output of an SSR pass or an HBIL pass from a previous frame.
+ * This would then create the possibility of SSR and IR depending on material properties such as `roughness`, `metalness` and `reflectivity`.
+**/
+
+class MeshPostProcessingMaterial extends MeshPhysicalMaterial {
+
+	constructor( parameters ) {
+
+		const aoPassMap = parameters.aoPassMap;
+		const aoPassMapScale = parameters.aoPassMapScale || 1.0;
+		delete parameters.aoPassMap;
+		delete parameters.aoPassMapScale;
+
+		super( parameters );
+
+		this.onBeforeCompile = this._onBeforeCompile;
+		this.customProgramCacheKey = this._customProgramCacheKey;
+		this._aoPassMap = aoPassMap;
+		this.aoPassMapScale = aoPassMapScale;
+		this._shader = null;
+
+	}
+
+	get aoPassMap() {
+
+		return this._aoPassMap;
+
+	}
+
+	set aoPassMap( aoPassMap ) {
+
+		this._aoPassMap = aoPassMap;
+		this.needsUpdate = true;
+		this._setUniforms();
+
+	}
+
+	_customProgramCacheKey() {
+
+		return this._aoPassMap !== undefined && this._aoPassMap !== null ? 'aoPassMap' : '';
+
+	}
+
+	_onBeforeCompile( shader ) {
+
+		this._shader = shader;
+
+		if ( this._aoPassMap !== undefined && this._aoPassMap !== null ) {
+
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <aomap_pars_fragment>',
+				aomap_pars_fragment_replacement
+			);
+			shader.fragmentShader = shader.fragmentShader.replace(
+				'#include <aomap_fragment>',
+				aomap_fragment_replacement
+			);
+
+		}
+
+		this._setUniforms();
+
+	}
+
+	_setUniforms() {
+
+		if ( this._shader ) {
+
+			this._shader.uniforms.tAoPassMap = { value: this._aoPassMap };
+			this._shader.uniforms.aoPassMapScale = { value: this.aoPassMapScale };
+
+		}
+
+	}
+
+}
+
+const aomap_pars_fragment_replacement = /* glsl */`
+#ifdef USE_AOMAP
+
+	uniform sampler2D aoMap;
+	uniform float aoMapIntensity;
+
+#endif
+
+	uniform sampler2D tAoPassMap;
+	uniform float aoPassMapScale;
+`;
+
+const aomap_fragment_replacement = /* glsl */`
+#ifndef AOPASSMAP_SWIZZLE
+	#define AOPASSMAP_SWIZZLE r
+#endif
+	float ambientOcclusion = texelFetch( tAoPassMap, ivec2( gl_FragCoord.xy * aoPassMapScale ), 0 ).AOPASSMAP_SWIZZLE;
+
+#ifdef USE_AOMAP
+
+	// reads channel R, compatible with a combined OcclusionRoughnessMetallic (RGB) texture
+	ambientOcclusion = min( ambientOcclusion, texture2D( aoMap, vAoMapUv ).r );
+	ambientOcclusion *= ( ambientOcclusion - 1.0 ) * aoMapIntensity + 1.0;
+
+#endif
+
+	reflectedLight.indirectDiffuse *= ambientOcclusion;
+
+	#if defined( USE_CLEARCOAT ) 
+		clearcoatSpecularIndirect *= ambientOcclusion;
+	#endif
+
+	#if defined( USE_SHEEN ) 
+		sheenSpecularIndirect *= ambientOcclusion;
+	#endif
+
+	#if defined( USE_ENVMAP ) && defined( STANDARD )
+
+		float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
+
+		reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
+
+	#endif
+`;
+
+export { MeshPostProcessingMaterial };

+ 10 - 0
examples/jsm/postprocessing/GTAOPass.js

@@ -162,6 +162,12 @@ class GTAOPass extends Pass {
 
 	}
 
+	get gtaoMap() {
+
+		return this.pdRenderTarget.texture;
+
+	}
+
 	setGBuffer( depthTexture, normalTexture ) {
 
 		if ( depthTexture !== undefined ) {
@@ -359,6 +365,9 @@ class GTAOPass extends Pass {
 
 		switch ( this.output ) {
 
+			case GTAOPass.OUTPUT.Off:
+				break;
+
 			case GTAOPass.OUTPUT.Diffuse:
 
 				this.copyMaterial.uniforms.tDiffuse.value = readBuffer.texture;
@@ -561,6 +570,7 @@ class GTAOPass extends Pass {
 }
 
 GTAOPass.OUTPUT = {
+	'Off': - 1,
 	'Default': 0,
 	'Diffuse': 1,
 	'Depth': 2,

BIN
examples/screenshots/webgl_postprocessing_material_ao.jpg


+ 1 - 0
examples/tags.json

@@ -88,6 +88,7 @@
 	"webgl_postprocessing_fxaa": [ "msaa", "multisampled" ],
 	"webgl_postprocessing_godrays": [ "light scattering" ],
 	"webgl_postprocessing_gtao": [ "ambient occlusion" ],
+	"webgl_postprocessing_material_ao": [ "ambient occlusion"],
 	"webgl_shadowmap_progressive": [ "shadow", "soft", "lightmap", "onBeforeCompile" ],
 	"webgl_postprocessing_ssaa": [ "msaa", "multisampled" ],
 	"webgl_postprocessing_ssaa_unbiased": [ "msaa", "multisampled" ],

+ 278 - 0
examples/webgl_postprocessing_material_ao.html

@@ -0,0 +1,278 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - postprocessing - GTAO</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>
+			body {
+				background-color: #ffffff;
+				color: #000;
+			}
+			a {
+				color: #2983ff;
+			}
+		</style>
+	</head>
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Mesh Post Processing Material by <a href="https://github.com/Rabbid76" target="_blank" rel="noopener">Rabbid76</a><br/>
+			<p>Improved application of the AO passes by using the AO directly in the material shader instead of simply blending with the whole scene</p>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import Stats from 'three/addons/libs/stats.module.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+			import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
+			import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+			import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+			import { GTAOPass } from 'three/addons/postprocessing/GTAOPass.js';
+			import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+			import { MeshPostProcessingMaterial } from 'three/addons/materials/MeshPostProcessingMaterial.js';
+
+			let renderer, camera, scene, composer, controls, stats;
+			const sceneParameters = {
+				output: 0,
+				envMapIntensity: 1.0,
+				ambientLightIntensity: 0.0,
+				lightIntensity: 50,
+				shadow: true,
+			};
+			const aoParameters = {
+				radius: 0.5,
+				distanceExponent: 2.,
+				thickness: 10.,
+				scale: 1.,
+				samples: 16,
+				distanceFallOff: 1.,
+			};
+
+			init();
+			animate();
+
+			function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				renderer = new THREE.WebGLRenderer();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+				renderer.shadowMap.enabled = sceneParameters.shadow;
+
+				const plyLoader = new PLYLoader();
+				const rgbeloader = new RGBELoader();
+
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 50 );
+				camera.position.set( 0, 3, 5 );
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 1, 0 );
+				controls.update();
+				controls.enablePan = false;
+				controls.enableDamping = true;
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				scene = new THREE.Scene();
+				composer = new EffectComposer( renderer );
+
+				const gtaoPass = new GTAOPass( scene, camera, width, height );
+				gtaoPass.output = GTAOPass.OUTPUT.Off;
+				const renderPasse = new RenderPass( scene, camera );
+				const outputPass = new OutputPass();
+
+				composer.addPass( gtaoPass );
+				composer.addPass( renderPasse );
+				composer.addPass( outputPass );
+
+				rgbeloader.load( 'textures/equirectangular/royal_esplanade_1k.hdr', function ( texture ) {
+
+					texture.mapping = THREE.EquirectangularReflectionMapping;
+					scene.environment = texture;
+
+				} );
+			
+				const groundMaterial = new MeshPostProcessingMaterial( { color: 0x7f7f7f, envMapIntensity: sceneParameters.envMapIntensity, aoPassMap: gtaoPass.gtaoMap } );
+				const objectMaterial = new MeshPostProcessingMaterial( { color: 0xffffff, roughness: 0.5, metalness: 0.5, envMapIntensity: sceneParameters.envMapIntensity, aoPassMap: gtaoPass.gtaoMap } );
+				const emissiveMaterial = new MeshPostProcessingMaterial( { color: 0, emissive: 0xffffff, aoPassMap: gtaoPass.gtaoMap } );
+				plyLoader.load( 'models/ply/binary/Lucy100k.ply', ( geometry ) => {
+
+					geometry.computeVertexNormals();
+					const lucy = new THREE.Mesh( geometry, objectMaterial );
+					lucy.receiveShadow = true;
+					lucy.castShadow = true;
+					lucy.scale.setScalar( 0.001 );
+					lucy.rotation.set( 0, Math.PI, 0 );
+					lucy.position.set( 0.04, 1.8, 0.02 );
+					scene.add( lucy );
+
+				} );
+				const ambientLight = new THREE.AmbientLight( 0xffffff, sceneParameters.ambientLightIntensity );
+				const lightGroup = new THREE.Group();
+				const planeGeometry = new THREE.PlaneGeometry( 6, 6 );
+				const cylinderGeometry = new THREE.CylinderGeometry( 0.5, 0.5, 1, 64 );
+				const sphereGeometry = new THREE.SphereGeometry( 0.5, 32, 32 );
+				const lightSphereGeometry = new THREE.SphereGeometry( 0.1, 32, 32 );
+				scene.background = new THREE.Color( 0xbfe3dd );
+				scene.add( ambientLight );
+				scene.add( lightGroup );
+				const targetObject = new THREE.Object3D();
+				targetObject.position.set( 0, 1, 0 );
+				scene.add( targetObject );
+				const lightColors = [ 0xff4040, 0x40ff40, 0x4040ff ];
+				for ( let j = 0; j < 3; ++ j ) {
+
+					const light = new THREE.SpotLight( lightColors[ j ], sceneParameters.lightIntensity, 0, Math.PI / 9 );
+					light.castShadow = true;
+					light.shadow.camera.far = 15;
+					light.position.set( 5 * Math.cos( Math.PI * j * 2 / 3 ), 2.5, 5 * Math.sin( Math.PI * j * 2 / 3 ) );
+					light.target = targetObject;
+					lightGroup.add( light );
+
+				}
+
+				const groundPlane = new THREE.Mesh( planeGeometry, groundMaterial );
+				groundPlane.rotation.x = - Math.PI / 2;
+				groundPlane.position.set( 0, 0, 0 );
+				groundPlane.receiveShadow = true;
+				scene.add( groundPlane );
+				const pedestal = new THREE.Mesh( cylinderGeometry, groundMaterial );
+				pedestal.position.set( 0, 0.5, 0 );
+				pedestal.receiveShadow = true;
+				pedestal.castShadow = true;
+				scene.add( pedestal );
+				const sphereMesh = new THREE.InstancedMesh( sphereGeometry, objectMaterial, 6 );
+				sphereMesh.receiveShadow = true;
+				sphereMesh.castShadow = true;
+				scene.add( sphereMesh );
+				[ ...Array( 6 ).keys() ].forEach( ( i ) => sphereMesh.setMatrixAt( i, new THREE.Matrix4().makeTranslation( Math.cos( Math.PI * i / 3 ), 0.5, Math.sin( Math.PI * i / 3 ) ) ) );
+				const lightSphereMesh = new THREE.InstancedMesh( lightSphereGeometry, emissiveMaterial, 4 );
+				scene.add( lightSphereMesh );
+				[ ...Array( 4 ).keys() ].forEach( ( i ) => lightSphereMesh.setMatrixAt( i, new THREE.Matrix4().makeTranslation( 0.4 * Math.cos( Math.PI * ( i + 0.5 ) / 2 ), 1.1, 0.45 * Math.sin( Math.PI * ( i + 0.5 ) / 2 ) ) ) );
+
+				const updateGtaoMaterial = () => gtaoPass.updateGtaoMaterial( aoParameters );
+				const updateOutput = () => {
+
+					composer.removePass( gtaoPass );
+					composer.insertPass( gtaoPass, sceneParameters.output == 1 ? 1 : 0 );
+
+					switch ( sceneParameters.output ) {
+
+						default:
+						case 0:
+							gtaoPass.output = GTAOPass.OUTPUT.Off;
+							gtaoPass.enabled = true;
+							renderPasse.enabled = true;
+							break;
+						case 1:
+							gtaoPass.output = GTAOPass.OUTPUT.Default;
+							gtaoPass.enabled = true;
+							renderPasse.enabled = true;
+							break;
+						case 2:
+							gtaoPass.output = GTAOPass.OUTPUT.Diffuse;
+							gtaoPass.enabled = false;
+							renderPasse.enabled = true;
+							break;
+						case 3:
+							gtaoPass.output = GTAOPass.OUTPUT.Denoise;
+							gtaoPass.enabled = true;
+							renderPasse.enabled = false;
+							break;
+
+					}
+
+					groundMaterial.aoPassMap = sceneParameters.output === 0 ? gtaoPass.gtaoMap : null;
+					objectMaterial.aoPassMap = sceneParameters.output === 0 ? gtaoPass.gtaoMap : null;
+
+				};
+
+				updateOutput();
+				updateGtaoMaterial();
+
+				const gui = new GUI();
+				gui.add( sceneParameters, 'output', {
+					'material AO': 0,
+					'post blended AO': 1,
+					'only diffuse': 2,
+					'only AO': 3,
+
+				} ).onChange( () => updateOutput() );
+				gui.add( sceneParameters, 'envMapIntensity' ).min( 0 ).max( 1 ).step( 0.01 ).onChange( () => {
+
+					groundMaterial.envMapIntensity = sceneParameters.envMapIntensity;
+					objectMaterial.envMapIntensity = sceneParameters.envMapIntensity;
+
+				} );
+				gui.add( sceneParameters, 'ambientLightIntensity' ).min( 0.0 ).max( 1.0 ).step( 0.01 ).onChange( () => {
+
+					ambientLight.intensity = sceneParameters.ambientLightIntensity;
+
+				} );
+				gui.add( sceneParameters, 'lightIntensity' ).min( 0 ).max( 100 ).step( 1 ).onChange( () => {
+
+					lightGroup.children.forEach( light => light.intensity = sceneParameters.lightIntensity );
+
+				} );
+				gui.add( sceneParameters, 'shadow' ).onChange( ( value ) => {
+
+					renderer.shadowMap.enabled = value;
+					lightGroup.children.forEach( light => light.castShadow = value );
+			
+				} );
+				gui.add( aoParameters, 'radius' ).min( 0.01 ).max( 2 ).step( 0.01 ).onChange( () => updateGtaoMaterial() );
+				gui.add( aoParameters, 'distanceExponent' ).min( 1 ).max( 4 ).step( 0.01 ).onChange( () => updateGtaoMaterial() );
+				gui.add( aoParameters, 'thickness' ).min( 0.01 ).max( 10 ).step( 0.01 ).onChange( () => updateGtaoMaterial() );
+				gui.add( aoParameters, 'distanceFallOff' ).min( 0 ).max( 1 ).step( 0.01 ).onChange( () => updateGtaoMaterial() );
+				gui.add( aoParameters, 'scale' ).min( 0.01 ).max( 2.0 ).step( 0.01 ).onChange( () => updateGtaoMaterial() );
+				gui.add( aoParameters, 'samples' ).min( 2 ).max( 32 ).step( 1 ).onChange( () => updateGtaoMaterial() );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+				composer.setSize( width, height );
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				controls.update();
+				stats.begin();
+				composer.render();
+				stats.end();
+
+			}
+
+		</script>
+	</body>
+</html>