webgpu_tsl_vfx_linkedparticles.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>three.js webgpu - vfx linked particles</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <meta property="og:title" content="three.js webgpu - vfx linked particles">
  8. <meta property="og:type" content="website">
  9. <meta property="og:url" content="https://threejs.org/examples/webgpu_tsl_vfx_linkedparticles.html">
  10. <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_tsl_vfx_linkedparticles.jpg">
  11. <link type="text/css" rel="stylesheet" href="example.css">
  12. </head>
  13. <body>
  14. <div id="info">
  15. <a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
  16. <div class="title-wrapper">
  17. <a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>VFX Linked Particles</span>
  18. </div>
  19. <small>
  20. Based on <a href="https://github.com/ULuIQ12/webgpu-tsl-linkedparticles" target="_blank" rel="noopener">this experiment</a> by Christophe Choffel.
  21. </small>
  22. </div>
  23. <script type="importmap">
  24. {
  25. "imports": {
  26. "three": "../build/three.webgpu.js",
  27. "three/webgpu": "../build/three.webgpu.js",
  28. "three/tsl": "../build/three.tsl.js",
  29. "three/addons/": "./jsm/"
  30. }
  31. }
  32. </script>
  33. <script type="module">
  34. import * as THREE from 'three/webgpu';
  35. import { atan, cos, float, max, min, mix, PI, TWO_PI, sin, vec2, vec3, color, Fn, hash, hue, If, instanceIndex, Loop, mx_fractal_noise_float, mx_fractal_noise_vec3, pass, pcurve, storage, deltaTime, time, uv, uniform, step } from 'three/tsl';
  36. import { bloom } from 'three/addons/tsl/display/BloomNode.js';
  37. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  38. import { Inspector } from 'three/addons/inspector/Inspector.js';
  39. import WebGPU from 'three/addons/capabilities/WebGPU.js';
  40. let camera, scene, renderer, renderPipeline, controls, timer, light;
  41. let updateParticles, spawnParticles; // TSL compute nodes
  42. let getInstanceColor; // TSL function
  43. const screenPointer = new THREE.Vector2();
  44. const scenePointer = new THREE.Vector3();
  45. const raycastPlane = new THREE.Plane( new THREE.Vector3( 0, 0, 1 ), 0 );
  46. const raycaster = new THREE.Raycaster();
  47. const nbParticles = Math.pow( 2, 13 );
  48. const timeScale = uniform( 1.0 );
  49. const particleLifetime = uniform( 0.5 );
  50. const particleSize = uniform( 1.0 );
  51. const linksWidth = uniform( 0.005 );
  52. const colorOffset = uniform( 0.0 );
  53. const colorVariance = uniform( 2.0 );
  54. const colorRotationSpeed = uniform( 1.0 );
  55. const spawnIndex = uniform( 0 );
  56. const nbToSpawn = uniform( 5 );
  57. const spawnPosition = uniform( vec3( 0.0 ) );
  58. const previousSpawnPosition = uniform( vec3( 0.0 ) );
  59. const turbFrequency = uniform( 0.5 );
  60. const turbAmplitude = uniform( 0.5 );
  61. const turbOctaves = uniform( 2 );
  62. const turbLacunarity = uniform( 2.0 );
  63. const turbGain = uniform( 0.5 );
  64. const turbFriction = uniform( 0.01 );
  65. init();
  66. async function init() {
  67. if ( WebGPU.isAvailable() === false ) {
  68. document.body.appendChild( WebGPU.getErrorMessage() );
  69. throw new Error( 'No WebGPU support' );
  70. }
  71. camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 200 );
  72. camera.position.set( 0, 0, 10 );
  73. scene = new THREE.Scene();
  74. timer = new THREE.Timer();
  75. timer.connect( document );
  76. // renderer
  77. renderer = new THREE.WebGPURenderer( { antialias: true } );
  78. renderer.setClearColor( 0x14171a );
  79. renderer.setPixelRatio( window.devicePixelRatio );
  80. renderer.setSize( window.innerWidth, window.innerHeight );
  81. renderer.setAnimationLoop( animate );
  82. renderer.inspector = new Inspector();
  83. renderer.toneMapping = THREE.ACESFilmicToneMapping;
  84. document.body.appendChild( renderer.domElement );
  85. await renderer.init();
  86. // TSL function
  87. // current color from index
  88. getInstanceColor = Fn( ( [ i ] ) => {
  89. return hue( color( 0x0000ff ), colorOffset.add( mx_fractal_noise_float( i.toFloat().mul( .1 ), 2, 2.0, 0.5, colorVariance ) ) );
  90. } );
  91. // Particles
  92. // storage buffers
  93. const particlePositions = storage( new THREE.StorageInstancedBufferAttribute( nbParticles, 4 ), 'vec4', nbParticles );
  94. const particleVelocities = storage( new THREE.StorageInstancedBufferAttribute( nbParticles, 4 ), 'vec4', nbParticles );
  95. // init particles buffers
  96. renderer.compute( Fn( () => {
  97. particlePositions.element( instanceIndex ).xyz.assign( vec3( 10000.0 ) );
  98. particlePositions.element( instanceIndex ).w.assign( vec3( - 1.0 ) ); // life is stored in w component; x<0 means dead
  99. } )().compute( nbParticles ) );
  100. // particles output
  101. const particleQuadSize = 0.05;
  102. const particleGeom = new THREE.PlaneGeometry( particleQuadSize, particleQuadSize );
  103. const particleMaterial = new THREE.SpriteNodeMaterial();
  104. particleMaterial.blending = THREE.AdditiveBlending;
  105. particleMaterial.depthWrite = false;
  106. particleMaterial.positionNode = particlePositions.toAttribute();
  107. particleMaterial.scaleNode = vec2( particleSize );
  108. particleMaterial.rotationNode = atan( particleVelocities.toAttribute().y, particleVelocities.toAttribute().x );
  109. particleMaterial.colorNode = Fn( () => {
  110. const life = particlePositions.toAttribute().w;
  111. const modLife = pcurve( life.oneMinus(), 8.0, 1.0 );
  112. const pulse = pcurve(
  113. sin( hash( instanceIndex ).mul( TWO_PI ).add( time.mul( 0.5 ).mul( TWO_PI ) ) ).mul( 0.5 ).add( 0.5 ),
  114. 0.25,
  115. 0.25
  116. ).mul( 10.0 ).add( 1.0 );
  117. return getInstanceColor( instanceIndex ).mul( pulse.mul( modLife ) );
  118. } )();
  119. particleMaterial.opacityNode = Fn( () => {
  120. const circle = step( uv().xy.sub( 0.5 ).length(), 0.5 );
  121. const life = particlePositions.toAttribute().w;
  122. return circle.mul( life );
  123. } )();
  124. const particleMesh = new THREE.InstancedMesh( particleGeom, particleMaterial, nbParticles );
  125. particleMesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
  126. particleMesh.frustumCulled = false;
  127. scene.add( particleMesh );
  128. // Links between particles
  129. // first, we define the indices for the links, 2 quads per particle, the indexation is fixed
  130. const linksIndices = [];
  131. for ( let i = 0; i < nbParticles; i ++ ) {
  132. const baseIndex = i * 8;
  133. for ( let j = 0; j < 2; j ++ ) {
  134. const offset = baseIndex + j * 4;
  135. linksIndices.push( offset, offset + 1, offset + 2, offset, offset + 2, offset + 3 );
  136. }
  137. }
  138. // storage buffers attributes for the links
  139. const nbVertices = nbParticles * 8;
  140. const linksVerticesSBA = new THREE.StorageBufferAttribute( nbVertices, 4 );
  141. const linksColorsSBA = new THREE.StorageBufferAttribute( nbVertices, 4 );
  142. // links output
  143. const linksGeom = new THREE.BufferGeometry();
  144. linksGeom.setAttribute( 'position', linksVerticesSBA );
  145. linksGeom.setAttribute( 'color', linksColorsSBA );
  146. linksGeom.setIndex( linksIndices );
  147. const linksMaterial = new THREE.MeshBasicNodeMaterial();
  148. linksMaterial.vertexColors = true;
  149. linksMaterial.side = THREE.DoubleSide;
  150. linksMaterial.transparent = true;
  151. linksMaterial.depthWrite = false;
  152. linksMaterial.depthTest = false;
  153. linksMaterial.blending = THREE.AdditiveBlending;
  154. linksMaterial.opacityNode = storage( linksColorsSBA, 'vec4', linksColorsSBA.count ).toAttribute().w;
  155. const linksMesh = new THREE.Mesh( linksGeom, linksMaterial );
  156. linksMesh.frustumCulled = false;
  157. scene.add( linksMesh );
  158. // compute nodes
  159. updateParticles = Fn( () => {
  160. const position = particlePositions.element( instanceIndex ).xyz;
  161. const life = particlePositions.element( instanceIndex ).w;
  162. const velocity = particleVelocities.element( instanceIndex ).xyz;
  163. const dt = deltaTime.mul( 0.1 ).mul( timeScale );
  164. If( life.greaterThan( 0.0 ), () => {
  165. // first we update the particles positions and velocities
  166. // velocity comes from a turbulence field, and is multiplied by the particle lifetime so that it slows down over time
  167. const localVel = mx_fractal_noise_vec3( position.mul( turbFrequency ), turbOctaves, turbLacunarity, turbGain, turbAmplitude ).mul( life.add( .01 ) );
  168. velocity.addAssign( localVel );
  169. velocity.mulAssign( turbFriction.oneMinus() );
  170. position.addAssign( velocity.mul( dt ) );
  171. // then we decrease the lifetime
  172. life.subAssign( dt.mul( particleLifetime.reciprocal() ) );
  173. // then we find the two closest particles and set a quad to each of them
  174. const closestDist1 = float( 10000.0 ).toVar();
  175. const closestPos1 = vec3( 0.0 ).toVar();
  176. const closestLife1 = float( 0.0 ).toVar();
  177. const closestDist2 = float( 10000.0 ).toVar();
  178. const closestPos2 = vec3( 0.0 ).toVar();
  179. const closestLife2 = float( 0.0 ).toVar();
  180. Loop( nbParticles, ( { i } ) => {
  181. const otherPart = particlePositions.element( i );
  182. If( i.notEqual( instanceIndex ).and( otherPart.w.greaterThan( 0.0 ) ), () => { // if not self and other particle is alive
  183. const otherPosition = otherPart.xyz;
  184. const dist = position.sub( otherPosition ).lengthSq();
  185. const moreThanZero = dist.greaterThan( 0.0 );
  186. If( dist.lessThan( closestDist1 ).and( moreThanZero ), () => {
  187. closestDist1.assign( dist );
  188. closestPos1.assign( otherPosition.xyz );
  189. closestLife1.assign( otherPart.w );
  190. } ).ElseIf( dist.lessThan( closestDist2 ).and( moreThanZero ), () => {
  191. closestDist2.assign( dist );
  192. closestPos2.assign( otherPosition.xyz );
  193. closestLife2.assign( otherPart.w );
  194. } );
  195. } );
  196. } );
  197. // then we update the links correspondingly
  198. const linksPositions = storage( linksVerticesSBA, 'vec4', linksVerticesSBA.count );
  199. const linksColors = storage( linksColorsSBA, 'vec4', linksColorsSBA.count );
  200. const firstLinkIndex = instanceIndex.mul( 8 );
  201. const secondLinkIndex = firstLinkIndex.add( 4 );
  202. // positions link 1
  203. linksPositions.element( firstLinkIndex ).xyz.assign( position );
  204. linksPositions.element( firstLinkIndex ).y.addAssign( linksWidth );
  205. linksPositions.element( firstLinkIndex.add( 1 ) ).xyz.assign( position );
  206. linksPositions.element( firstLinkIndex.add( 1 ) ).y.addAssign( linksWidth.negate() );
  207. linksPositions.element( firstLinkIndex.add( 2 ) ).xyz.assign( closestPos1 );
  208. linksPositions.element( firstLinkIndex.add( 2 ) ).y.addAssign( linksWidth.negate() );
  209. linksPositions.element( firstLinkIndex.add( 3 ) ).xyz.assign( closestPos1 );
  210. linksPositions.element( firstLinkIndex.add( 3 ) ).y.addAssign( linksWidth );
  211. // positions link 2
  212. linksPositions.element( secondLinkIndex ).xyz.assign( position );
  213. linksPositions.element( secondLinkIndex ).y.addAssign( linksWidth );
  214. linksPositions.element( secondLinkIndex.add( 1 ) ).xyz.assign( position );
  215. linksPositions.element( secondLinkIndex.add( 1 ) ).y.addAssign( linksWidth.negate() );
  216. linksPositions.element( secondLinkIndex.add( 2 ) ).xyz.assign( closestPos2 );
  217. linksPositions.element( secondLinkIndex.add( 2 ) ).y.addAssign( linksWidth.negate() );
  218. linksPositions.element( secondLinkIndex.add( 3 ) ).xyz.assign( closestPos2 );
  219. linksPositions.element( secondLinkIndex.add( 3 ) ).y.addAssign( linksWidth );
  220. // colors are the same for all vertices of both quads
  221. const linkColor = getInstanceColor( instanceIndex );
  222. // store the minimum lifetime of the closest particles in the w component of colors
  223. const l1 = max( 0.0, min( closestLife1, life ) ).pow( 0.8 ); // pow is here to apply a slight curve to the opacity
  224. const l2 = max( 0.0, min( closestLife2, life ) ).pow( 0.8 );
  225. Loop( 4, ( { i } ) => {
  226. linksColors.element( firstLinkIndex.add( i ) ).xyz.assign( linkColor );
  227. linksColors.element( firstLinkIndex.add( i ) ).w.assign( l1 );
  228. linksColors.element( secondLinkIndex.add( i ) ).xyz.assign( linkColor );
  229. linksColors.element( secondLinkIndex.add( i ) ).w.assign( l2 );
  230. } );
  231. } );
  232. } )().compute( nbParticles ).setName( 'Update Particles' );
  233. spawnParticles = Fn( () => {
  234. const particleIndex = spawnIndex.add( instanceIndex ).mod( nbParticles ).toInt();
  235. const position = particlePositions.element( particleIndex ).xyz;
  236. const life = particlePositions.element( particleIndex ).w;
  237. const velocity = particleVelocities.element( particleIndex ).xyz;
  238. life.assign( 1.0 ); // sets it alive
  239. // random spherical direction
  240. const rRange = float( 0.01 );
  241. const rTheta = hash( particleIndex ).mul( TWO_PI );
  242. const rPhi = hash( particleIndex.add( 1 ) ).mul( PI );
  243. const rx = sin( rTheta ).mul( cos( rPhi ) );
  244. const ry = sin( rTheta ).mul( sin( rPhi ) );
  245. const rz = cos( rTheta );
  246. const rDir = vec3( rx, ry, rz );
  247. // position is interpolated between the previous cursor position and the current one over the number of particles spawned
  248. const pos = mix( previousSpawnPosition, spawnPosition, instanceIndex.toFloat().div( nbToSpawn.sub( 1 ).toFloat() ).clamp() );
  249. position.assign( pos.add( rDir.mul( rRange ) ) );
  250. // start in that direction
  251. velocity.assign( rDir.mul( 5.0 ) );
  252. } )().compute( nbToSpawn.value ).setName( 'Spawn Particles' );
  253. // background , an inverted icosahedron
  254. const backgroundGeom = new THREE.IcosahedronGeometry( 100, 5 ).applyMatrix4( new THREE.Matrix4().makeScale( - 1, 1, 1 ) );
  255. const backgroundMaterial = new THREE.MeshStandardNodeMaterial();
  256. backgroundMaterial.roughness = 0.4;
  257. backgroundMaterial.metalness = 0.9;
  258. backgroundMaterial.flatShading = true;
  259. backgroundMaterial.colorNode = color( 0x0 );
  260. const backgroundMesh = new THREE.Mesh( backgroundGeom, backgroundMaterial );
  261. scene.add( backgroundMesh );
  262. // light for the background
  263. light = new THREE.PointLight( 0xffffff, 3000 );
  264. scene.add( light );
  265. // post processing
  266. renderPipeline = new THREE.RenderPipeline( renderer );
  267. const scenePass = pass( scene, camera );
  268. const scenePassColor = scenePass.getTextureNode( 'output' );
  269. const bloomPass = bloom( scenePassColor, 0.75, 0.1, 0.5 );
  270. renderPipeline.outputNode = scenePassColor.add( bloomPass );
  271. // controls
  272. controls = new OrbitControls( camera, renderer.domElement );
  273. controls.enableDamping = true;
  274. controls.autoRotate = true;
  275. controls.maxDistance = 75;
  276. window.addEventListener( 'resize', onWindowResize );
  277. // pointer handling
  278. window.addEventListener( 'pointermove', onPointerMove );
  279. // GUI
  280. const gui = renderer.inspector.createParameters( 'Parameters' );
  281. gui.add( controls, 'autoRotate' ).name( 'Auto Rotate' );
  282. gui.add( controls, 'autoRotateSpeed', - 10.0, 10.0, 0.01 ).name( 'Auto Rotate Speed' );
  283. const partFolder = gui.addFolder( 'Particles' );
  284. partFolder.add( timeScale, 'value', 0.0, 4.0, 0.01 ).name( 'timeScale' );
  285. partFolder.add( nbToSpawn, 'value', 1, 100, 1 ).name( 'Spawn rate' );
  286. partFolder.add( particleSize, 'value', 0.01, 3.0, 0.01 ).name( 'Size' );
  287. partFolder.add( particleLifetime, 'value', 0.01, 2.0, 0.01 ).name( 'Lifetime' );
  288. partFolder.add( linksWidth, 'value', 0.001, 0.1, 0.001 ).name( 'Links width' );
  289. partFolder.add( colorVariance, 'value', 0.0, 10.0, 0.01 ).name( 'Color variance' );
  290. partFolder.add( colorRotationSpeed, 'value', 0.0, 5.0, 0.01 ).name( 'Color rotation speed' );
  291. const turbFolder = gui.addFolder( 'Turbulence' );
  292. turbFolder.add( turbFriction, 'value', 0.0, 0.3, 0.01 ).name( 'Friction' );
  293. turbFolder.add( turbFrequency, 'value', 0.0, 1.0, 0.01 ).name( 'Frequency' );
  294. turbFolder.add( turbAmplitude, 'value', 0.0, 10.0, 0.01 ).name( 'Amplitude' );
  295. turbFolder.add( turbOctaves, 'value', 1, 9, 1 ).name( 'Octaves' );
  296. turbFolder.add( turbLacunarity, 'value', 1.0, 5.0, 0.01 ).name( 'Lacunarity' );
  297. turbFolder.add( turbGain, 'value', 0.0, 1.0, 0.01 ).name( 'Gain' );
  298. const bloomFolder = gui.addFolder( 'bloom' );
  299. bloomFolder.add( bloomPass.threshold, 'value', 0, 2.0, 0.01 ).name( 'Threshold' );
  300. bloomFolder.add( bloomPass.strength, 'value', 0, 10, 0.01 ).name( 'Strength' );
  301. bloomFolder.add( bloomPass.radius, 'value', 0, 1, 0.01 ).name( 'Radius' );
  302. }
  303. function onWindowResize() {
  304. camera.aspect = window.innerWidth / window.innerHeight;
  305. camera.updateProjectionMatrix();
  306. renderer.setSize( window.innerWidth, window.innerHeight );
  307. }
  308. function onPointerMove( e ) {
  309. screenPointer.x = ( e.clientX / window.innerWidth ) * 2 - 1;
  310. screenPointer.y = - ( e.clientY / window.innerHeight ) * 2 + 1;
  311. }
  312. function updatePointer() {
  313. raycaster.setFromCamera( screenPointer, camera );
  314. raycaster.ray.intersectPlane( raycastPlane, scenePointer );
  315. }
  316. function animate() {
  317. timer.update();
  318. // compute particles
  319. renderer.compute( updateParticles );
  320. renderer.compute( spawnParticles );
  321. // update particle index for next spawn
  322. spawnIndex.value = ( spawnIndex.value + nbToSpawn.value ) % nbParticles;
  323. // update raycast plane to face camera
  324. raycastPlane.normal.applyEuler( camera.rotation );
  325. updatePointer();
  326. // lerping spawn position
  327. previousSpawnPosition.value.copy( spawnPosition.value );
  328. spawnPosition.value.lerp( scenePointer, 0.1 );
  329. // rotating colors
  330. colorOffset.value += timer.getDelta() * colorRotationSpeed.value * timeScale.value;
  331. const elapsedTime = timer.getElapsed();
  332. light.position.set(
  333. Math.sin( elapsedTime * 0.5 ) * 30,
  334. Math.cos( elapsedTime * 0.3 ) * 30,
  335. Math.sin( elapsedTime * 0.2 ) * 30,
  336. );
  337. controls.update();
  338. renderPipeline.render();
  339. }
  340. </script>
  341. </body>
  342. </html>
粤ICP备19079148号