Michael Herzog 1 неделя назад
Родитель
Сommit
6bb220fa4c

+ 1 - 0
examples/files.json

@@ -416,6 +416,7 @@
 		"webgpu_ocean",
 		"webgpu_parallax_uv",
 		"webgpu_particles",
+		"webgpu_particles_soft",
 		"webgpu_performance",
 		"webgpu_performance_renderbundle",
 		"webgpu_pmrem_cubemap",

+ 60 - 0
examples/jsm/tsl/utils/SoftParticles.js

@@ -0,0 +1,60 @@
+import { Fn, float, positionView, viewportDepthTexture, perspectiveDepthToViewZ, cameraNear, cameraFar } from 'three/tsl';
+
+/**
+ * @module SoftParticles
+ * @three_import import { softParticles } from 'three/addons/tsl/utils/SoftParticles.js';
+ */
+
+/**
+ * A symmetric contrast curve, as described in the "Soft Particles" white paper
+ * (NVIDIA, Tristan Lorach).
+ *
+ * @tsl
+ * @function
+ * @private
+ * @param {Node<float>} input - The value to remap, expected in the `[0, 1]` range.
+ * @param {Node<float>} power - The contrast power. `1` is linear, higher values sharpen the transition.
+ * @return {Node<float>} The remapped value.
+ */
+const contrastCurve = /*@__PURE__*/ Fn( ( [ input, power ] ) => {
+
+	const aboveHalf = input.greaterThan( 0.5 ).toConst();
+	const folded = aboveHalf.select( input.oneMinus(), input ).toConst();
+	const output = folded.mul( 2 ).saturate().pow( power ).mul( 0.5 ).toConst();
+
+	return aboveHalf.select( output.oneMinus(), output );
+
+} ).setLayout( {
+	name: 'contrastCurve',
+	type: 'float',
+	inputs: [
+		{ name: 'input', type: 'float' },
+		{ name: 'power', type: 'float' }
+	]
+} );
+
+/**
+ * Computes an opacity node for soft particles, based on the "Soft Particles" white
+ * paper (NVIDIA, Tristan Lorach).
+ *
+ * @tsl
+ * @function
+ * @param {Object} [parameters={}] - The configuration parameters.
+ * @param {Node<float>} [parameters.opacity=float(1)] - The sprite's base opacity, which the soft fade is multiplied with.
+ * @param {Node<float>|number} [parameters.distance=1] - The world-space distance over which the sprite fades out against the scene.
+ * @param {Node<float>|number} [parameters.contrast=2] - The contrast power of the fade curve. `1` is linear, higher values sharpen the transition.
+ * @param {Node<float>} [parameters.viewportDepth=viewportDepthTexture()] - The opaque scene depth the particles fade against.
+ * @return {Node<float>} The opacity node to assign to `material.opacityNode`.
+ */
+export function softParticles( { opacity = float( 1 ), distance = 1, contrast = 2, viewportDepth = viewportDepthTexture() } = {} ) {
+
+	// Read the opaque scene depth captured before the particle pass and convert both
+	// the scene depth and the particle's depth to view space, so the gap between them
+	// can be measured in world units.
+	const sceneViewZ = perspectiveDepthToViewZ( viewportDepth, cameraNear, cameraFar ).toConst();
+	const depthDelta = positionView.z.sub( sceneViewZ ).div( distance ).saturate();
+
+	// Fade out as the particle approaches the scene; stay opaque when well in front of it.
+	return opacity.mul( contrastCurve( depthDelta, contrast ) );
+
+}

BIN
examples/screenshots/webgpu_particles_soft.jpg


+ 197 - 0
examples/webgpu_particles_soft.html

@@ -0,0 +1,197 @@
+<html lang="en">
+	<head>
+		<title>three.js webgpu - particles - soft</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<meta property="og:title" content="three.js webgpu - particles - soft">
+		<meta property="og:type" content="website">
+		<meta property="og:url" content="https://threejs.org/examples/webgpu_particles_soft.html">
+		<meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_particles_soft.jpg">
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>WebGPU Soft Particles</span>
+			</div>
+
+			<small>Soft particles fade smoke sprites out where they intersect solid geometry.</small>
+		</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 { float, range, texture, mix, uv, color, rotateUV, positionLocal, time, uniform } from 'three/tsl';
+			import { softParticles } from 'three/addons/tsl/utils/SoftParticles.js';
+
+			import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { Inspector } from 'three/addons/inspector/Inspector.js';
+
+			let camera, scene, renderer;
+			let controls;
+
+			init();
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 6, 8, 8 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x000000 );
+				scene.fog = new THREE.FogExp2( 0x000000, 0.025 );
+
+				// lights
+
+				scene.add( new THREE.AmbientLight( 0xffffff, 0.25 ) );
+
+				const spotLight = new THREE.SpotLight( 0xffffff, 800 );
+				spotLight.position.set( 10, 15, 8 );
+				spotLight.target.position.set( 0, 4, 0 );
+				spotLight.angle = 0.5;
+				spotLight.penumbra = 0.7;
+				spotLight.distance = 0;
+
+				scene.add( spotLight );
+
+				//
+
+				const loader = new PLYLoader();
+				loader.load( './models/ply/binary/Lucy100k.ply', function ( geometry ) {
+
+					geometry.computeVertexNormals();
+
+					const material = new THREE.MeshStandardMaterial( );
+					const mesh = new THREE.Mesh( geometry, material );
+
+					mesh.position.y = 4;
+					mesh.rotation.y = Math.PI;
+					mesh.scale.multiplyScalar( 0.005 );
+			
+					scene.add( mesh );
+
+				} );
+
+				const groundMaterial = new THREE.MeshStandardNodeMaterial( { color: 0x444444, roughness: 1 } );
+				const ground = new THREE.Mesh( new THREE.PlaneGeometry( 100, 100 ), groundMaterial );
+				ground.rotation.x = - Math.PI / 2;
+				scene.add( ground );
+
+				// smoke texture
+
+				const textureLoader = new THREE.TextureLoader();
+				const map = textureLoader.load( 'textures/opengameart/smoke1.png' );
+				map.colorSpace = THREE.SRGBColorSpace;
+
+				// simple smoke particle system
+
+				const lifeRange = range( 0.1, 1 );
+				const offsetRange = range( new THREE.Vector3( - 2, 0, - 2 ), new THREE.Vector3( 2, 7, 2 ) );
+
+				const speed = float( 0.2 );
+				const scaledTime = time.add( 20 ).mul( speed );
+
+				const lifeTime = scaledTime.mul( lifeRange ).mod( 1 );
+				const scaleRange = range( 6, 8 );
+				const rotateRange = range( 0.1, 2 );
+
+				const life = lifeTime.div( lifeRange );
+
+				const textureNode = texture( map, rotateUV( uv(), scaledTime.mul( rotateRange ) ) );
+
+				const fade = life.smoothstep( 0, 0.3 ).mul( life.smoothstep( 1, 0.7 ) );
+
+				// soft particles
+
+				const softEnabled = uniform( 1 ); // 0 = hard particles, 1 = soft particles
+				const softDistance = uniform( 1 ); // world-space fade distance
+				const softContrast = uniform( 2 );
+
+				const baseOpacity = textureNode.a.mul( fade );
+
+				const smokeOpacity = mix( baseOpacity, softParticles( {
+					opacity: baseOpacity,
+					distance: softDistance,
+					contrast: softContrast
+				} ), softEnabled );
+
+				const smokeColor = mix( color( 0x6f6f6f ), color( 0x303030 ), positionLocal.y.mul( 0.1 ).clamp() );
+
+				// instanced sprites
+
+				const smokeMaterial = new THREE.SpriteNodeMaterial();
+				smokeMaterial.colorNode = smokeColor;
+				smokeMaterial.opacityNode = smokeOpacity;
+				smokeMaterial.positionNode = offsetRange.mul( lifeTime );
+				smokeMaterial.scaleNode = scaleRange.mul( lifeTime.max( 0.3 ) );
+				smokeMaterial.depthWrite = false;
+
+				const smoke = new THREE.Sprite( smokeMaterial );
+				smoke.count = 50;
+				smoke.frustumCulled = false;
+				scene.add( smoke );
+
+				// renderer
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				renderer.inspector = new Inspector();
+				document.body.appendChild( renderer.domElement );
+
+				// controls
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 6;
+				controls.maxDistance = 20;
+				controls.target.set( 0, 4, 0 );
+				controls.enableDamping = true;
+				controls.update();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// gui
+
+				const gui = renderer.inspector.createParameters( 'Settings' );
+				gui.add( softEnabled, 'value', { hard: 0, soft: 1 } ).name( 'mode' );
+				gui.add( softDistance, 'value', 0.1, 2, 0.01 ).name( 'soft distance' );
+				gui.add( softContrast, 'value', 1, 6, 0.01 ).name( 'soft contrast' );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function render() {
+
+				controls.update();
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

粤ICP备19079148号