Browse Source

WebGPURenderer: Particles example (#24247)

* reuse buffer used in others mesh/sprite

* update geometry if was disposed on renderer process

* add initial value for timer

* Add RotateUVNode

* SpriteNodeMaterial: move .positionNode to work like particles

* WebGPURenderer: fix AdditiveBlending

* WebGPU: particles example

* cleanup

* cleanup

* fix title

* cleanup

* cleanup
sunag 3 years ago
parent
commit
ec597f68a7

+ 1 - 0
examples/files.json

@@ -319,6 +319,7 @@
 		"webgpu_loader_gltf",
 		"webgpu_materials",
 		"webgpu_nodes_playground",
+		"webgpu_particles",
 		"webgpu_rtt",
 		"webgpu_sandbox",
 		"webgpu_skinning",

+ 3 - 0
examples/jsm/nodes/Nodes.js

@@ -80,6 +80,7 @@ import JoinNode from './utils/JoinNode.js';
 import MatcapUVNode from './utils/MatcapUVNode.js';
 import MaxMipLevelNode from './utils/MaxMipLevelNode.js';
 import OscNode from './utils/OscNode.js';
+import RotateUVNode from './utils/RotateUVNode.js'
 import SplitNode from './utils/SplitNode.js';
 import SpriteSheetUVNode from './utils/SpriteSheetUVNode.js';
 import TimerNode from './utils/TimerNode.js';
@@ -188,6 +189,7 @@ const nodeLib = {
 	MatcapUVNode,
 	MaxMipLevelNode,
 	OscNode,
+	RotateUVNode,
 	SplitNode,
 	SpriteSheetUVNode,
 	TimerNode,
@@ -295,6 +297,7 @@ export {
 	MatcapUVNode,
 	MaxMipLevelNode,
 	OscNode,
+	RotateUVNode,
 	SplitNode,
 	SpriteSheetUVNode,
 	TimerNode,

+ 29 - 21
examples/jsm/nodes/geometry/RangeNode.js

@@ -39,50 +39,58 @@ class RangeNode extends Node {
 		const { min, max } = this;
 		const { object, geometry } = builder;
 
-		const vectorLength = this.getVectorLength();
-		const attributeName = 'node' + this.id;
-
 		let output = null;
 
 		if ( object.isInstancedMesh === true ) {
 
+			const vectorLength = this.getVectorLength();
+			const attributeName = 'node' + this.id;
+
 			const length = vectorLength * object.count;
 			const array = new Float32Array( length );
 
-			if ( vectorLength === 1 ) {
+			const attributeGeometry = geometry.getAttribute( attributeName );
 
-				for ( let i = 0; i < length; i ++ ) {
+			if ( attributeGeometry === undefined || attributeGeometry.array.length < length ) {
 
-					array[ i ] = MathUtils.lerp( min, max, Math.random() );
+				if ( vectorLength === 1 ) {
 
-				}
+					for ( let i = 0; i < length; i ++ ) {
 
-			} else if ( min.isColor ) {
+						array[ i ] = MathUtils.lerp( min, max, Math.random() );
 
-				for ( let i = 0; i < length; i += 3 ) {
+					}
 
-					array[ i ] = MathUtils.lerp( min.r, max.r, Math.random() );
-					array[ i + 1 ] = MathUtils.lerp( min.g, max.g, Math.random() );
-					array[ i + 2 ] = MathUtils.lerp( min.b, max.b, Math.random() );
+				} else if ( min.isColor ) {
 
-				}
+					for ( let i = 0; i < length; i += 3 ) {
+
+						array[ i ] = MathUtils.lerp( min.r, max.r, Math.random() );
+						array[ i + 1 ] = MathUtils.lerp( min.g, max.g, Math.random() );
+						array[ i + 2 ] = MathUtils.lerp( min.b, max.b, Math.random() );
 
-			} else {
+					}
 
-				for ( let i = 0; i < length; i ++ ) {
+				} else {
 
-					const index = i % vectorLength;
+					for ( let i = 0; i < length; i ++ ) {
 
-					const minValue = min.getComponent( index );
-					const maxValue = max.getComponent( index );
+						const index = i % vectorLength;
 
-					array[ i ] = MathUtils.lerp( minValue, maxValue, Math.random() );
+						const minValue = min.getComponent( index );
+						const maxValue = max.getComponent( index );
+
+						array[ i ] = MathUtils.lerp( minValue, maxValue, Math.random() );
+
+					}
 
 				}
 
-			}
+				geometry.setAttribute( attributeName, new InstancedBufferAttribute( array, vectorLength ) );
 
-			geometry.setAttribute( attributeName, new InstancedBufferAttribute( array, vectorLength ) );
+				geometry.dispose();
+
+			}
 
 			output = attribute( attributeName, builder.getTypeFromLength( vectorLength ) );
 

+ 4 - 10
examples/jsm/nodes/materials/SpriteNodeMaterial.js

@@ -2,8 +2,8 @@ import NodeMaterial from './NodeMaterial.js';
 import { SpriteMaterial } from 'three';
 import {
 	vec2, vec3, vec4,
-	assign, add, mul, sub,
-	positionLocal, bypass, length, cos, sin, uniform,
+	uniform, add, mul, sub,
+	positionLocal, length, cos, sin,
 	modelViewMatrix, cameraProjectionMatrix, modelWorldMatrix, materialRotation
 } from '../shadernode/ShaderNodeElements.js';
 
@@ -42,15 +42,9 @@ class SpriteNodeMaterial extends NodeMaterial {
 
 		const { positionNode, rotationNode, scaleNode } = this;
 
-		let vertex = positionLocal;
+		const vertex = positionLocal;
 
-		if ( positionNode !== null ) {
-
-			vertex = bypass( vertex, assign( positionLocal, positionNode ) );
-
-		}
-
-		let mvPosition = mul( modelViewMatrix, vec4( 0, 0, 0, 1 ) );
+		let mvPosition = mul( modelViewMatrix, positionNode ? vec4( positionNode.xyz, 1 ) : vec4( 0, 0, 0, 1 ) );
 
 		let scale = vec2(
 			length( vec3( modelWorldMatrix[ 0 ].x, modelWorldMatrix[ 0 ].y, modelWorldMatrix[ 0 ].z ) ),

+ 6 - 3
examples/jsm/nodes/shadernode/ShaderNodeElements.js

@@ -19,6 +19,7 @@ import LightingContextNode from '../lighting/LightingContextNode.js';
 import MatcapUVNode from '../utils/MatcapUVNode.js';
 import MaxMipLevelNode from '../utils/MaxMipLevelNode.js';
 import OscNode from '../utils/OscNode.js';
+import RotateUVNode from '../utils/RotateUVNode.js';
 import SpriteSheetUVNode from '../utils/SpriteSheetUVNode.js';
 import TimerNode from '../utils/TimerNode.js';
 
@@ -96,12 +97,14 @@ export const oscSquare = nodeProxy( OscNode, OscNode.SQUARE );
 export const oscTriangle = nodeProxy( OscNode, OscNode.TRIANGLE );
 export const oscSawtooth = nodeProxy( OscNode, OscNode.SAWTOOTH );
 
+export const rotateUV = nodeProxy( RotateUVNode );
+
 export const spritesheetUV = nodeProxy( SpriteSheetUVNode );
 
 // @TODO: add supports to use node in timeScale
-export const timerLocal = ( timeScale ) => nodeObject( new TimerNode( TimerNode.LOCAL, timeScale ) );
-export const timerGlobal = ( timeScale ) => nodeObject( new TimerNode( TimerNode.GLOBAL, timeScale ) );
-export const timerDelta = ( timeScale ) => nodeObject( new TimerNode( TimerNode.DELTA, timeScale ) );
+export const timerLocal = ( timeScale, value = 0 ) => nodeObject( new TimerNode( TimerNode.LOCAL, timeScale, value ) );
+export const timerGlobal = ( timeScale, value = 0 ) => nodeObject( new TimerNode( TimerNode.GLOBAL, timeScale, value ) );
+export const timerDelta = ( timeScale, value = 0 ) => nodeObject( new TimerNode( TimerNode.DELTA, timeScale, value ) );
 
 // geometry
 

+ 32 - 0
examples/jsm/nodes/utils/RotateUVNode.js

@@ -0,0 +1,32 @@
+import TempNode from '../core/TempNode.js';
+import { vec2, add, sub, mul, cos, sin } from '../shadernode/ShaderNodeBaseElements.js';
+
+class RotateUVNode extends TempNode {
+
+	constructor( uvNode, rotationNode, centerNode = vec2( .5 ) ) {
+
+		super( 'vec2' );
+
+		this.uvNode = uvNode;
+		this.rotationNode = rotationNode;
+		this.centerNode = centerNode;
+
+	}
+
+	construct() {
+
+		const { uvNode, rotationNode, centerNode } = this;
+
+		const cosAngle = cos( rotationNode );
+		const sinAngle = sin( rotationNode );
+
+		return vec2(
+			add( add( mul( cosAngle, sub( uvNode.x, centerNode.x ) ), mul( sinAngle, sub( uvNode.y, centerNode.y ) ) ), centerNode.x ),
+			add( sub( mul( cosAngle, sub( uvNode.y, centerNode.y ) ), mul( sinAngle, sub( uvNode.x, centerNode.x ) ) ), centerNode.y )
+		);
+
+	}
+
+}
+
+export default RotateUVNode;

+ 2 - 2
examples/jsm/nodes/utils/TimerNode.js

@@ -7,9 +7,9 @@ class TimerNode extends UniformNode {
 	static GLOBAL = 'global';
 	static DELTA = 'delta';
 
-	constructor( scope = TimerNode.LOCAL, scale = 1 ) {
+	constructor( scope = TimerNode.LOCAL, scale = 1, value = 0 ) {
 
-		super( 0 );
+		super( value );
 
 		this.scope = scope;
 		this.scale = scale;

+ 6 - 0
examples/jsm/renderers/webgpu/WebGPUGeometries.js

@@ -9,6 +9,12 @@ class WebGPUGeometries {
 
 	}
 
+	has( geometry ) {
+
+		return this.geometries.has( geometry );
+
+	}
+
 	update( geometry ) {
 
 		if ( this.geometries.has( geometry ) === false ) {

+ 1 - 1
examples/jsm/renderers/webgpu/WebGPUObjects.js

@@ -15,7 +15,7 @@ class WebGPUObjects {
 		const updateMap = this.updateMap;
 		const frame = this.info.render.frame;
 
-		if ( updateMap.get( geometry ) !== frame ) {
+		if ( this.geometries.has( geometry ) === false || updateMap.get( geometry ) !== frame ) {
 
 			this.geometries.update( geometry );
 

+ 9 - 2
examples/jsm/renderers/webgpu/WebGPURenderPipeline.js

@@ -144,7 +144,13 @@ class WebGPURenderPipeline {
 				break;
 
 			case AdditiveBlending:
-				// no alphaBlend settings
+
+				alphaBlend = {
+					srcFactor: GPUBlendFactor.Zero,
+					dstFactor: GPUBlendFactor.One,
+					operation: GPUBlendOperation.Add
+				};
+
 				break;
 
 			case SubtractiveBlending:
@@ -162,6 +168,7 @@ class WebGPURenderPipeline {
 				break;
 
 			case MultiplyBlending:
+
 				if ( premultipliedAlpha === true ) {
 
 					alphaBlend = {
@@ -318,7 +325,6 @@ class WebGPURenderPipeline {
 		switch ( blending ) {
 
 			case NormalBlending:
-
 				colorBlend.srcFactor = ( premultipliedAlpha === true ) ? GPUBlendFactor.One : GPUBlendFactor.SrcAlpha;
 				colorBlend.dstFactor = GPUBlendFactor.OneMinusSrcAlpha;
 				colorBlend.operation = GPUBlendOperation.Add;
@@ -326,6 +332,7 @@ class WebGPURenderPipeline {
 
 			case AdditiveBlending:
 				colorBlend.srcFactor = ( premultipliedAlpha === true ) ? GPUBlendFactor.One : GPUBlendFactor.SrcAlpha;
+				colorBlend.dstFactor = GPUBlendFactor.One;
 				colorBlend.operation = GPUBlendOperation.Add;
 				break;
 

BIN
examples/screenshots/webgpu_particles.jpg


BIN
examples/textures/opengameart/smoke1.png


+ 190 - 0
examples/webgpu_particles.html

@@ -0,0 +1,190 @@
+<html lang="en">
+	<head>
+		<title>three.js - WebGPU - Particles</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 - Particles
+		</div>
+
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three-nodes/": "./jsm/nodes/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import * as Nodes from 'three-nodes/Nodes.js';
+
+			import { GUI } from './jsm/libs/lil-gui.module.min.js';
+
+			import { range, texture, mix, uv, mul, mod, rotateUV, color, max, min, div, saturate, positionWorld, invert, timerLocal } from 'three-nodes/Nodes.js';
+
+			import WebGPU from './jsm/capabilities/WebGPU.js';
+			import WebGPURenderer from './jsm/renderers/webgpu/WebGPURenderer.js';
+
+			import { OrbitControls } from './jsm/controls/OrbitControls.js';
+
+			let camera, scene, renderer;
+			let controls;
+
+			init().then( animate ).catch( error );
+
+			async 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, 5000 );
+				camera.position.set( 1300, 500, 0 );
+
+				scene = new THREE.Scene();
+				//scene.fogNode = new Nodes.FogRangeNode( Nodes.color( 0x0000ff ), Nodes.float( 1500 ), Nodes.float( 2100 )  );
+
+				// textures
+
+				const textureLoader = new THREE.TextureLoader();
+				const map = textureLoader.load( 'textures/opengameart/smoke1.png' );
+
+				// create nodes
+
+				const lifeRange = range( .1, 1 );
+				const offsetRange = range( new THREE.Vector3( - 2, 3, - 2 ), new THREE.Vector3( 2, 5, 2 ) );
+
+				const timer = timerLocal( .2, 1/*100000*/ ); // @TODO: need to work with 64-bit precision
+
+				const lifeTime = mod( mul( timer, lifeRange ), 1 );
+				const scaleRange = range( .3, 2 );
+				const rotateRange = range( .1, 4 );
+
+				const life = div( lifeTime, lifeRange );
+
+				const fakeLightEffect = max( .2, invert( positionWorld.y ) );
+
+				const textureNode = texture( map, rotateUV( uv(), mul( timer, rotateRange ) ) );
+
+				const opacityNode = mul( textureNode.a, invert( life ) );
+
+				const smokeColor = mix( color( 0x2c1501 ), color( 0x222222 ), saturate( mul( positionWorld.y, 3 ) ) );
+
+				// create particles
+
+				const smokeNodeMaterial = new Nodes.SpriteNodeMaterial();
+				smokeNodeMaterial.colorNode = mul( mix( color( 0xf27d0c ), smokeColor, min( mul( life, 2.5 ), 1 ) ), fakeLightEffect );
+				smokeNodeMaterial.opacityNode = opacityNode;
+				smokeNodeMaterial.positionNode = mul( offsetRange, lifeTime );
+				smokeNodeMaterial.scaleNode = mul( scaleRange, max( .3, lifeTime ) );
+				smokeNodeMaterial.depthWrite = false;
+				smokeNodeMaterial.transparent = true;
+
+				const smokeInstancedSprite = new THREE.Sprite( smokeNodeMaterial );
+				smokeInstancedSprite.scale.setScalar( 400 );
+				smokeInstancedSprite.isInstancedMesh = true;
+				smokeInstancedSprite.count = 2000;
+				scene.add( smokeInstancedSprite );
+
+				//
+
+				const fireNodeMaterial = new Nodes.SpriteNodeMaterial();
+				fireNodeMaterial.colorNode = mix( color( 0xb72f17 ), color( 0xb72f17 ), life );
+				fireNodeMaterial.positionNode = mul( range( new THREE.Vector3( - 1, 1, - 1 ), new THREE.Vector3( 1, 2, 1 ) ), lifeTime );
+				fireNodeMaterial.scaleNode = smokeNodeMaterial.scaleNode;
+				fireNodeMaterial.opacityNode = opacityNode;
+				fireNodeMaterial.blending = THREE.AdditiveBlending;
+				fireNodeMaterial.transparent = true;
+				fireNodeMaterial.depthWrite = false;
+
+				const fireInstancedSprite = new THREE.Sprite( fireNodeMaterial );
+				fireInstancedSprite.scale.setScalar( 400 );
+				fireInstancedSprite.isInstancedMesh = true;
+				fireInstancedSprite.count = 100;
+				fireInstancedSprite.position.y = - 100;
+				fireInstancedSprite.renderOrder = 1;
+				scene.add( fireInstancedSprite );
+
+				//
+
+				const helper = new THREE.GridHelper( 3000, 40, 0x303030, 0x303030 );
+				helper.material.colorNode = new Nodes.AttributeNode( 'color' );
+				helper.position.y = - 75;
+				scene.add( helper );
+
+				//
+
+				renderer = new WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				//
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.maxDistance = 2700;
+				controls.target.set( 0, 500, 0 );
+				controls.update();
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// gui
+
+				const gui = new GUI();
+
+				gui.add( timer, 'scale', 0, 1, 0.01 ).name( 'speed' );
+
+				return renderer.init();
+
+			}
+
+			function onWindowResize() {
+
+				const { innerWidth, innerHeight } = window;
+
+				camera.aspect = innerWidth / innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( innerWidth, innerHeight );
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+				render();
+
+			}
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+			function error( error ) {
+
+				console.error( error );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -54,6 +54,7 @@ const exceptionList = [
 	'webgpu_loader_gltf',
 	'webgpu_materials',
 	'webgpu_nodes_playground',
+	'webgpu_particles',
 	'webgpu_rtt',
 	'webgpu_sandbox',
 	'webgpu_skinning_instancing',