Просмотр исходного кода

Examples: Add WebGPU HDR Example (#31893)

* Examples: Add WebGPU HDR Example

* fix lint issue

* fix: prevent mobile scrolling during touch interactions in WebGPU HDR example

* fix

* update aa example

* Update param type

* removed unused import

* Update AfterImageNode.js

Wrap `damp` into `nodeObject()`.

* Update AfterImageNode.js

* chore: add webgpu_hdr to e2e test exception list

* fix: remove unnecessary transparent flag from WebGPU HDR renderer

* feedbacks

---------

Co-authored-by: Michael Herzog <michael.herzog@human-interactive.org>
Renaud Rohlinger 5 месяцев назад
Родитель
Сommit
8d43f7a991

+ 1 - 0
examples/files.json

@@ -329,6 +329,7 @@
 		"webgpu_depth_texture",
 		"webgpu_display_stereo",
 		"webgpu_equirectangular",
+		"webgpu_hdr",
 		"webgpu_instance_mesh",
 		"webgpu_instance_path",
 		"webgpu_instance_points",

+ 7 - 7
examples/jsm/tsl/display/AfterImageNode.js

@@ -1,5 +1,5 @@
 import { RenderTarget, Vector2, QuadMesh, NodeMaterial, RendererUtils, TempNode, NodeUpdateType } from 'three/webgpu';
-import { nodeObject, Fn, float, uv, texture, passTexture, uniform, sign, max, convertToTexture } from 'three/tsl';
+import { nodeObject, Fn, float, uv, texture, passTexture, sign, max, convertToTexture } from 'three/tsl';
 
 const _size = /*@__PURE__*/ new Vector2();
 const _quadMeshComp = /*@__PURE__*/ new QuadMesh();
@@ -24,9 +24,9 @@ class AfterImageNode extends TempNode {
 	 * Constructs a new after image node.
 	 *
 	 * @param {TextureNode} textureNode - The texture node that represents the input of the effect.
-	 * @param {number} [damp=0.96] - The damping intensity. A higher value means a stronger after image effect.
+	 * @param {Node<float>} [damp=0.96] - The damping intensity. A higher value means a stronger after image effect.
 	 */
-	constructor( textureNode, damp = 0.96 ) {
+	constructor( textureNode, damp = float( 0.96 ) ) {
 
 		super( 'vec4' );
 
@@ -49,9 +49,9 @@ class AfterImageNode extends TempNode {
 		 * persists longer, while a lower value means it fades faster. Should be in
 		 * the range `[0, 1]`.
 		 *
-		 * @type {UniformNode<float>}
+		 * @type {Node<float>}
 		 */
-		this.damp = uniform( damp );
+		this.damp = damp;
 
 		/**
 		 * The render target used for compositing the effect.
@@ -234,9 +234,9 @@ class AfterImageNode extends TempNode {
  * @tsl
  * @function
  * @param {Node<vec4>} node - The node that represents the input of the effect.
- * @param {number} [damp=0.96] - The damping intensity. A higher value means a stronger after image effect.
+ * @param {(Node<float>|number)} [damp=0.96] - The damping intensity. A higher value means a stronger after image effect.
  * @returns {AfterImageNode}
  */
-export const afterImage = ( node, damp ) => nodeObject( new AfterImageNode( convertToTexture( node ), damp ) );
+export const afterImage = ( node, damp ) => nodeObject( new AfterImageNode( convertToTexture( node ), nodeObject( damp ) ) );
 
 export default AfterImageNode;

BIN
examples/screenshots/webgpu_hdr.jpg


+ 196 - 0
examples/webgpu_hdr.html

@@ -0,0 +1,196 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+	<meta charset="utf-8" />
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
+	<title>three.js webgpu - HDR Draw</title>
+	<link type="text/css" rel="stylesheet" href="main.css" />
+	<style>
+		#no-hdr {
+			position: absolute;
+			font-family: monospace;
+			font-size: 11px;
+			font-weight: normal;
+			text-align: center;
+			background: #000;
+			color: #fff;
+			left: 50%;
+			transform: translateX(-50%);
+			padding: 1.5em;
+			max-width: 600px;
+			margin: 5em auto 0;
+		}
+	</style>
+	</head>
+	<body>
+
+		<div id="info" style="color: #000">
+			<a href="https://threejs.org" target="_blank" rel="noopener">threejs</a> - HDR Draw
+		</div>
+		<div id="no-hdr" style="display: none">
+			<div>
+			The browser says your device or monitor doesn't support HDR.<br />
+			If you're on a laptop using an external monitor, try the built in
+			monitor<br />
+			or, try this site on your phone. Most phones support HDR.
+			</div>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+				"three": "../build/three.webgpu.js",
+				"three/webgpu": "../build/three.webgpu.js",
+				"three/tsl": "../build/three.tsl.js",
+				"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+			import { pass, uv, uniform } from 'three/tsl';
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import { afterImage } from 'three/addons/tsl/display/AfterImageNode.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import { ExtendedSRGBColorSpace, ExtendedSRGBColorSpaceImpl } from 'three/addons/math/ColorSpaces.js';
+
+			const params = {
+				intensity: uniform( 4.0, 'float' ).setName( 'intensity' ),
+				hardness: uniform( 0.4, 'float' ).setName( 'hardness' ),
+				radius: uniform( 0.5, 'float' ).setName( 'radius' ),
+				afterImageDecay: uniform( 0.985, 'float' ).setName( 'afterImageDecay' ),
+			};
+
+			const hdrMediaQuery = window.matchMedia( '(dynamic-range: high)' );
+
+			function updateHDRWarning() {
+
+				const displayIsHDR = hdrMediaQuery.matches;
+				document.querySelector( '#no-hdr' ).style.display = displayIsHDR ? 'none' : '';
+
+			}
+
+			hdrMediaQuery.addEventListener( 'change', updateHDRWarning );
+			updateHDRWarning();
+
+			if ( WebGPU.isAvailable() === false ) {
+
+				document.body.appendChild( WebGPU.getErrorMessage() );
+				throw new Error( 'No WebGPU support' );
+
+			}
+
+			// Enable Extended sRGB output color space for HDR presentation
+			THREE.ColorManagement.define( { [ ExtendedSRGBColorSpace ]: ExtendedSRGBColorSpaceImpl } );
+
+			// Renderer (HalfFloat output + Extended sRGB)
+			const renderer = new THREE.WebGPURenderer( {
+				antialias: true,
+				outputType: THREE.HalfFloatType,
+			} );
+
+			renderer.outputColorSpace = ExtendedSRGBColorSpace;
+			renderer.setPixelRatio( window.devicePixelRatio );
+			renderer.setSize( window.innerWidth, window.innerHeight );
+			document.body.appendChild( renderer.domElement );
+
+			const camera = new THREE.OrthographicCamera( 0, window.innerWidth, window.innerHeight, 0, 1, 2 );
+			camera.position.z = 1;
+
+			// Brush scene (rendered into drawTarget)
+			const brushScene = new THREE.Scene();
+
+			brushScene.background = new THREE.Color( 0xffffff );
+			const brushMat = new THREE.MeshBasicNodeMaterial();
+			brushMat.transparent = true;
+			brushMat.depthTest = false;
+			brushMat.depthWrite = false;
+			brushMat.blending = THREE.AdditiveBlending; // additive to build HDR energy
+
+			const postProcessing = new THREE.PostProcessing( renderer );
+			const brushPass = pass( brushScene, camera, { type: THREE.HalfFloatType } );
+			brushPass.renderTarget.texture.colorSpace = ExtendedSRGBColorSpace;
+
+			postProcessing.outputNode = afterImage( brushPass, params.afterImageDecay );
+
+			// HDR brush uniforms
+			const uColor = params.intensity;
+			const uHard = params.hardness;
+			const uRadius = params.radius;
+
+			// Radial falloff in TSL
+			const d = uv().sub( 0.5 ).length();
+			const t = d.div( uRadius );
+			const a = t.clamp().oneMinus().pow( uHard.mul( 8.0 ).add( 1.0 ) );
+
+			brushMat.colorNode = uColor.mul( a );
+			brushMat.opacityNode = a; // premultiplied style with additive blending
+
+			const brushMesh = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1 ), brushMat );
+			brushMesh.scale.set( 300, 300, 1 ); // ~300px default brush size
+			brushScene.add( brushMesh );
+
+			function onPointerMove( e ) {
+
+				const rect = renderer.domElement.getBoundingClientRect();
+				const x = e.clientX - rect.left;
+				const y = e.clientY - rect.top;
+
+				// camera has origin at bottom-left (0,0)
+				brushMesh.position.set( x, window.innerHeight - y, 0 );
+
+			}
+
+			window.addEventListener( 'pointermove', onPointerMove, { passive: false } );
+
+			// Prevent mobile scroll on touch
+			renderer.domElement.addEventListener( 'touchstart', ( e ) => e.preventDefault(), { passive: false } );
+			renderer.domElement.addEventListener( 'touchmove', ( e ) => e.preventDefault(), { passive: false } );
+			renderer.domElement.addEventListener( 'touchend', ( e ) => e.preventDefault(), { passive: false } );
+
+			// GUI setup
+			const gui = new GUI();
+
+			const colorFolder = gui.addFolder( 'HDR' );
+			colorFolder.add( params.intensity, 'value', 0, 10, 0.1 ).name( 'Intensity' );
+			colorFolder.open();
+
+			const brushFolder = gui.addFolder( 'Brush Settings' );
+			brushFolder.add( params.hardness, 'value', 0, 0.99, 0.01 ).name( 'Hardness' );
+			brushFolder.add( params.radius, 'value', 0.1, 2.0, 0.01 ).name( 'Radius' );
+			brushFolder.open();
+
+			const effectFolder = gui.addFolder( 'Effects' );
+			effectFolder
+				.add( params.afterImageDecay, 'value', 0.9, 0.999, 0.001 )
+				.name( 'After Image Decay' );
+			effectFolder.open();
+
+			gui.open();
+
+			// Resize handling
+			function onResize() {
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				camera.right = window.innerWidth;
+				camera.top = window.innerHeight;
+				camera.updateProjectionMatrix();
+
+
+			}
+
+			window.addEventListener( 'resize', onResize );
+
+			// Main loop
+			renderer.setAnimationLoop( async () => {
+
+				postProcessing.render();
+
+			} );
+
+		</script>
+	</body>
+</html>

+ 5 - 5
examples/webgpu_postprocessing_afterimage.html

@@ -27,7 +27,7 @@
 		<script type="module">
 
 			import * as THREE from 'three/webgpu';
-			import { instancedBufferAttribute, mod, pass, texture, float, time, vec2, vec3, vec4, sin, cos } from 'three/tsl';
+			import { instancedBufferAttribute, uniform, mod, pass, texture, float, time, vec2, vec3, vec4, sin, cos } from 'three/tsl';
 			import { afterImage } from 'three/addons/tsl/display/AfterImageNode.js';
 
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
@@ -38,7 +38,7 @@
 
 			const params = {
 
-				damp: 0.8,
+				damp: uniform( 0.8, 'float' ).setName( 'damp' ),
 				enabled: true
 
 			};
@@ -81,7 +81,7 @@
 					colors.push( color.r, color.g, color.b );
 
 					timeOffsets.push( i / count );
-			
+
 				}
 
 				const positionAttribute = new THREE.InstancedBufferAttribute( new Float32Array( vertices ), 3 );
@@ -158,13 +158,13 @@
 
 				const angle = Math.random() * Math.PI * 2;
 				const u = Math.random() * 2 - 1;
-			
+
 				v.set(
 					Math.cos( angle ) * Math.sqrt( 1 - Math.pow( u, 2 ) ) * r,
 					Math.sin( angle ) * Math.sqrt( 1 - Math.pow( u, 2 ) ) * r,
 					u * r
 				);
-			
+
 				return v;
 
 			}

+ 1 - 0
test/e2e/puppeteer.js

@@ -143,6 +143,7 @@ const exceptionList = [
 	'webgpu_compute_sort_bitonic',
 	'webgpu_compute_reduce',
 	'webgpu_struct_drawindirect',
+	'webgpu_hdr',
 
 	// WebGPURenderer: Unknown problem
 	'webgpu_backdrop_water',

粤ICP备19079148号