Browse Source

WebGPU: Compute Rain (#27053)

* compute rain

* fix title

* Update puppeteer.js

* fix for 120Hz or differents frame rate screen

* forceSinglePass for optimization

* cleanup

* improved random

* cleanup

* ripple pivot offset

* update

* TODO: Fix .castShadow and remove it

* cleanup

* update

* update initial camera
sunag 1 year ago
parent
commit
9b302a919b

+ 1 - 0
examples/files.json

@@ -315,6 +315,7 @@
 		"webgpu_clearcoat",
 		"webgpu_compute_audio",
 		"webgpu_compute_particles",
+		"webgpu_compute_particles_rain",
 		"webgpu_compute_points",
 		"webgpu_compute_texture",
 		"webgpu_compute_texture_pingpong",

BIN
examples/screenshots/webgpu_compute_particles_rain.jpg


+ 399 - 0
examples/webgpu_compute_particles_rain.html

@@ -0,0 +1,399 @@
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Compute Particles Rain</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 - GPU Compute Rain
+		</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 { tslFn, texture, uv, uint, positionWorld, modelWorldMatrix, cameraViewMatrix, timerLocal, timerDelta, cameraProjectionMatrix, vec2, instanceIndex, positionGeometry, storage, MeshBasicNodeMaterial, If } from 'three/nodes';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
+
+			const maxParticleCount = 50000;
+			const instanceCount = maxParticleCount / 2;
+
+			let camera, scene, renderer;
+			let controls, stats;
+			let computeParticles;
+			let monkey;
+			let clock;
+
+			let collisionBox, collisionCamera, collisionPosRT, collisionPosMaterial;
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				const { innerWidth, innerHeight } = window;
+
+				camera = new THREE.PerspectiveCamera( 60, innerWidth / innerHeight, .1, 110 );
+				camera.layers.enable( 2 ); // @TODO: Fix .castShadow and remove it
+				camera.position.set( 40, 8, 0 );
+				camera.lookAt( 0, 0, 0 );
+
+				scene = new THREE.Scene();
+
+				const dirLight = new THREE.DirectionalLight( 0xffffff, .5 );
+				dirLight.castShadow = true;
+				dirLight.position.set( 3, 12, 17 );
+				dirLight.castShadow = true;
+				dirLight.shadow.camera.near = 5;
+				dirLight.shadow.camera.far = 50;
+				dirLight.shadow.camera.right = 10;
+				dirLight.shadow.camera.left = - 10;
+				dirLight.shadow.camera.top	= 10;
+				dirLight.shadow.camera.bottom = - 10;
+				dirLight.shadow.mapSize.width = 2048;
+				dirLight.shadow.mapSize.height = 2048;
+				dirLight.shadow.bias = - 0.01;
+
+				scene.add( dirLight );
+				scene.add( new THREE.AmbientLight( 0x111111 ) );
+
+				//
+
+				collisionCamera = new THREE.OrthographicCamera( - 50, 50, 50, - 50, .1, 50 );
+				collisionCamera.position.y = 50;
+				collisionCamera.lookAt( 0, 0, 0 );
+				collisionCamera.layers.disableAll();
+				collisionCamera.layers.enable( 1 );
+
+				collisionPosRT = new THREE.RenderTarget( 1024, 1024 );
+				collisionPosRT.texture.type = THREE.HalfFloatType;
+
+				collisionPosMaterial = new MeshBasicNodeMaterial();
+				collisionPosMaterial.colorNode = positionWorld;
+
+				//
+
+				const createBuffer = ( type = 'vec3' ) => storage( new THREE.InstancedBufferAttribute( new Float32Array( maxParticleCount * 4 ), 4 ), type, maxParticleCount );
+
+				const positionBuffer = createBuffer();
+				const velocityBuffer = createBuffer();
+				const ripplePositionBuffer = createBuffer();
+				const rippleTimeBuffer = createBuffer();
+
+				// compute
+
+				const timer = timerLocal();
+
+				const randUint = () => uint( Math.random() * 0xFFFFFF );
+
+				const computeInit = tslFn( () => {
+
+					const position = positionBuffer.element( instanceIndex );
+					const velocity = velocityBuffer.element( instanceIndex );
+					const rippleTime = rippleTimeBuffer.element( instanceIndex );
+
+					const randX = instanceIndex.hash();
+					const randY = instanceIndex.add( randUint() ).hash();
+					const randZ = instanceIndex.add( randUint() ).hash();
+
+					position.x = randX.mul( 100 ).add( - 50 );
+					position.y = randY.mul( 25 );
+					position.z = randZ.mul( 100 ).add( - 50 );
+
+					velocity.y = randX.mul( - .04 ).add( - .2 );
+
+					rippleTime.x = 1000;
+
+				} )().compute( maxParticleCount );
+
+				//
+
+				const computeUpdate = tslFn( () => {
+
+					const getCoord = ( pos ) => pos.add( 50 ).div( 100 );
+
+					const position = positionBuffer.element( instanceIndex );
+					const velocity = velocityBuffer.element( instanceIndex );
+					const ripplePosition = ripplePositionBuffer.element( instanceIndex );
+					const rippleTime = rippleTimeBuffer.element( instanceIndex );
+
+					position.addAssign( velocity );
+
+					rippleTime.x = rippleTime.x.add( timerDelta().mul( 4 ) );
+
+					//
+
+					const collisionArea = texture( collisionPosRT.texture, getCoord( position.xz ) );
+
+					const surfaceOffset = .05;
+
+					const floorPosition = collisionArea.y.add( surfaceOffset );
+
+					// floor
+
+					const ripplePivotOffsetY = - .9;
+
+					If( position.y.add( ripplePivotOffsetY ).lessThan( floorPosition ), () => {
+
+						position.y = 25;
+
+						ripplePosition.x = position.x;
+						ripplePosition.y = floorPosition;
+						ripplePosition.z = position.z;
+
+						// reset hit time: x = time
+
+						rippleTime.x = 1;
+
+						// next drops will not fall in the same place
+
+						position.x = instanceIndex.add( timer ).hash().mul( 100 ).add( - 50 );
+						position.z = instanceIndex.add( timer.add( randUint() ) ).hash().mul( 100 ).add( - 50 );
+
+					} );
+
+					const rippleOnSurface = texture( collisionPosRT.texture, getCoord( ripplePosition.xz ) );
+
+					const rippleFloorArea = rippleOnSurface.y.add( surfaceOffset );
+
+					If( ripplePosition.y.greaterThan( rippleFloorArea ), () => {
+
+						rippleTime.x = 1000;
+
+					} );
+
+				} );
+
+				computeParticles = computeUpdate().compute( maxParticleCount );
+
+				// rain
+
+				const billboarding = tslFn( () => {
+
+					const particlePosition = positionBuffer.toAttribute();
+
+					const worldMatrix = modelWorldMatrix.toVar();
+					worldMatrix[ 3 ][ 0 ] = particlePosition.x;
+					worldMatrix[ 3 ][ 1 ] = particlePosition.y;
+					worldMatrix[ 3 ][ 2 ] = particlePosition.z;
+
+					const modelViewMatrix = cameraViewMatrix.mul( worldMatrix );
+					modelViewMatrix[ 0 ][ 0 ] = 1;
+					modelViewMatrix[ 0 ][ 1 ] = 0;
+					modelViewMatrix[ 0 ][ 2 ] = 0;
+
+					//modelViewMatrix[ 0 ][ 0 ] = modelWorldMatrix[ 0 ].length();
+					//modelViewMatrix[ 1 ][ 1 ] = modelWorldMatrix[ 1 ].length();
+
+					modelViewMatrix[ 2 ][ 0 ] = 0;
+					modelViewMatrix[ 2 ][ 1 ] = 0;
+					modelViewMatrix[ 2 ][ 2 ] = 1;
+
+					return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionGeometry );
+
+				} );
+
+				const rainMaterial = new MeshBasicNodeMaterial();
+				rainMaterial.colorNode = uv().distance( vec2( .5, 0 ) ).oneMinus().mul( 3 ).exp().mul( .1 );
+				rainMaterial.vertexNode = billboarding();
+				rainMaterial.opacity = .2;
+				rainMaterial.side = THREE.DoubleSide;
+				rainMaterial.forceSinglePass = true;
+				rainMaterial.depthWrite = false;
+				rainMaterial.depthTest = true;
+				rainMaterial.transparent = true;
+
+				const rainParticles = new THREE.Mesh( new THREE.PlaneGeometry( .1, 2 ), rainMaterial );
+				rainParticles.isInstancedMesh = true;
+				rainParticles.count = instanceCount;
+				rainParticles.layers.disableAll();
+				rainParticles.layers.enable( 2 );
+				scene.add( rainParticles );
+
+				// ripple
+
+				const rippleTime = rippleTimeBuffer.element( instanceIndex ).x;
+
+				const rippleEffect = tslFn( () => {
+
+					const center = uv().add( vec2( - .5 ) ).length().mul( 7 );
+					const distance = rippleTime.sub( center );
+
+					return distance.min( 1 ).sub( distance.max( 1 ).sub( 1 ) );
+
+				} );
+
+				const rippleMaterial = new MeshBasicNodeMaterial();
+				rippleMaterial.colorNode = rippleEffect();
+				rippleMaterial.positionNode = positionGeometry.add( ripplePositionBuffer.toAttribute() );
+				rippleMaterial.opacityNode = rippleTime.mul( .3 ).oneMinus().max( 0 ).mul( .5 );
+				rippleMaterial.side = THREE.DoubleSide;
+				rippleMaterial.forceSinglePass = true;
+				rippleMaterial.depthWrite = false;
+				rippleMaterial.depthTest = true;
+				rippleMaterial.transparent = true;
+
+				// ripple geometry
+
+				const surfaceRippleGeometry = new THREE.PlaneGeometry( 2.5, 2.5 );
+				surfaceRippleGeometry.rotateX( - Math.PI / 2 );
+
+				const xRippleGeometry = new THREE.PlaneGeometry( 1, 2 );
+				xRippleGeometry.rotateY( - Math.PI / 2 );
+
+				const zRippleGeometry = new THREE.PlaneGeometry( 1, 2 );
+
+				const rippleGeometry = BufferGeometryUtils.mergeGeometries( [ surfaceRippleGeometry, xRippleGeometry, zRippleGeometry ] );
+
+				const rippleParticles = new THREE.Mesh( rippleGeometry, rippleMaterial );
+				rippleParticles.isInstancedMesh = true;
+				rippleParticles.count = instanceCount;
+				rippleParticles.layers.disableAll();
+				rippleParticles.layers.enable( 2 );
+				scene.add( rippleParticles );
+
+				// floor geometry
+
+				const floorGeometry = new THREE.PlaneGeometry( 1000, 1000 );
+				floorGeometry.rotateX( - Math.PI / 2 );
+
+				const plane = new THREE.Mesh( floorGeometry, new THREE.MeshBasicMaterial( { color: 0x050505 } ) );
+				scene.add( plane );
+
+				//
+
+				collisionBox = new THREE.Mesh( new THREE.BoxGeometry( 30, 1, 15 ), new THREE.MeshStandardMaterial() );
+				collisionBox.material.color.set( 0x333333 );
+				collisionBox.position.y = 12;
+				collisionBox.scale.x = 3.5;
+				collisionBox.layers.enable( 1 );
+				collisionBox.castShadow = true;
+				scene.add( collisionBox );
+
+				//
+
+				const loader = new THREE.BufferGeometryLoader();
+				loader.load( 'models/json/suzanne_buffergeometry.json', function ( geometry ) {
+
+					geometry.computeVertexNormals();
+
+					monkey = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial( { roughness: 1, metalness: 0 } ) );
+					monkey.receiveShadow = true;
+					monkey.scale.setScalar( 5 );
+					monkey.rotation.y = Math.PI / 2;
+					monkey.position.y = 4.5;
+					monkey.layers.enable( 1 ); // add to collision layer
+
+					scene.add( monkey );
+
+				} );
+
+				//
+
+				clock = new THREE.Clock();
+
+				//
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				document.body.appendChild( renderer.domElement );
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				//
+
+				renderer.compute( computeInit );
+
+				//
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 5;
+				controls.maxDistance = 50;
+				controls.update();
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// gui
+
+				const gui = new GUI();
+
+				gui.add( collisionBox.position, 'z', - 50, 50, .001 ).name( 'position' );
+				gui.add( collisionBox.scale, 'x', .1, 3.5, 0.01 ).name( 'scale' );
+				gui.add( rainParticles, 'count', 200, maxParticleCount, 1 ).name( 'drop count' ).onChange( ( v ) => rippleParticles.count = v );
+
+			}
+
+			function onWindowResize() {
+
+				const { innerWidth, innerHeight } = window;
+
+				camera.aspect = innerWidth / innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( innerWidth, innerHeight );
+
+			}
+
+			function animate() {
+
+				stats.update();
+
+				if ( monkey ) {
+
+					monkey.rotation.y += clock.getDelta();
+
+				}
+
+				// position
+
+				scene.overrideMaterial = collisionPosMaterial;
+				renderer.setRenderTarget( collisionPosRT );
+				renderer.render( scene, collisionCamera );
+
+				// compute
+
+				renderer.compute( computeParticles );
+
+				// result
+
+				scene.overrideMaterial = null;
+				renderer.setRenderTarget( null );
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -112,6 +112,7 @@ const exceptionList = [
 	'webgpu_clearcoat',
 	'webgpu_compute_audio',
 	'webgpu_compute_particles',
+	'webgpu_compute_particles_rain',
 	'webgpu_compute_points',
 	'webgpu_compute_texture',
 	'webgpu_compute_texture_pingpong',