Bläddra i källkod

NodeMaterial: Particles (#22538)

* cleanup

* new nodes

* PointsNodeMaterial: add sizeNode property

* WebGLNodeBuilder: positionNode and sizeNode support

* add webgl_points_nodes example

* new initial position

* update title

* mix to lerpPosition

* cleanup

* turn a bit more easy the example

* turn a bit more easy the example (2)

* cleanup

* fix property name
sunag 3 år sedan
förälder
incheckning
68b5f4f0cc

+ 1 - 0
examples/files.json

@@ -235,6 +235,7 @@
 		"webgl_materials_standard_nodes",
 		"webgl_mirror_nodes",
 		"webgl_performance_nodes",
+		"webgl_points_nodes",
 		"webgl_postprocessing_nodes",
 		"webgl_postprocessing_nodes_pass",
 		"webgl_sprites_nodes"

+ 24 - 0
examples/jsm/renderers/nodes/accessors/PointUVNode.js

@@ -0,0 +1,24 @@
+import Node from '../core/Node.js';
+
+class PointUVNode extends Node {
+
+	constructor() {
+
+		super( 'vec2' );
+
+		Object.defineProperty( this, 'isPointUVNode', { value: true } );
+
+	}
+
+	generate( builder, output ) {
+
+		const type = this.getType( builder );
+		const snippet = 'vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y )';
+
+		return builder.format( snippet, type, output );
+
+	}
+
+}
+
+export default PointUVNode;

+ 1 - 1
examples/jsm/renderers/nodes/accessors/PositionNode.js

@@ -24,7 +24,7 @@ class PositionNode extends Node {
 	generate( builder, output ) {
 
 		const type = this.getType( builder );
-		const nodeData = builder.getDataFromNode( this, builder.shaderStage );
+		const nodeData = builder.getDataFromNode( this );
 		const scope = this.scope;
 
 		let localPositionNode = nodeData.localPositionNode;

+ 4 - 0
examples/jsm/renderers/nodes/materials/PointsNodeMaterial.js

@@ -13,6 +13,8 @@ class PointsNodeMaterial extends PointsMaterial {
 
 		this.lightNode = null;
 
+		this.sizeNode = null;
+
 		this.positionNode = null;
 
 	}
@@ -26,6 +28,8 @@ class PointsNodeMaterial extends PointsMaterial {
 
 		this.lightNode = source.lightNode;
 
+		this.sizeNode = source.sizeNode;
+
 		this.positionNode = source.positionNode;
 
 		return super.copy( source );

+ 44 - 0
examples/jsm/renderers/nodes/utils/JoinNode.js

@@ -0,0 +1,44 @@
+import Node from '../core/Node.js';
+
+class JoinNode extends Node {
+
+	constructor( values = [] ) {
+
+		super();
+
+		this.values = values;
+
+	}
+
+	getType( builder ) {
+
+		return builder.getTypeFromLength( this.values.length );
+
+	}
+
+	generate( builder, output ) {
+
+		const type = this.getType( builder );
+		const values = this.values;
+
+		const snippetValues = [];
+
+		for ( let i = 0; i < values.length; i ++ ) {
+
+			const input = values[ i ];
+
+			const inputSnippet = input.build( builder, 'float' );
+
+			snippetValues.push( inputSnippet );
+
+		}
+
+		const snippet = `${type}( ${ snippetValues.join( ', ' ) } )`;
+
+		return builder.format( snippet, type, output );
+
+	}
+
+}
+
+export default JoinNode;

+ 69 - 0
examples/jsm/renderers/nodes/utils/SpriteSheetUVNode.js

@@ -0,0 +1,69 @@
+import Node from '../core/Node.js';
+import FloatNode from '../inputs/FloatNode.js';
+import UVNode from '../accessors/UVNode.js';
+import MathNode from '../math/MathNode.js';
+import OperatorNode from '../math/OperatorNode.js';
+import SplitNode from '../utils/SplitNode.js';
+import JoinNode from '../utils/JoinNode.js';
+
+class SpriteSheetUVNode extends Node {
+
+	constructor( count, uv = new UVNode() ) {
+
+		super( 'vec2' );
+
+		this.count = count;
+		this.uv = uv;
+		this.frame = new FloatNode( 0 ).setConst( true );
+
+	}
+
+	generate( builder, output ) {
+
+		const nodeData = builder.getDataFromNode( this );
+
+		let uvFrame = nodeData.uvFrame;
+
+		if ( nodeData.uvFrame === undefined ) {
+
+			const uv = this.uv;
+			const count = this.count;
+			const frame = this.frame;
+
+			const one = new FloatNode( 1 ).setConst( true );
+
+			const width = new SplitNode( count, 'x' );
+			const height = new SplitNode( count, 'y' );
+
+			const total = new OperatorNode( '*', width, height );
+
+			const roundFrame = new MathNode( MathNode.FLOOR, new MathNode( MathNode.MOD, frame, total ) );
+
+			const frameNum = new OperatorNode( '+', roundFrame, one );
+
+			const cell = new MathNode( MathNode.MOD, roundFrame, width );
+			const row = new MathNode( MathNode.CEIL, new OperatorNode( '/', frameNum, width ) );
+			const rowInv = new OperatorNode( '-', height, row );
+
+			const scale = new OperatorNode( '/', one, count );
+
+			const uvFrameOffset = new JoinNode( [
+				new OperatorNode( '*', cell, new SplitNode( scale, 'x' ) ),
+				new OperatorNode( '*', rowInv, new SplitNode( scale, 'y' ) )
+			] );
+
+			const uvScale = new OperatorNode( '*', uv, scale );
+
+			uvFrame = new OperatorNode( '+', uvScale, uvFrameOffset );
+
+			nodeData.uvFrame = uvFrame;
+
+		}
+
+		return uvFrame.build( builder, output );
+
+	}
+
+}
+
+export default SpriteSheetUVNode;

+ 28 - 7
examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js

@@ -94,6 +94,18 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 		}
 
+		if ( material.sizeNode && material.sizeNode.isNode ) {
+
+			this.addSlot( 'vertex', new NodeSlot( material.sizeNode, 'SIZE', 'float' ) );
+
+		}
+
+		if ( material.positionNode && material.positionNode.isNode ) {
+
+			this.addSlot( 'vertex', new NodeSlot( material.positionNode, 'POSITION', 'vec3' ) );
+
+		}
+
 	}
 
 	getTexture( textureProperty, uvSnippet, biasSnippet = null ) {
@@ -250,13 +262,6 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 	}
 
-	/*prependCode( code ) {
-
-		this.shader.vertexShader = code + this.shader.vertexShader;
-		this.shader.fragmentShader = code + this.shader.fragmentShader;
-
-	}*/
-
 	build() {
 
 		super.build();
@@ -346,6 +351,22 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 			#endif` );
 
+		this.addCodeAfterInclude( 'vertex', 'begin_vertex',
+			`#ifdef NODE_POSITION
+
+				NODE_CODE_POSITION
+				transformed = NODE_POSITION;
+
+			#endif` );
+
+		this.addCodeAfterSnippet( 'vertex', 'gl_PointSize = size;',
+			`#ifdef NODE_SIZE
+
+				NODE_CODE_SIZE
+				gl_PointSize = NODE_SIZE;
+
+			#endif` );
+
 		for ( const shaderStage of shaderStages ) {
 
 			this.addCodeAfterSnippet( shaderStage, 'main() {',

BIN
examples/screenshots/webgl_points_nodes.jpg


BIN
examples/textures/sprites/firetorch_1.jpg


+ 212 - 0
examples/webgl_points_nodes.html

@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - node 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> - webgl node particles example
+		</div>
+
+		<script type="importmap">
+		{
+			"imports": {
+				"three": "../build/three.module.js"
+			}
+		}
+		</script>
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import Stats from './jsm/libs/stats.module.js';
+
+			import { GUI } from './jsm/libs/dat.gui.module.js';
+
+			import { TeapotGeometry } from './jsm/geometries/TeapotGeometry.js';
+
+			import { OrbitControls } from './jsm/controls/OrbitControls.js';
+
+			import { nodeFrame } from './jsm/renderers/webgl/nodes/WebGLNodes.js';
+
+			import PointsNodeMaterial from './jsm/renderers/nodes/materials/PointsNodeMaterial.js';
+
+			import FloatNode from './jsm/renderers/nodes/inputs/FloatNode.js';
+			import Vector2Node from './jsm/renderers/nodes/inputs/Vector2Node.js';
+			import TextureNode from './jsm/renderers/nodes/inputs/TextureNode.js';
+			import PointUVNode from './jsm/renderers/nodes/accessors/PointUVNode.js';
+			import PositionNode from './jsm/renderers/nodes/accessors/PositionNode.js';
+			import AttributeNode from './jsm/renderers/nodes/core/AttributeNode.js';
+			import OperatorNode from './jsm/renderers/nodes/math/OperatorNode.js';
+			import MathNode from './jsm/renderers/nodes/math/MathNode.js';
+			import TimerNode from './jsm/renderers/nodes/utils/TimerNode.js';
+			import SpriteSheetUVNode from './jsm/renderers/nodes/utils/SpriteSheetUVNode.js';
+
+			let camera, scene, renderer, stats;
+
+			init();
+			animate();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 2, 2000 );
+				camera.position.x = 0;
+				camera.position.y = 100;
+				camera.position.z = - 300;
+
+				scene = new THREE.Scene();
+				scene.fog = new THREE.FogExp2( 0x000000, 0.001 );
+
+				// geometries
+
+				const teapotGeometry = new TeapotGeometry( 50, 7 );
+				const sphereGeometry = new THREE.SphereGeometry( 50, 130, 16 );
+
+				const geometry = new THREE.BufferGeometry();
+
+				// buffers
+
+				const speed = [];
+				const intensity = [];
+				const size = [];
+
+				const positionAttribute = teapotGeometry.getAttribute( 'position' );
+				const particleCount = positionAttribute.count;
+
+				for ( let i = 0; i < particleCount; i ++ ) {
+
+					speed.push( 20 + Math.random() * 50 );
+
+					intensity.push( Math.random() * .15 );
+
+					size.push( 30 + Math.random() * 230 );
+
+				}
+
+				geometry.setAttribute( 'position', positionAttribute );
+				geometry.setAttribute( 'targetPosition', sphereGeometry.getAttribute( 'position' ) );
+				geometry.setAttribute( 'particleSpeed', new THREE.Float32BufferAttribute( speed, 1 ) );
+				geometry.setAttribute( 'particleIntensity', new THREE.Float32BufferAttribute( intensity, 1 ) );
+				geometry.setAttribute( 'particleSize', new THREE.Float32BufferAttribute( size, 1 ) );
+
+				// maps
+
+				// Forked from: https://answers.unrealengine.com/questions/143267/emergency-need-help-with-fire-fx-weird-loop.html
+
+				const fireMap = new THREE.TextureLoader().load( 'textures/sprites/firetorch_1.jpg' );
+
+				// nodes
+
+				const targetPosition = new AttributeNode( 'targetPosition', 'vec3' );
+				const particleSpeed = new AttributeNode( 'particleSpeed', 'float' );
+				const particleIntensity = new AttributeNode( 'particleIntensity', 'float' );
+				const particleSize = new AttributeNode( 'particleSize', 'float' );
+
+				const time = new TimerNode();
+
+				const spriteSheetCount = new Vector2Node( new THREE.Vector2( 6, 6 ) ).setConst( true );
+
+				const fireUV = new SpriteSheetUVNode( spriteSheetCount, new PointUVNode() );
+				fireUV.frame = new OperatorNode( '*', time, particleSpeed );
+
+				const fireSprite = new TextureNode( fireMap, fireUV );
+				const fire = new OperatorNode( '*', fireSprite, particleIntensity );
+
+				const lerpPosition = new FloatNode( 0 );
+
+				const positionNode = new MathNode( MathNode.MIX, new PositionNode( PositionNode.LOCAL ), targetPosition, lerpPosition );
+
+				// material
+
+				const material = new PointsNodeMaterial( {
+					depthWrite: false,
+					transparent: true,
+					sizeAttenuation: true,
+					blending: THREE.AdditiveBlending
+				} );
+
+				material.colorNode = fire;
+				material.sizeNode = particleSize;
+				material.positionNode = positionNode;
+
+				const particles = new THREE.Points( geometry, material );
+				scene.add( particles );
+
+				// renderer
+
+				renderer = new THREE.WebGLRenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				// stats
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				// gui
+
+				const gui = new GUI();
+				const guiNode = { lerpPosition: 0 };
+
+				gui.add( material, 'sizeAttenuation' ).onChange( function () {
+
+					material.needsUpdate = true;
+
+				} );
+
+				gui.add( guiNode, 'lerpPosition', 0, 1 ).onChange( function () {
+
+					lerpPosition.value = guiNode.lerpPosition;
+
+				} );
+
+				gui.open();
+
+				// controls
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.maxDistance = 1000;
+				controls.update();
+
+				// events
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				render();
+				stats.update();
+
+			}
+
+			function render() {
+
+				nodeFrame.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>