GPUParticleSystem.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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. THREE.GPUParticleSystem = function( options ) {
  18. THREE.Object3D.apply( this, arguments );
  19. options = options || {};
  20. // parse options and use defaults
  21. this.PARTICLE_COUNT = options.maxParticles || 1000000;
  22. this.PARTICLE_CONTAINERS = options.containerCount || 1;
  23. this.PARTICLE_NOISE_TEXTURE = options.particleNoiseTex || null;
  24. this.PARTICLE_SPRITE_TEXTURE = options.particleSpriteTex || null;
  25. this.PARTICLES_PER_CONTAINER = Math.ceil( this.PARTICLE_COUNT / this.PARTICLE_CONTAINERS );
  26. this.PARTICLE_CURSOR = 0;
  27. this.time = 0;
  28. // custom vertex and fragement shader
  29. var GPUParticleShader = {
  30. vertexShader: [
  31. 'precision highp float;',
  32. '#define FLOAT_MAX 1.70141184e38',
  33. '#define FLOAT_MIN 1.17549435e-38',
  34. 'lowp vec4 encode_float( highp float v ) {',
  35. ' highp float av = abs( v );',
  36. // handle special cases
  37. ' if( av < FLOAT_MIN ) {',
  38. ' return vec4( 0.0 );',
  39. ' } else if ( v > FLOAT_MAX ) {',
  40. ' return vec4( 127.0, 128.0, 0.0, 0.0 ) / 255.0;',
  41. ' } else if( v < -FLOAT_MAX ) {',
  42. ' return vec4( 255.0, 128.0, 0.0, 0.0 ) / 255.0;',
  43. ' }',
  44. ' highp vec4 c = vec4( 0 );',
  45. // compute exponent and mantissa
  46. ' highp float e = floor( log2( av ) );',
  47. ' highp float m = av * pow( 2.0, - e ) - 1.0;',
  48. // unpack mantissa
  49. ' c[ 1 ] = floor( 128.0 * m );',
  50. ' m -= c[ 1 ] / 128.0;',
  51. ' c[ 2 ] = floor( 32768.0 * m );',
  52. ' m -= c[ 2 ] / 32768.0;',
  53. ' c[ 3 ] = floor( 8388608.0 * m );',
  54. // unpack exponent
  55. ' highp float ebias = e + 127.0;',
  56. ' c[ 0 ] = floor( ebias / 2.0 );',
  57. ' ebias -= c[ 0 ] * 2.0;',
  58. ' c[ 1 ] += floor( ebias ) * 128.0;',
  59. // unpack sign bit
  60. ' c[ 0 ] += 128.0 * step( 0.0, - v );',
  61. // scale back to range
  62. ' return c / 255.0;',
  63. '}',
  64. 'uniform float uTime;',
  65. 'uniform float uScale;',
  66. 'uniform sampler2D tNoise;',
  67. 'attribute vec4 particlePositionsStartTime;',
  68. 'attribute vec4 particleVelColSizeLife;',
  69. 'varying vec4 vColor;',
  70. 'varying float lifeLeft;',
  71. 'void main() {',
  72. // unpack things from our attributes'
  73. ' vColor = encode_float( particleVelColSizeLife.y );',
  74. // convert our velocity back into a value we can use'
  75. ' vec4 velTurb = encode_float( particleVelColSizeLife.x );',
  76. ' vec3 velocity = vec3( velTurb.xyz );',
  77. ' float turbulence = velTurb.w;',
  78. ' vec3 newPosition;',
  79. ' float timeElapsed = uTime - particlePositionsStartTime.a;',
  80. ' lifeLeft = 1.0 - ( timeElapsed / particleVelColSizeLife.w );',
  81. ' gl_PointSize = ( uScale * particleVelColSizeLife.z ) * lifeLeft;',
  82. ' velocity.x = ( velocity.x - 0.5 ) * 3.0;',
  83. ' velocity.y = ( velocity.y - 0.5 ) * 3.0;',
  84. ' velocity.z = ( velocity.z - 0.5 ) * 3.0;',
  85. ' newPosition = particlePositionsStartTime.xyz + ( velocity * 10.0 ) * ( uTime - particlePositionsStartTime.a );',
  86. ' vec3 noise = texture2D( tNoise, vec2( newPosition.x * 0.015 + ( uTime * 0.05 ), newPosition.y * 0.02 + ( uTime * 0.015 ) ) ).rgb;',
  87. ' vec3 noiseVel = ( noise.rgb - 0.5 ) * 30.0;',
  88. ' newPosition = mix( newPosition, newPosition + vec3( noiseVel * ( turbulence * 5.0 ) ), ( timeElapsed / particleVelColSizeLife.a ) );',
  89. ' if( velocity.y > 0. && velocity.y < .05 ) {',
  90. ' lifeLeft = 0.0;',
  91. ' }',
  92. ' if( velocity.x < - 1.45 ) {',
  93. ' lifeLeft = 0.0;',
  94. ' }',
  95. ' if( timeElapsed > 0.0 ) {',
  96. ' gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );',
  97. ' } else {',
  98. ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
  99. ' lifeLeft = 0.0;',
  100. ' gl_PointSize = 0.;',
  101. ' }',
  102. '}'
  103. ].join( '\n' ),
  104. fragmentShader: [
  105. 'float scaleLinear( float value, vec2 valueDomain ) {',
  106. ' return ( value - valueDomain.x ) / ( valueDomain.y - valueDomain.x );',
  107. '}',
  108. 'float scaleLinear( float value, vec2 valueDomain, vec2 valueRange ) {',
  109. ' return mix( valueRange.x, valueRange.y, scaleLinear( value, valueDomain ) );',
  110. '}',
  111. 'varying vec4 vColor;',
  112. 'varying float lifeLeft;',
  113. 'uniform sampler2D tSprite;',
  114. 'void main() {',
  115. 'float alpha = 0.;',
  116. 'if( lifeLeft > 0.995 ) {',
  117. ' alpha = scaleLinear( lifeLeft, vec2( 1.0, 0.995 ), vec2( 0.0, 1.0 ) );',
  118. '} else {',
  119. ' alpha = lifeLeft * 0.75;',
  120. '}',
  121. 'vec4 tex = texture2D( tSprite, gl_PointCoord );',
  122. 'gl_FragColor = vec4( vColor.rgb * tex.a, alpha * tex.a );',
  123. '}'
  124. ].join( '\n' )
  125. };
  126. // preload a million random numbers
  127. this.rand = [];
  128. var i;
  129. for ( i = 1e5; i > 0; i-- ) {
  130. this.rand.push( Math.random() - 0.5 );
  131. }
  132. this.random = function() {
  133. return ++ i >= this.rand.length ? this.rand[ i = 1 ] : this.rand[ i ];
  134. };
  135. var textureLoader = new THREE.TextureLoader();
  136. this.particleNoiseTex = this.PARTICLE_NOISE_TEXTURE || textureLoader.load( 'textures/perlin-512.png' );
  137. this.particleNoiseTex.wrapS = this.particleNoiseTex.wrapT = THREE.RepeatWrapping;
  138. this.particleSpriteTex = this.PARTICLE_SPRITE_TEXTURE || textureLoader.load( 'textures/particle2.png' );
  139. this.particleSpriteTex.wrapS = this.particleSpriteTex.wrapT = THREE.RepeatWrapping;
  140. this.particleShaderMat = new THREE.ShaderMaterial( {
  141. transparent: true,
  142. depthWrite: false,
  143. uniforms: {
  144. 'uTime': {
  145. value: 0.0
  146. },
  147. 'uScale': {
  148. value: 1.0
  149. },
  150. 'tNoise': {
  151. value: this.particleNoiseTex
  152. },
  153. 'tSprite': {
  154. value: this.particleSpriteTex
  155. }
  156. },
  157. blending: THREE.AdditiveBlending,
  158. vertexShader: GPUParticleShader.vertexShader,
  159. fragmentShader: GPUParticleShader.fragmentShader
  160. } );
  161. // define defaults for all values
  162. this.particleShaderMat.defaultAttributeValues.particlePositionsStartTime = [ 0, 0, 0, 0 ];
  163. this.particleShaderMat.defaultAttributeValues.particleVelColSizeLife = [ 0, 0, 0, 0 ];
  164. this.particleContainers = [];
  165. this.init = function() {
  166. for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
  167. var c = new THREE.GPUParticleContainer( this.PARTICLES_PER_CONTAINER, this );
  168. this.particleContainers.push( c );
  169. this.add( c );
  170. }
  171. };
  172. this.spawnParticle = function( options ) {
  173. this.PARTICLE_CURSOR ++;
  174. if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) {
  175. this.PARTICLE_CURSOR = 1;
  176. }
  177. var currentContainer = this.particleContainers[ Math.floor( this.PARTICLE_CURSOR / this.PARTICLES_PER_CONTAINER ) ];
  178. currentContainer.spawnParticle( options );
  179. };
  180. this.update = function( time ) {
  181. for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
  182. this.particleContainers[ i ].update( time );
  183. }
  184. };
  185. this.dispose = function() {
  186. this.particleShaderMat.dispose();
  187. this.particleNoiseTex.dispose();
  188. this.particleSpriteTex.dispose();
  189. for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) {
  190. this.particleContainers[ i ].dispose();
  191. }
  192. };
  193. this.init();
  194. };
  195. THREE.GPUParticleSystem.prototype = Object.create( THREE.Object3D.prototype );
  196. THREE.GPUParticleSystem.prototype.constructor = THREE.GPUParticleSystem;
  197. // Subclass for particle containers, allows for very large arrays to be spread out
  198. THREE.GPUParticleContainer = function( maxParticles, particleSystem ) {
  199. THREE.Object3D.apply( this, arguments );
  200. this.PARTICLE_COUNT = maxParticles || 100000;
  201. this.PARTICLE_CURSOR = 0;
  202. this.time = 0;
  203. this.DPR = window.devicePixelRatio;
  204. this.GPUParticleSystem = particleSystem;
  205. // construct a couple small arrays used for packing variables into floats etc.
  206. var UINT8_VIEW = new Uint8Array( 4 );
  207. var FLOAT_VIEW = new Float32Array( UINT8_VIEW.buffer );
  208. function decodeFloat( x, y, z, w ) {
  209. UINT8_VIEW[ 0 ] = Math.floor( w );
  210. UINT8_VIEW[ 1 ] = Math.floor( z );
  211. UINT8_VIEW[ 2 ] = Math.floor( y );
  212. UINT8_VIEW[ 3 ] = Math.floor( x );
  213. return FLOAT_VIEW[ 0 ];
  214. }
  215. function hexToRgb( hex ) {
  216. var r = hex >> 16;
  217. var g = ( hex & 0x00FF00 ) >> 8;
  218. var b = hex & 0x0000FF;
  219. if ( r > 0 ) r--;
  220. if ( g > 0 ) g--;
  221. if ( b > 0 ) b--;
  222. return [ r, g, b ];
  223. }
  224. this.particles = [];
  225. this.deadParticles = [];
  226. this.particlesAvailableSlot = [];
  227. // create a container for particles
  228. this.particleUpdate = false;
  229. // shader based particle system
  230. this.particleShaderGeo = new THREE.BufferGeometry();
  231. // new hyper compressed attributes
  232. this.particleVertices = new Float32Array( this.PARTICLE_COUNT * 3 ); // position
  233. this.particlePositionsStartTime = new Float32Array( this.PARTICLE_COUNT * 4 ); // position
  234. this.particleVelColSizeLife = new Float32Array( this.PARTICLE_COUNT * 4 );
  235. var i;
  236. for ( i = 0; i < this.PARTICLE_COUNT; i ++ ) {
  237. this.particlePositionsStartTime[ i * 4 + 0 ] = 100; // x
  238. this.particlePositionsStartTime[ i * 4 + 1 ] = 0; // y
  239. this.particlePositionsStartTime[ i * 4 + 2 ] = 0.0; // z
  240. this.particlePositionsStartTime[ i * 4 + 3 ] = 0.0; // startTime
  241. this.particleVertices[ i * 3 + 0 ] = 0; // x
  242. this.particleVertices[ i * 3 + 1 ] = 0; // y
  243. this.particleVertices[ i * 3 + 2 ] = 0.0; // z
  244. this.particleVelColSizeLife[ i * 4 + 0 ] = decodeFloat( 128, 128, 0, 0 ); // velocity
  245. this.particleVelColSizeLife[ i * 4 + 1 ] = decodeFloat( 0, 254, 0, 254 ); // color
  246. this.particleVelColSizeLife[ i * 4 + 2 ] = 1.0; // size
  247. this.particleVelColSizeLife[ i * 4 + 3 ] = 0.0; // lifespan
  248. }
  249. this.particleShaderGeo.addAttribute( 'position', new THREE.BufferAttribute( this.particleVertices, 3 ) );
  250. this.particleShaderGeo.addAttribute( 'particlePositionsStartTime', new THREE.BufferAttribute( this.particlePositionsStartTime, 4 ).setDynamic( true ) );
  251. this.particleShaderGeo.addAttribute( 'particleVelColSizeLife', new THREE.BufferAttribute( this.particleVelColSizeLife, 4 ).setDynamic( true ) );
  252. this.posStart = this.particleShaderGeo.getAttribute( 'particlePositionsStartTime' );
  253. this.velCol = this.particleShaderGeo.getAttribute( 'particleVelColSizeLife' );
  254. this.particleShaderMat = this.GPUParticleSystem.particleShaderMat;
  255. this.init = function() {
  256. this.particleSystem = new THREE.Points( this.particleShaderGeo, this.particleShaderMat );
  257. this.particleSystem.frustumCulled = false;
  258. this.add( this.particleSystem );
  259. };
  260. var options = {},
  261. position = new THREE.Vector3(),
  262. velocity = new THREE.Vector3(),
  263. positionRandomness = 0,
  264. velocityRandomness = 0,
  265. color = 0xffffff,
  266. colorRandomness = 0,
  267. turbulence = 0,
  268. lifetime = 0,
  269. size = 0,
  270. sizeRandomness = 0,
  271. smoothPosition = false;
  272. var maxVel = 2;
  273. var maxSource = 250;
  274. this.offset = 0;
  275. this.count = 0;
  276. this.spawnParticle = function( options ) {
  277. options = options || {};
  278. // setup reasonable default values for all arguments
  279. position = options.position !== undefined ? position.copy( options.position ) : position.set( 0, 0, 0 );
  280. velocity = options.velocity !== undefined ? velocity.copy( options.velocity ) : velocity.set( 0, 0, 0 );
  281. positionRandomness = options.positionRandomness !== undefined ? options.positionRandomness : 0;
  282. velocityRandomness = options.velocityRandomness !== undefined ? options.velocityRandomness : 0;
  283. color = options.color !== undefined ? options.color : 0xffffff;
  284. colorRandomness = options.colorRandomness !== undefined ? options.colorRandomness : 1;
  285. turbulence = options.turbulence !== undefined ? options.turbulence : 1;
  286. lifetime = options.lifetime !== undefined ? options.lifetime : 5;
  287. size = options.size !== undefined ? options.size : 10;
  288. sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0;
  289. smoothPosition = options.smoothPosition !== undefined ? options.smoothPosition : false;
  290. if ( this.DPR !== undefined ) size *= this.DPR;
  291. i = this.PARTICLE_CURSOR;
  292. this.posStart.array[ i * 4 + 0 ] = position.x + ( ( particleSystem.random() ) * positionRandomness ); // - ( velocity.x * particleSystem.random() ); //x
  293. this.posStart.array[ i * 4 + 1 ] = position.y + ( ( particleSystem.random() ) * positionRandomness ); // - ( velocity.y * particleSystem.random() ); //y
  294. this.posStart.array[ i * 4 + 2 ] = position.z + ( ( particleSystem.random() ) * positionRandomness ); // - ( velocity.z * particleSystem.random() ); //z
  295. this.posStart.array[ i * 4 + 3 ] = this.time + ( particleSystem.random() * 2e-2 ); //startTime
  296. if ( smoothPosition === true ) {
  297. this.posStart.array[ i * 4 + 0 ] += - ( velocity.x * particleSystem.random() ); //x
  298. this.posStart.array[ i * 4 + 1 ] += - ( velocity.y * particleSystem.random() ); //y
  299. this.posStart.array[ i * 4 + 2 ] += - ( velocity.z * particleSystem.random() ); //z
  300. }
  301. var velX = velocity.x + ( particleSystem.random() ) * velocityRandomness;
  302. var velY = velocity.y + ( particleSystem.random() ) * velocityRandomness;
  303. var velZ = velocity.z + ( particleSystem.random() ) * velocityRandomness;
  304. // convert turbulence rating to something we can pack into a vec4
  305. var turbulence = Math.floor( turbulence * 254 );
  306. // clamp our value to between 0.0 and 1.0
  307. velX = Math.floor( maxSource * ( ( velX - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ) ) );
  308. velY = Math.floor( maxSource * ( ( velY - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ) ) );
  309. velZ = Math.floor( maxSource * ( ( velZ - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ) ) );
  310. this.velCol.array[ i * 4 + 0 ] = decodeFloat( velX, velY, velZ, turbulence ); // vel
  311. var rgb = hexToRgb( color );
  312. for ( var c = 0; c < rgb.length; c ++ ) {
  313. rgb[ c ] = Math.floor( rgb[ c ] + ( ( particleSystem.random() ) * colorRandomness ) * 254 );
  314. if ( rgb[ c ] > 254 ) rgb[ c ] = 254;
  315. if ( rgb[ c ] < 0 ) rgb[ c ] = 0;
  316. }
  317. this.velCol.array[ i * 4 + 1 ] = decodeFloat( rgb[ 0 ], rgb[ 1 ], rgb[ 2 ], 254 ); // color
  318. this.velCol.array[ i * 4 + 2 ] = size + ( particleSystem.random() ) * sizeRandomness; // size
  319. this.velCol.array[ i * 4 + 3 ] = lifetime; // lifespan
  320. if ( this.offset === 0 ) {
  321. this.offset = this.PARTICLE_CURSOR;
  322. }
  323. this.count ++;
  324. this.PARTICLE_CURSOR ++;
  325. if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) {
  326. this.PARTICLE_CURSOR = 0;
  327. }
  328. this.particleUpdate = true;
  329. };
  330. this.update = function( time ) {
  331. this.time = time;
  332. this.particleShaderMat.uniforms.uTime.value = time;
  333. this.geometryUpdate();
  334. };
  335. this.geometryUpdate = function() {
  336. if ( this.particleUpdate === true ) {
  337. this.particleUpdate = false;
  338. // if we can get away with a partial buffer update, do so
  339. if ( ( this.offset + this.count ) < this.PARTICLE_COUNT ) {
  340. this.posStart.updateRange.offset = this.velCol.updateRange.offset = this.offset * 4;
  341. this.posStart.updateRange.count = this.velCol.updateRange.count = this.count * 4;
  342. } else {
  343. this.posStart.updateRange.offset = 0;
  344. this.posStart.updateRange.count = this.velCol.updateRange.count = ( this.PARTICLE_COUNT * 4 );
  345. }
  346. this.posStart.needsUpdate = true;
  347. this.velCol.needsUpdate = true;
  348. this.offset = 0;
  349. this.count = 0;
  350. }
  351. };
  352. this.dispose = function() {
  353. this.particleShaderGeo.dispose();
  354. };
  355. this.init();
  356. };
  357. THREE.GPUParticleContainer.prototype = Object.create( THREE.Object3D.prototype );
  358. THREE.GPUParticleContainer.prototype.constructor = THREE.GPUParticleContainer;