Jelajahi Sumber

WebGPU: Introduce Fat Points (#26930)

* Introduce Fat Points

* Introduce FatPointsNodeMaterial

* Enable damping

* Clean up

* Clean up

* Clean up

* Clean up

* Delete FatPointsMaterial

* Clean up

* Add screenshot

* Updated files
WestLangley 1 tahun lalu
induk
melakukan
c44c4ecd51

+ 1 - 0
examples/files.json

@@ -339,6 +339,7 @@
 		"webgpu_morphtargets",
 		"webgpu_occlusion",
 		"webgpu_particles",
+		"webgpu_points_fat",
 		"webgpu_rtt",
 		"webgpu_sandbox",
 		"webgpu_shadowmap",

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

@@ -72,6 +72,7 @@ export { default as BufferNode, buffer } from './accessors/BufferNode.js';
 export { default as CameraNode, cameraProjectionMatrix, cameraViewMatrix, cameraNormalMatrix, cameraWorldMatrix, cameraPosition, cameraNear, cameraFar } from './accessors/CameraNode.js';
 export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTextureNode.js';
 export { default as ExtendedMaterialNode, materialNormal } from './accessors/ExtendedMaterialNode.js';
+export { default as FatPointsMaterialNode, materialPointWidth } from './accessors/FatPointsMaterialNode.js';
 export { default as InstanceNode, instance } from './accessors/InstanceNode.js';
 export { default as LineMaterialNode, materialLineDashSize, materialLineDashOffset, materialLineGapSize, materialLineScale, materialLineWidth } from './accessors/LineMaterialNode.js';
 export { default as MaterialNode, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialReflectivity, materialRoughness, materialMetalness, materialRotation, materialSheen, materialSheenRoughness } from './accessors/MaterialNode.js';

+ 21 - 0
examples/jsm/nodes/accessors/FatPointsMaterialNode.js

@@ -0,0 +1,21 @@
+import MaterialNode from './MaterialNode.js';
+import { addNodeClass } from '../core/Node.js';
+import { nodeImmutable } from '../shadernode/ShaderNode.js';
+
+class FatPointsMaterialNode extends MaterialNode {
+
+	setup( /*builder*/ ) {
+
+		return this.getFloat( this.scope );
+
+	}
+
+}
+
+FatPointsMaterialNode.POINTWIDTH = 'pointWidth';
+
+export default FatPointsMaterialNode;
+
+export const materialPointWidth = nodeImmutable( FatPointsMaterialNode, FatPointsMaterialNode.POINTWIDTH );
+
+addNodeClass( 'FatPointsMaterialNode', FatPointsMaterialNode );

+ 1 - 0
examples/jsm/nodes/core/PropertyNode.js

@@ -55,5 +55,6 @@ export const shininess = nodeImmutable( PropertyNode, 'float', 'Shininess' );
 export const output = nodeImmutable( PropertyNode, 'vec4', 'Output' );
 export const dashSize = nodeImmutable( PropertyNode, 'float', 'dashSize' );
 export const gapSize = nodeImmutable( PropertyNode, 'float', 'gapSize' );
+export const pointWidth = nodeImmutable( PropertyNode, 'float', 'pointWidth' );
 
 addNodeClass( 'PropertyNode', PropertyNode );

+ 164 - 0
examples/jsm/nodes/materials/FatPointsNodeMaterial.js

@@ -0,0 +1,164 @@
+import NodeMaterial, { addNodeMaterial } from './NodeMaterial.js';
+import { varying } from '../core/VaryingNode.js';
+import { property } from '../core/PropertyNode.js';
+import { attribute } from '../core/AttributeNode.js';
+import { cameraProjectionMatrix } from '../accessors/CameraNode.js';
+import { materialColor } from '../accessors/MaterialNode.js';
+import { modelViewMatrix } from '../accessors/ModelNode.js';
+import { positionGeometry } from '../accessors/PositionNode.js';
+import { smoothstep } from '../math/MathNode.js';
+import { tslFn, vec2, vec4 } from '../shadernode/ShaderNode.js';
+import { uv } from '../accessors/UVNode.js';
+import { materialPointWidth } from '../accessors/FatPointsMaterialNode.js'; // or should this be a property, instead?
+import { viewport } from '../display/ViewportNode.js';
+import { color } from 'three/nodes';
+
+import { PointsMaterial } from 'three';
+
+const defaultValues = new PointsMaterial();
+
+class FatPointsNodeMaterial extends NodeMaterial {
+
+	constructor( params = {} ) {
+
+		super();
+
+		this.normals = false;
+
+		this.lights = false;
+
+		this.useAlphaToCoverage = true;
+
+		this.useColor = params.vertexColors;
+
+		this.pointWidth = 1;
+
+		this.pointColorNode = null;
+
+		this.setDefaultValues( defaultValues );
+
+		this.setupShaders();
+
+		this.setValues( params );
+
+	}
+
+	setupShaders() {
+
+		const useAlphaToCoverage = this.alphaToCoverage;
+		const useColor = this.useColor;
+
+		this.vertexNode = tslFn( () => {
+
+			//vUv = uv;
+			varying( vec2(), 'vUv' ).assign( uv() ); // @TODO: Analyze other way to do this
+
+			const instancePosition = attribute( 'instancePosition' );
+
+			// camera space
+			const mvPos = property( 'vec4', 'mvPos' );
+			mvPos.assign( modelViewMatrix.mul( vec4( instancePosition, 1.0 ) ) );
+
+			const aspect = viewport.z.div( viewport.w );
+
+			// clip space
+			const clipPos = cameraProjectionMatrix.mul( mvPos );
+
+			// offset in ndc space
+			const offset = property( 'vec2', 'offset' );
+			offset.assign( positionGeometry.xy );
+			offset.assign( offset.mul( materialPointWidth ) );
+			offset.assign( offset.div( viewport.z ) );
+			offset.y.assign( offset.y.mul( aspect ) );
+
+			// back to clip space
+			offset.assign( offset.mul( clipPos.w ) );
+
+			//clipPos.xy += offset;
+			clipPos.assign( clipPos.add( vec4( offset, 0, 0 ) ) );
+
+			return clipPos;
+
+			//vec4 mvPosition = mvPos; // this was used for somethihng...
+
+		} )();
+
+		this.colorNode = tslFn( () => {
+
+			const vUv = varying( vec2(), 'vUv' );
+
+			// force assignment into correct place in flow
+			const alpha = property( 'float', 'alpha' );
+			alpha.assign( 1 );
+
+			const a = vUv.x;
+			const b = vUv.y;
+
+			const len2 = a.mul( a ).add( b.mul( b ) );
+
+			if ( useAlphaToCoverage ) {
+
+				// force assignment out of following 'if' statement - to avoid uniform control flow errors
+				const dlen = property( 'float', 'dlen' );
+				dlen.assign( len2.fwidth() );
+
+				alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
+
+			} else {
+
+				len2.greaterThan( 1.0 ).discard();
+
+			}
+
+			let pointColorNode;
+
+			if ( this.pointColorNode ) {
+
+				pointColorNode = this.pointColorNode;
+
+			} else {
+
+				if ( useColor ) {
+
+					const instanceColor = attribute( 'instanceColor' );
+
+					pointColorNode = color( instanceColor ).mul( color( materialColor ) );
+
+				} else {
+
+					pointColorNode = materialColor;
+
+				}
+
+			}
+
+			return vec4( pointColorNode, alpha );
+
+		} )();
+
+		this.needsUpdate = true;
+
+	}
+
+	get alphaToCoverage() {
+
+		return this.useAlphaToCoverage;
+
+	}
+
+	set alphaToCoverage( value ) {
+
+		if ( this.useAlphaToCoverage !== value ) {
+
+			this.useAlphaToCoverage = value;
+			this.setupShaders();
+
+		}
+
+	}
+
+}
+
+export default FatPointsNodeMaterial;
+
+addNodeMaterial( 'FatPointsNodeMaterial', FatPointsNodeMaterial );

+ 1 - 0
examples/jsm/nodes/materials/Materials.js

@@ -1,6 +1,7 @@
 // @TODO: We can simplify "export { default as SomeNode, other, exports } from '...'" to just "export * from '...'" if we will use only named exports
 
 export { default as NodeMaterial, addNodeMaterial, createNodeMaterialFromType } from './NodeMaterial.js';
+export { default as FatPointsNodeMaterial } from './FatPointsNodeMaterial.js';
 export { default as LineBasicNodeMaterial } from './LineBasicNodeMaterial.js';
 export { default as LineDashedNodeMaterial } from './LineDashedNodeMaterial.js';
 export { default as Line2NodeMaterial } from './Line2NodeMaterial.js';

+ 21 - 0
examples/jsm/points/FatPoints.js

@@ -0,0 +1,21 @@
+import {
+	Mesh
+} from 'three';
+import { FatPointsGeometry } from '../points/FatPointsGeometry.js';
+import { FatPointsNodeMaterial } from 'three/nodes';
+
+class FatPoints extends Mesh {
+
+	constructor( geometry = new FatPointsGeometry(), material = new FatPointsNodeMaterial() ) {
+
+		super( geometry, material );
+
+		this.isFatPoints = true;
+
+		this.type = 'FatPoints';
+
+	}
+
+}
+
+export { FatPoints };

+ 174 - 0
examples/jsm/points/FatPointsGeometry.js

@@ -0,0 +1,174 @@
+import {
+	Box3,
+	Float32BufferAttribute,
+	InstancedBufferGeometry,
+	InstancedBufferAttribute,
+	Sphere,
+	Vector3
+} from 'three';
+
+const _vector = new Vector3();
+
+class FatPointsGeometry extends InstancedBufferGeometry {
+
+	constructor() {
+
+		super();
+
+		this.isFatPointsGeometry = true;
+
+		this.type = 'FatPointsGeometry';
+
+		const positions = [ - 1, 1, 0, 1, 1, 0, - 1, - 1, 0, 1, - 1, 0 ];
+		const uvs = [ - 1, 1, 1, 1, - 1, - 1, 1, - 1 ];
+		const index = [ 0, 2, 1, 2, 3, 1 ];
+
+		this.setIndex( index );
+		this.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
+		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
+
+	}
+
+	applyMatrix4( matrix ) {
+
+		const pos = this.attributes.instancePosition;
+
+		if ( pos !== undefined ) {
+
+			pos.applyMatrix4( matrix );
+
+			pos.needsUpdate = true;
+
+		}
+
+		if ( this.boundingBox !== null ) {
+
+			this.computeBoundingBox();
+
+		}
+
+		if ( this.boundingSphere !== null ) {
+
+			this.computeBoundingSphere();
+
+		}
+
+		return this;
+
+	}
+
+	setPositions( array ) {
+
+		let points;
+
+		if ( array instanceof Float32Array ) {
+
+			points = array;
+
+		} else if ( Array.isArray( array ) ) {
+
+			points = new Float32Array( array );
+
+		}
+
+		this.setAttribute( 'instancePosition', new InstancedBufferAttribute( points, 3 ) ); // xyz
+
+		//
+
+		this.computeBoundingBox();
+		this.computeBoundingSphere();
+
+		return this;
+
+	}
+
+	setColors( array ) {
+
+		let colors;
+
+		if ( array instanceof Float32Array ) {
+
+			colors = array;
+
+		} else if ( Array.isArray( array ) ) {
+
+			colors = new Float32Array( array );
+
+		}
+
+		this.setAttribute( 'instanceColor', new InstancedBufferAttribute( colors, 3 ) ); // rgb
+
+		return this;
+
+	}
+
+	computeBoundingBox() {
+
+		if ( this.boundingBox === null ) {
+
+			this.boundingBox = new Box3();
+
+		}
+
+		const pos = this.attributes.instancePosition;
+
+		if ( pos !== undefined ) {
+
+			this.boundingBox.setFromBufferAttribute( pos );
+
+		}
+
+	}
+
+	computeBoundingSphere() {
+
+		if ( this.boundingSphere === null ) {
+
+			this.boundingSphere = new Sphere();
+
+		}
+
+		if ( this.boundingBox === null ) {
+
+			this.computeBoundingBox();
+
+		}
+
+		const pos = this.attributes.instancePosition;
+
+		if ( pos !== undefined ) {
+
+			const center = this.boundingSphere.center;
+
+			this.boundingBox.getCenter( center );
+
+			let maxRadiusSq = 0;
+
+			for ( let i = 0, il = pos.count; i < il; i ++ ) {
+
+				_vector.fromBufferAttribute( pos, i );
+				maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) );
+
+			}
+
+			this.boundingSphere.radius = Math.sqrt( maxRadiusSq );
+
+			if ( isNaN( this.boundingSphere.radius ) ) {
+
+				console.error( 'THREE.FatPointsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this );
+
+			}
+
+		}
+
+	}
+
+	toJSON() {
+
+		// todo
+
+	}
+
+}
+
+export { FatPointsGeometry };

TEMPAT SAMPAH
examples/screenshots/webgpu_points_fat.jpg


+ 234 - 0
examples/webgpu_points_fat.html

@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - points - fat</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="container"></div>
+
+		<div id="info"><a href="https://threejs.org" target="_blank">three.js</a> - fat points</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 WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGL from 'three/addons/capabilities/WebGL.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { FatPoints } from 'three/addons/points/FatPoints.js';
+			//import { FatPointsNodeMaterial } from 'three/addons/points/FatPointsNodeMaterial.js'; // why not this, instead?
+			import { FatPointsGeometry } from 'three/addons/points/FatPointsGeometry.js';
+
+			import { color, FatPointsNodeMaterial } from 'three/nodes';
+
+			import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js';
+
+			let renderer, scene, camera, camera2, controls, backgroundNode;
+			let material;
+			let stats;
+			let gui;
+
+			// viewport
+			let insetWidth;
+			let insetHeight;
+
+			init();
+			animate();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false && WebGL.isWebGL2Available() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU or WebGL2 support' );
+
+				}
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( - 40, 0, 60 );
+
+				camera2 = new THREE.PerspectiveCamera( 40, 1, 1, 1000 );
+				camera2.position.copy( camera.position );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.enableDamping = true;
+				controls.minDistance = 10;
+				controls.maxDistance = 500;
+
+				backgroundNode = color( 0x222222 );
+
+				// Position and THREE.Color Data
+
+				const positions = [];
+				const colors = [];
+
+				const points = GeometryUtils.hilbert3D( new THREE.Vector3( 0, 0, 0 ), 20.0, 1, 0, 1, 2, 3, 4, 5, 6, 7 );
+
+				const spline = new THREE.CatmullRomCurve3( points );
+				const divisions = Math.round( 4 * points.length );
+				const point = new THREE.Vector3();
+				const pointColor = new THREE.Color();
+
+				for ( let i = 0, l = divisions; i < l; i ++ ) {
+
+					const t = i / l;
+
+					spline.getPoint( t, point );
+					positions.push( point.x, point.y, point.z );
+
+					pointColor.setHSL( t, 1.0, 0.5, THREE.SRGBColorSpace );
+					colors.push( pointColor.r, pointColor.g, pointColor.b );
+
+				}
+
+				// Fat Points
+
+				const geometry = new FatPointsGeometry();
+				geometry.setPositions( positions );
+				geometry.setColors( colors );
+
+				geometry.instanceCount = positions.length / 3; // this should not be necessary
+
+				material = new FatPointsNodeMaterial( {
+
+					color: 0xffffff,
+					pointWidth: 10, // in pixel units
+
+					vertexColors: true,
+					alphaToCoverage: true,
+
+				} );
+
+				const fatPoints = new FatPoints( geometry, material );
+				fatPoints.scale.set( 1, 1, 1 );
+				scene.add( fatPoints );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+				onWindowResize();
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				initGui();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				insetWidth = window.innerHeight / 4; // square
+				insetHeight = window.innerHeight / 4;
+
+				camera2.aspect = insetWidth / insetHeight;
+				camera2.updateProjectionMatrix();
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				stats.update();
+
+				// main scene
+
+				renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );
+
+				controls.update();
+
+				renderer.autoClear = true;
+
+				scene.backgroundNode = null;
+
+				renderer.render( scene, camera );
+
+				// inset scene
+
+				renderer.clearDepth(); // important!
+
+				renderer.setScissorTest( true );
+
+				renderer.setScissor( 20, 20, insetWidth, insetHeight );
+
+				renderer.setViewport( 20, 20, insetWidth, insetHeight );
+
+				camera2.position.copy( camera.position );
+
+				camera2.quaternion.copy( camera.quaternion );
+
+				renderer.autoClear = false;
+
+				scene.backgroundNode = backgroundNode;
+
+				renderer.render( scene, camera2 );
+
+				renderer.setScissorTest( false );
+
+			}
+
+			//
+
+			function initGui() {
+
+				gui = new GUI();
+
+				const param = {
+					'width': 10,
+					'alphaToCoverage': true,
+				};
+
+				gui.add( param, 'width', 1, 20, 1 ).onChange( function ( val ) {
+
+					material.pointWidth = val;
+
+				} );
+
+				gui.add( param, 'alphaToCoverage' ).onChange( function ( val ) {
+
+					material.alphaToCoverage = val;
+
+				} );
+
+			}
+
+		</script>
+
+	</body>
+
+</html>