GPUParticleSystem.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. /*
  2. * GPU Particle System
  3. * @author flimshaw - Charlie Hoey - http://charliehoey.com
  4. *
  5. * A simple to use, general purpose GPU system. Particles are spawn-and-forget with
  6. * several options available, and do not require monitoring or cleanup after spawning.
  7. * Because the paths of all particles are completely deterministic once spawned, the scale
  8. * and direction of time is also variable.
  9. *
  10. * Currently uses a static wrapping perlin noise texture for turbulence, and a small png texture for
  11. * particles, but adding support for a particle texture atlas or changing to a different type of turbulence
  12. * would be a fairly light day's work.
  13. *
  14. * Shader and javascript packing code derrived from several Stack Overflow examples.
  15. *
  16. */
  17. import {
  18. AdditiveBlending,
  19. BufferAttribute,
  20. BufferGeometry,
  21. Color,
  22. Math as _Math,
  23. Object3D,
  24. Points,
  25. RepeatWrapping,
  26. ShaderMaterial,
  27. TextureLoader,
  28. Vector3
  29. } from "../../../build/three.module.js";
  30. var GPUParticleSystem = function ( options ) {
  31. Object3D.apply( this, arguments );
  32. options = options || {};
  33. // parse options and use defaults
  34. this.PARTICLE_COUNT = options.maxParticles || 1000000;
  35. this.PARTICLE_CONTAINERS = options.containerCount || 1;
  36. this.PARTICLE_NOISE_TEXTURE = options.particleNoiseTex || null;
  37. this.PARTICLE_SPRITE_TEXTURE = options.particleSpriteTex || null;
  38. this.PARTICLES_PER_CONTAINER = Math.ceil( this.PARTICLE_COUNT / this.PARTICLE_CONTAINERS );
  39. this.PARTICLE_CURSOR = 0;
  40. this.time = 0;
  41. this.particleContainers = [];
  42. this.rand = [];
  43. // custom vertex and fragement shader
  44. var GPUParticleShader = {
  45. vertexShader: [
  46. 'uniform float uTime;',
  47. 'uniform float uScale;',
  48. 'uniform sampler2D tNoise;',
  49. 'attribute vec3 positionStart;',
  50. 'attribute float startTime;',
  51. 'attribute vec3 velocity;',
  52. 'attribute float turbulence;',
  53. 'attribute vec3 color;',
  54. 'attribute float size;',
  55. 'attribute float lifeTime;',
  56. 'varying vec4 vColor;',
  57. 'varying float lifeLeft;',
  58. 'void main() {',
  59. // unpack things from our attributes'
  60. ' vColor = vec4( color, 1.0 );',
  61. // convert our velocity back into a value we can use'
  62. ' vec3 newPosition;',
  63. ' vec3 v;',
  64. ' float timeElapsed = uTime - startTime;',
  65. ' lifeLeft = 1.0 - ( timeElapsed / lifeTime );',
  66. ' gl_PointSize = ( uScale * size ) * lifeLeft;',
  67. ' v.x = ( velocity.x - 0.5 ) * 3.0;',
  68. ' v.y = ( velocity.y - 0.5 ) * 3.0;',
  69. ' v.z = ( velocity.z - 0.5 ) * 3.0;',
  70. ' newPosition = positionStart + ( v * 10.0 ) * timeElapsed;',
  71. ' vec3 noise = texture2D( tNoise, vec2( newPosition.x * 0.015 + ( uTime * 0.05 ), newPosition.y * 0.02 + ( uTime * 0.015 ) ) ).rgb;',
  72. ' vec3 noiseVel = ( noise.rgb - 0.5 ) * 30.0;',
  73. ' newPosition = mix( newPosition, newPosition + vec3( noiseVel * ( turbulence * 5.0 ) ), ( timeElapsed / lifeTime ) );',
  74. ' if( v.y > 0. && v.y < .05 ) {',
  75. ' lifeLeft = 0.0;',
  76. ' }',
  77. ' if( v.x < - 1.45 ) {',
  78. ' lifeLeft = 0.0;',
  79. ' }',
  80. ' if( timeElapsed > 0.0 ) {',
  81. ' gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );',
  82. ' } else {',
  83. ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
  84. ' lifeLeft = 0.0;',
  85. ' gl_PointSize = 0.;',
  86. ' }',
  87. '}'
  88. ].join( '\n' ),
  89. fragmentShader: [
  90. 'float scaleLinear( float value, vec2 valueDomain ) {',
  91. ' return ( value - valueDomain.x ) / ( valueDomain.y - valueDomain.x );',
  92. '}',
  93. 'float scaleLinear( float value, vec2 valueDomain, vec2 valueRange ) {',
  94. ' return mix( valueRange.x, valueRange.y, scaleLinear( value, valueDomain ) );',
  95. '}',
  96. 'varying vec4 vColor;',
  97. 'varying float lifeLeft;',
  98. 'uniform sampler2D tSprite;',
  99. 'void main() {',
  100. ' float alpha = 0.;',
  101. ' if( lifeLeft > 0.995 ) {',
  102. ' alpha = scaleLinear( lifeLeft, vec2( 1.0, 0.995 ), vec2( 0.0, 1.0 ) );',
  103. ' } else {',
  104. ' alpha = lifeLeft * 0.75;',
  105. ' }',
  106. ' vec4 tex = texture2D( tSprite, gl_PointCoord );',
  107. ' gl_FragColor = vec4( vColor.rgb * tex.a, alpha * tex.a );',
  108. '}'
  109. ].join( '\n' )
  110. };
  111. // preload a million random numbers
  112. var i;
  113. for ( i = 1e5; i > 0; i -- ) {
  114. this.rand.push( Math.random() - 0.5 );
  115. }
  116. this.random = function () {
  117. return ++ i >= this.rand.length ? this.rand[ i = 1 ] : this.rand[ i ];
  118. };
  119. var textureLoader = new TextureLoader();
  120. this.particleNoiseTex = this.PARTICLE_NOISE_TEXTURE || textureLoader.load( 'textures/perlin-512.png' );
  121. this.particleNoiseTex.wrapS = this.particleNoiseTex.wrapT = RepeatWrapping;
  122. this.particleSpriteTex = this.PARTICLE_SPRITE_TEXTURE || textureLoader.load( 'textures/particle2.png' );
  123. this.particleSpriteTex.wrapS = this.particleSpriteTex.wrapT = RepeatWrapping;
  124. this.particleShaderMat = new ShaderMaterial( {
  125. transparent: true,
  126. depthWrite: false,
  127. uniforms: {
  128. 'uTime': {
  129. value: 0.0
  130. },
  131. 'uScale': {
  132. value: 1.0
  133. },
  134. 'tNoise': {
  135. value: this.particleNoiseTex
  136. },
  137. 'tSprite': {
  138. value: this.particleSpriteTex
  139. }
  140. },
  141. blending: AdditiveBlending,
  142. vertexShader: GPUParticleShader.vertexShader,
  143. fragmentShader: GPUParticleShader.fragmentShader
  144. } );
  145. // define defaults for all values
  146. this.particleShaderMat.defaultAttributeValues.particlePositionsStartTime = [ 0, 0, 0, 0 ];
  147. this.particleShaderMat.defaultAttributeValues.particleVelColSizeLife = [ 0, 0, 0, 0 ];
  148. this.init = function () {
  149. for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
  150. var c = new GPUParticleContainer( this.PARTICLES_PER_CONTAINER, this );
  151. this.particleContainers.push( c );
  152. this.add( c );
  153. }
  154. };
  155. this.spawnParticle = function ( options ) {
  156. this.PARTICLE_CURSOR ++;
  157. if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) {
  158. this.PARTICLE_CURSOR = 1;
  159. }
  160. var currentContainer = this.particleContainers[ Math.floor( this.PARTICLE_CURSOR / this.PARTICLES_PER_CONTAINER ) ];
  161. currentContainer.spawnParticle( options );
  162. };
  163. this.update = function ( time ) {
  164. for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
  165. this.particleContainers[ i ].update( time );
  166. }
  167. };
  168. this.dispose = function () {
  169. this.particleShaderMat.dispose();
  170. this.particleNoiseTex.dispose();
  171. this.particleSpriteTex.dispose();
  172. for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
  173. this.particleContainers[ i ].dispose();
  174. }
  175. };
  176. this.init();
  177. };
  178. GPUParticleSystem.prototype = Object.create( Object3D.prototype );
  179. GPUParticleSystem.prototype.constructor = GPUParticleSystem;
  180. // Subclass for particle containers, allows for very large arrays to be spread out
  181. var GPUParticleContainer = function ( maxParticles, particleSystem ) {
  182. Object3D.apply( this, arguments );
  183. this.PARTICLE_COUNT = maxParticles || 100000;
  184. this.PARTICLE_CURSOR = 0;
  185. this.time = 0;
  186. this.offset = 0;
  187. this.count = 0;
  188. this.DPR = window.devicePixelRatio;
  189. this.GPUParticleSystem = particleSystem;
  190. this.particleUpdate = false;
  191. // geometry
  192. this.particleShaderGeo = new BufferGeometry();
  193. this.particleShaderGeo.addAttribute( 'position', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
  194. this.particleShaderGeo.addAttribute( 'positionStart', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
  195. this.particleShaderGeo.addAttribute( 'startTime', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
  196. this.particleShaderGeo.addAttribute( 'velocity', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
  197. this.particleShaderGeo.addAttribute( 'turbulence', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
  198. this.particleShaderGeo.addAttribute( 'color', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) );
  199. this.particleShaderGeo.addAttribute( 'size', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
  200. this.particleShaderGeo.addAttribute( 'lifeTime', new BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) );
  201. // material
  202. this.particleShaderMat = this.GPUParticleSystem.particleShaderMat;
  203. var position = new Vector3();
  204. var velocity = new Vector3();
  205. var color = new Color();
  206. this.spawnParticle = function ( options ) {
  207. var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' );
  208. var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' );
  209. var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' );
  210. var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' );
  211. var colorAttribute = this.particleShaderGeo.getAttribute( 'color' );
  212. var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' );
  213. var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' );
  214. options = options || {};
  215. // setup reasonable default values for all arguments
  216. position = options.position !== undefined ? position.copy( options.position ) : position.set( 0, 0, 0 );
  217. velocity = options.velocity !== undefined ? velocity.copy( options.velocity ) : velocity.set( 0, 0, 0 );
  218. color = options.color !== undefined ? color.set( options.color ) : color.set( 0xffffff );
  219. var positionRandomness = options.positionRandomness !== undefined ? options.positionRandomness : 0;
  220. var velocityRandomness = options.velocityRandomness !== undefined ? options.velocityRandomness : 0;
  221. var colorRandomness = options.colorRandomness !== undefined ? options.colorRandomness : 1;
  222. var turbulence = options.turbulence !== undefined ? options.turbulence : 1;
  223. var lifetime = options.lifetime !== undefined ? options.lifetime : 5;
  224. var size = options.size !== undefined ? options.size : 10;
  225. var sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0;
  226. var smoothPosition = options.smoothPosition !== undefined ? options.smoothPosition : false;
  227. if ( this.DPR !== undefined ) size *= this.DPR;
  228. var i = this.PARTICLE_CURSOR;
  229. // position
  230. positionStartAttribute.array[ i * 3 + 0 ] = position.x + ( particleSystem.random() * positionRandomness );
  231. positionStartAttribute.array[ i * 3 + 1 ] = position.y + ( particleSystem.random() * positionRandomness );
  232. positionStartAttribute.array[ i * 3 + 2 ] = position.z + ( particleSystem.random() * positionRandomness );
  233. if ( smoothPosition === true ) {
  234. positionStartAttribute.array[ i * 3 + 0 ] += - ( velocity.x * particleSystem.random() );
  235. positionStartAttribute.array[ i * 3 + 1 ] += - ( velocity.y * particleSystem.random() );
  236. positionStartAttribute.array[ i * 3 + 2 ] += - ( velocity.z * particleSystem.random() );
  237. }
  238. // velocity
  239. var maxVel = 2;
  240. var velX = velocity.x + particleSystem.random() * velocityRandomness;
  241. var velY = velocity.y + particleSystem.random() * velocityRandomness;
  242. var velZ = velocity.z + particleSystem.random() * velocityRandomness;
  243. velX = _Math.clamp( ( velX - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 );
  244. velY = _Math.clamp( ( velY - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 );
  245. velZ = _Math.clamp( ( velZ - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 );
  246. velocityAttribute.array[ i * 3 + 0 ] = velX;
  247. velocityAttribute.array[ i * 3 + 1 ] = velY;
  248. velocityAttribute.array[ i * 3 + 2 ] = velZ;
  249. // color
  250. color.r = _Math.clamp( color.r + particleSystem.random() * colorRandomness, 0, 1 );
  251. color.g = _Math.clamp( color.g + particleSystem.random() * colorRandomness, 0, 1 );
  252. color.b = _Math.clamp( color.b + particleSystem.random() * colorRandomness, 0, 1 );
  253. colorAttribute.array[ i * 3 + 0 ] = color.r;
  254. colorAttribute.array[ i * 3 + 1 ] = color.g;
  255. colorAttribute.array[ i * 3 + 2 ] = color.b;
  256. // turbulence, size, lifetime and starttime
  257. turbulenceAttribute.array[ i ] = turbulence;
  258. sizeAttribute.array[ i ] = size + particleSystem.random() * sizeRandomness;
  259. lifeTimeAttribute.array[ i ] = lifetime;
  260. startTimeAttribute.array[ i ] = this.time + particleSystem.random() * 2e-2;
  261. // offset
  262. if ( this.offset === 0 ) {
  263. this.offset = this.PARTICLE_CURSOR;
  264. }
  265. // counter and cursor
  266. this.count ++;
  267. this.PARTICLE_CURSOR ++;
  268. if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) {
  269. this.PARTICLE_CURSOR = 0;
  270. }
  271. this.particleUpdate = true;
  272. };
  273. this.init = function () {
  274. this.particleSystem = new Points( this.particleShaderGeo, this.particleShaderMat );
  275. this.particleSystem.frustumCulled = false;
  276. this.add( this.particleSystem );
  277. };
  278. this.update = function ( time ) {
  279. this.time = time;
  280. this.particleShaderMat.uniforms.uTime.value = time;
  281. this.geometryUpdate();
  282. };
  283. this.geometryUpdate = function () {
  284. if ( this.particleUpdate === true ) {
  285. this.particleUpdate = false;
  286. var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' );
  287. var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' );
  288. var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' );
  289. var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' );
  290. var colorAttribute = this.particleShaderGeo.getAttribute( 'color' );
  291. var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' );
  292. var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' );
  293. if ( this.offset + this.count < this.PARTICLE_COUNT ) {
  294. positionStartAttribute.updateRange.offset = this.offset * positionStartAttribute.itemSize;
  295. startTimeAttribute.updateRange.offset = this.offset * startTimeAttribute.itemSize;
  296. velocityAttribute.updateRange.offset = this.offset * velocityAttribute.itemSize;
  297. turbulenceAttribute.updateRange.offset = this.offset * turbulenceAttribute.itemSize;
  298. colorAttribute.updateRange.offset = this.offset * colorAttribute.itemSize;
  299. sizeAttribute.updateRange.offset = this.offset * sizeAttribute.itemSize;
  300. lifeTimeAttribute.updateRange.offset = this.offset * lifeTimeAttribute.itemSize;
  301. positionStartAttribute.updateRange.count = this.count * positionStartAttribute.itemSize;
  302. startTimeAttribute.updateRange.count = this.count * startTimeAttribute.itemSize;
  303. velocityAttribute.updateRange.count = this.count * velocityAttribute.itemSize;
  304. turbulenceAttribute.updateRange.count = this.count * turbulenceAttribute.itemSize;
  305. colorAttribute.updateRange.count = this.count * colorAttribute.itemSize;
  306. sizeAttribute.updateRange.count = this.count * sizeAttribute.itemSize;
  307. lifeTimeAttribute.updateRange.count = this.count * lifeTimeAttribute.itemSize;
  308. } else {
  309. positionStartAttribute.updateRange.offset = 0;
  310. startTimeAttribute.updateRange.offset = 0;
  311. velocityAttribute.updateRange.offset = 0;
  312. turbulenceAttribute.updateRange.offset = 0;
  313. colorAttribute.updateRange.offset = 0;
  314. sizeAttribute.updateRange.offset = 0;
  315. lifeTimeAttribute.updateRange.offset = 0;
  316. // Use -1 to update the entire buffer, see #11476
  317. positionStartAttribute.updateRange.count = - 1;
  318. startTimeAttribute.updateRange.count = - 1;
  319. velocityAttribute.updateRange.count = - 1;
  320. turbulenceAttribute.updateRange.count = - 1;
  321. colorAttribute.updateRange.count = - 1;
  322. sizeAttribute.updateRange.count = - 1;
  323. lifeTimeAttribute.updateRange.count = - 1;
  324. }
  325. positionStartAttribute.needsUpdate = true;
  326. startTimeAttribute.needsUpdate = true;
  327. velocityAttribute.needsUpdate = true;
  328. turbulenceAttribute.needsUpdate = true;
  329. colorAttribute.needsUpdate = true;
  330. sizeAttribute.needsUpdate = true;
  331. lifeTimeAttribute.needsUpdate = true;
  332. this.offset = 0;
  333. this.count = 0;
  334. }
  335. };
  336. this.dispose = function () {
  337. this.particleShaderGeo.dispose();
  338. };
  339. this.init();
  340. };
  341. GPUParticleContainer.prototype = Object.create( Object3D.prototype );
  342. GPUParticleContainer.prototype.constructor = GPUParticleContainer;
  343. export { GPUParticleSystem, GPUParticleContainer };