Răsfoiți Sursa

Addons: Add `LensflareNode`. (#29715)

* LensflareNode: Experimenting with different approaches.

* LensflareNode: Improve example.

* Examples: Clean up

* Examples: Add comment.

* LensflareNode: Updating API.

* Examples: Clean up.
Michael Herzog 1 an în urmă
părinte
comite
167c022af4

+ 1 - 0
examples/files.json

@@ -393,6 +393,7 @@
 		"webgpu_postprocessing_dof",
 		"webgpu_postprocessing_pixel",
 		"webgpu_postprocessing_fxaa",
+		"webgpu_postprocessing_lensflare",
 		"webgpu_postprocessing_masking",
 		"webgpu_postprocessing_motion_blur",
 		"webgpu_postprocessing_outline",

+ 161 - 0
examples/jsm/tsl/display/LensflareNode.js

@@ -0,0 +1,161 @@
+import { RenderTarget, Vector2 } from 'three';
+import { convertToTexture, TempNode, nodeObject, Fn, NodeUpdateType, QuadMesh, PostProcessingUtils, NodeMaterial, passTexture, uv, vec2, vec3, vec4, max, float, sub, int, Loop, fract, pow, distance } from 'three/tsl';
+
+const _quadMesh = /*@__PURE__*/ new QuadMesh();
+const _size = /*@__PURE__*/ new Vector2();
+let _rendererState;
+
+/**
+ * References:
+ * https://john-chapman-graphics.blogspot.com/2013/02/pseudo-lens-flare.html
+ * https://john-chapman.github.io/2017/11/05/pseudo-lens-flare.html
+ */
+class LensflareNode extends TempNode {
+
+	static get type() {
+
+		return 'LensflareNode';
+
+	}
+
+	constructor( textureNode, params = {} ) {
+
+		super();
+
+		this.textureNode = textureNode;
+
+		const {
+			ghostTint = vec3( 1, 1, 1 ),
+			threshold = float( 0.5 ),
+			ghostSamples = float( 4 ),
+			ghostSpacing = float( 0.25 ),
+			ghostAttenuationFactor = float( 25 ),
+			downSampleRatio = 4
+		} = params;
+
+		this.ghostTint = ghostTint;
+		this.threshold = threshold;
+		this.ghostSamples = ghostSamples;
+		this.ghostSpacing = ghostSpacing;
+		this.ghostAttenuationFactor = ghostAttenuationFactor;
+		this.downSampleRatio = downSampleRatio;
+
+		this.updateBeforeType = NodeUpdateType.FRAME;
+
+		// render targets
+
+		this._renderTarget = new RenderTarget( 1, 1, { depthBuffer: false } );
+		this._renderTarget.texture.name = 'LensflareNode';
+
+		// materials
+
+		this._material = new NodeMaterial();
+		this._material.name = 'LensflareNode';
+
+		//
+
+		this._textureNode = passTexture( this, this._renderTarget.texture );
+
+	}
+
+	getTextureNode() {
+
+		return this._textureNode;
+
+	}
+
+	setSize( width, height ) {
+
+		const resx = Math.round( width / this.downSampleRatio );
+		const resy = Math.round( height / this.downSampleRatio );
+
+		this._renderTarget.setSize( resx, resy );
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+
+		const size = renderer.getDrawingBufferSize( _size );
+		this.setSize( size.width, size.height );
+
+		_rendererState = PostProcessingUtils.resetRendererState( renderer, _rendererState );
+
+		_quadMesh.material = this._material;
+
+		// clear
+
+		renderer.setMRT( null );
+
+		// lensflare
+
+		renderer.setRenderTarget( this._renderTarget );
+		_quadMesh.render( renderer );
+
+		// restore
+
+		PostProcessingUtils.restoreRendererState( renderer, _rendererState );
+
+	}
+
+	setup( builder ) {
+
+		const lensflare = Fn( () => {
+
+			// flip uvs so lens flare pivot around the image center
+
+			const texCoord = uv().oneMinus().toVar();
+
+			// ghosts are positioned along this vector
+
+			const ghostVec = sub( vec2( 0.5 ), texCoord ).mul( this.ghostSpacing ).toVar();
+
+			// sample ghosts
+
+			const result = vec4().toVar();
+
+			Loop( { start: int( 0 ), end: int( this.ghostSamples ), type: 'int', condition: '<' }, ( { i } ) => {
+
+				// use fract() to ensure that the texture coordinates wrap around
+
+				const sampleUv = fract( texCoord.add( ghostVec.mul( float( i ) ) ) ).toVar();
+
+				// reduce contributions from samples at the screen edge
+
+				const d = distance( sampleUv, vec2( 0.5 ) );
+				const weight = pow( d.oneMinus(), this.ghostAttenuationFactor );
+
+				// accumulate
+
+				let sample = this.textureNode.uv( sampleUv ).rgb;
+
+				sample = max( sample.sub( this.threshold ), vec3( 0 ) ).mul( this.ghostTint );
+
+				result.addAssign( sample.mul( weight ) );
+
+			} );
+
+			return result;
+
+		} );
+
+		this._material.fragmentNode = lensflare().context( builder.getSharedContext() );
+		this._material.needsUpdate = true;
+
+		return this._textureNode;
+
+	}
+
+	dispose() {
+
+		this._renderTarget.dispose();
+		this._material.dispose();
+
+	}
+
+}
+
+export default LensflareNode;
+
+export const lensflare = ( inputNode, params ) => nodeObject( new LensflareNode( convertToTexture( inputNode ), params ) );

BIN
examples/models/gltf/space_ship_hallway.glb


BIN
examples/screenshots/webgpu_postprocessing_lensflare.jpg


BIN
examples/textures/equirectangular/ice_planet_close.jpg


+ 183 - 0
examples/webgpu_postprocessing_lensflare.html

@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - postprocessing lensflares</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>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - postprocessing lensflares<br />
+			<a href="https://skfb.ly/6SqUF" target="_blank" rel="noopener">Space Ship Hallway</a> by 
+			<a href="https://sketchfab.com/yeeyeeman" target="_blank" rel="noopener">yeeyeeman</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">Creative Commons Attribution</a>.<br />
+			<a href="https://www.spacespheremaps.com/planetary-spheremaps/" target="_blank" rel="noopener">Ice Planet Close</a> from <a href="https://www.spacespheremaps.com/" target="_blank" rel="noopener">Space Spheremaps</a>.
+		</div>
+
+		<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, mrt, output, emissive, uniform } from 'three/tsl';
+			import { bloom } from 'three/addons/tsl/display/BloomNode.js';
+			import { lensflare } from 'three/addons/tsl/display/LensflareNode.js';
+			import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
+
+			import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, controls, stats;
+			let postProcessing;
+
+			init();
+
+			async function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				//
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 0.5, - 0.5 );
+
+				scene = new THREE.Scene();
+
+				const texture = await new UltraHDRLoader().loadAsync( 'textures/equirectangular/ice_planet_close.jpg' );
+
+				texture.mapping = THREE.EquirectangularReflectionMapping;
+
+				scene.background = texture;
+				scene.environment = texture;
+
+				scene.backgroundIntensity = 2;
+				scene.environmentIntensity = 15;
+
+				// model
+
+				const loader = new GLTFLoader();
+				const gltf = await loader.loadAsync( 'models/gltf/space_ship_hallway.glb' );
+
+				const object = gltf.scene;
+
+				const aabb = new THREE.Box3().setFromObject( object );
+				const center = aabb.getCenter( new THREE.Vector3() );
+
+				object.position.x += ( object.position.x - center.x );
+				object.position.y += ( object.position.y - center.y );
+				object.position.z += ( object.position.z - center.z );
+
+				scene.add( object );
+			
+				//
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				container.appendChild( renderer.domElement );
+
+				//
+
+				const scenePass = pass( scene, camera );
+				scenePass.setMRT( mrt( {
+					output,
+					emissive
+				} ) );
+
+				const outputPass = scenePass.getTextureNode();
+				const emissivePass = scenePass.getTextureNode( 'emissive' );
+
+				const bloomPass = bloom( emissivePass, 1, 1 );
+
+				const threshold = uniform( 0.5 );
+				const ghostAttenuationFactor = uniform( 25 );
+				const ghostSpacing = uniform( 0.25 );
+
+				const flarePass = lensflare( bloomPass, {
+					threshold,
+					ghostAttenuationFactor,
+					ghostSpacing
+				} );
+
+				const blurPass = gaussianBlur( flarePass, 8 ); // optional (blurring produces better flare quality but also adds some overhead)
+
+				postProcessing = new THREE.PostProcessing( renderer );
+				postProcessing.outputNode = outputPass.add( bloomPass ).add( blurPass );
+
+				//
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableDamping = true;
+				controls.enablePan = false;
+				controls.enableZoom = false;
+				controls.target.copy( camera.position );
+				controls.target.z -= 0.01;
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				//
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				//
+
+				const gui = new GUI();
+
+				const bloomFolder = gui.addFolder( 'bloom' );
+				bloomFolder.add( bloomPass.strength, 'value', 0.0, 2.0 ).name( 'strength' );
+				bloomFolder.add( bloomPass.radius, 'value', 0.0, 1.0 ).name( 'radius' );
+			
+				const lensflareFolder = gui.addFolder( 'lensflare' );
+				lensflareFolder.add( threshold, 'value', 0.0, 1.0 ).name( 'threshold' );
+				lensflareFolder.add( ghostAttenuationFactor, 'value', 10.0, 50.0 ).name( 'attenuation' );
+				lensflareFolder.add( ghostSpacing, 'value', 0.0, 0.3 ).name( 'spacing' );
+
+				const toneMappingFolder = gui.addFolder( 'tone mapping' );
+				toneMappingFolder.add( renderer, 'toneMappingExposure', 0.1, 2 ).name( 'exposure' );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function render() {
+
+				stats.update();
+
+				controls.update();
+
+				postProcessing.render();
+
+			}
+
+		</script>
+
+	</body>
+</html>

粤ICP备19079148号