Created
October 19, 2020 01:29
-
-
Save lisajamhoury/85887e6cf06af6c66916a89c17519204 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* GPU Particle System | |
* @author flimshaw - Charlie Hoey - http://charliehoey.com | |
* | |
* A simple to use, general purpose GPU system. Particles are spawn-and-forget with | |
* several options available, and do not require monitoring or cleanup after spawning. | |
* Because the paths of all particles are completely deterministic once spawned, the scale | |
* and direction of time is also variable. | |
* | |
* Currently uses a static wrapping perlin noise texture for turbulence, and a small png texture for | |
* particles, but adding support for a particle texture atlas or changing to a different type of turbulence | |
* would be a fairly light day's work. | |
* | |
* Shader and javascript packing code derrived from several Stack Overflow examples. | |
* | |
*/ | |
THREE.GPUParticleSystem = function ( options ) { | |
THREE.Object3D.apply( this, arguments ); | |
options = options || {}; | |
// parse options and use defaults | |
this.PARTICLE_COUNT = options.maxParticles || 1000000; | |
this.PARTICLE_CONTAINERS = options.containerCount || 1; | |
this.PARTICLE_NOISE_TEXTURE = options.particleNoiseTex || null; | |
this.PARTICLE_SPRITE_TEXTURE = options.particleSpriteTex || null; | |
this.PARTICLES_PER_CONTAINER = Math.ceil( this.PARTICLE_COUNT / this.PARTICLE_CONTAINERS ); | |
this.PARTICLE_CURSOR = 0; | |
this.time = 0; | |
this.particleContainers = []; | |
this.rand = []; | |
// custom vertex and fragement shader | |
var GPUParticleShader = { | |
vertexShader: [ | |
'uniform float uTime;', | |
'uniform float uScale;', | |
'uniform sampler2D tNoise;', | |
'attribute vec3 positionStart;', | |
'attribute float startTime;', | |
'attribute vec3 velocity;', | |
'attribute float turbulence;', | |
'attribute vec3 color;', | |
'attribute float size;', | |
'attribute float lifeTime;', | |
'varying vec4 vColor;', | |
'varying float lifeLeft;', | |
'void main() {', | |
// unpack things from our attributes' | |
' vColor = vec4( color, 1.0 );', | |
// convert our velocity back into a value we can use' | |
' vec3 newPosition;', | |
' vec3 v;', | |
' float timeElapsed = uTime - startTime;', | |
' lifeLeft = 1.0 - ( timeElapsed / lifeTime );', | |
' gl_PointSize = ( uScale * size ) * lifeLeft;', | |
' v.x = ( velocity.x - 0.5 ) * 3.0;', | |
' v.y = ( velocity.y - 0.5 ) * 3.0;', | |
' v.z = ( velocity.z - 0.5 ) * 3.0;', | |
' newPosition = positionStart + ( v * 10.0 ) * timeElapsed;', | |
' vec3 noise = texture2D( tNoise, vec2( newPosition.x * 0.015 + ( uTime * 0.05 ), newPosition.y * 0.02 + ( uTime * 0.015 ) ) ).rgb;', | |
' vec3 noiseVel = ( noise.rgb - 0.5 ) * 30.0;', | |
' newPosition = mix( newPosition, newPosition + vec3( noiseVel * ( turbulence * 5.0 ) ), ( timeElapsed / lifeTime ) );', | |
' if( v.y > 0. && v.y < .05 ) {', | |
' lifeLeft = 0.0;', | |
' }', | |
' if( v.x < - 1.45 ) {', | |
' lifeLeft = 0.0;', | |
' }', | |
' if( timeElapsed > 0.0 ) {', | |
' gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );', | |
' } else {', | |
' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', | |
' lifeLeft = 0.0;', | |
' gl_PointSize = 0.;', | |
' }', | |
'}' | |
].join( '\n' ), | |
fragmentShader: [ | |
'float scaleLinear( float value, vec2 valueDomain ) {', | |
' return ( value - valueDomain.x ) / ( valueDomain.y - valueDomain.x );', | |
'}', | |
'float scaleLinear( float value, vec2 valueDomain, vec2 valueRange ) {', | |
' return mix( valueRange.x, valueRange.y, scaleLinear( value, valueDomain ) );', | |
'}', | |
'varying vec4 vColor;', | |
'varying float lifeLeft;', | |
'uniform sampler2D tSprite;', | |
'void main() {', | |
' float alpha = 0.;', | |
' if( lifeLeft > 0.995 ) {', | |
' alpha = scaleLinear( lifeLeft, vec2( 1.0, 0.995 ), vec2( 0.0, 1.0 ) );', | |
' } else {', | |
' alpha = lifeLeft * 0.75;', | |
' }', | |
' vec4 tex = texture2D( tSprite, gl_PointCoord );', | |
' gl_FragColor = vec4( vColor.rgb * tex.a, alpha * tex.a );', | |
'}' | |
].join( '\n' ) | |
}; | |
// preload a million random numbers | |
var i; | |
for ( i = 1e5; i > 0; i -- ) { | |
this.rand.push( Math.random() - 0.5 ); | |
} | |
this.random = function () { | |
return ++ i >= this.rand.length ? this.rand[ i = 1 ] : this.rand[ i ]; | |
}; | |
var textureLoader = new THREE.TextureLoader(); | |
this.particleNoiseTex = this.PARTICLE_NOISE_TEXTURE || textureLoader.load( 'textures/perlin-512.png' ); | |
this.particleNoiseTex.wrapS = this.particleNoiseTex.wrapT = THREE.RepeatWrapping; | |
this.particleSpriteTex = this.PARTICLE_SPRITE_TEXTURE || textureLoader.load( 'textures/particle2.png' ); | |
this.particleSpriteTex.wrapS = this.particleSpriteTex.wrapT = THREE.RepeatWrapping; | |
this.particleShaderMat = new THREE.ShaderMaterial( { | |
transparent: true, | |
depthWrite: false, | |
uniforms: { | |
'uTime': { | |
value: 0.0 | |
}, | |
'uScale': { | |
value: 1.0 | |
}, | |
'tNoise': { | |
value: this.particleNoiseTex | |
}, | |
'tSprite': { | |
value: this.particleSpriteTex | |
} | |
}, | |
blending: THREE.AdditiveBlending, | |
vertexShader: GPUParticleShader.vertexShader, | |
fragmentShader: GPUParticleShader.fragmentShader | |
} ); | |
// define defaults for all values | |
this.particleShaderMat.defaultAttributeValues.particlePositionsStartTime = [ 0, 0, 0, 0 ]; | |
this.particleShaderMat.defaultAttributeValues.particleVelColSizeLife = [ 0, 0, 0, 0 ]; | |
this.init = function () { | |
for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) { | |
var c = new THREE.GPUParticleContainer( this.PARTICLES_PER_CONTAINER, this ); | |
this.particleContainers.push( c ); | |
this.add( c ); | |
} | |
}; | |
this.spawnParticle = function ( options ) { | |
this.PARTICLE_CURSOR ++; | |
if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) { | |
this.PARTICLE_CURSOR = 1; | |
} | |
var currentContainer = this.particleContainers[ Math.floor( this.PARTICLE_CURSOR / this.PARTICLES_PER_CONTAINER ) ]; | |
currentContainer.spawnParticle( options ); | |
}; | |
this.update = function ( time ) { | |
for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) { | |
this.particleContainers[ i ].update( time ); | |
} | |
}; | |
this.dispose = function () { | |
this.particleShaderMat.dispose(); | |
this.particleNoiseTex.dispose(); | |
this.particleSpriteTex.dispose(); | |
for ( var i = 0; i < this.PARTICLE_CONTAINERS; i ++ ) { | |
this.particleContainers[ i ].dispose(); | |
} | |
}; | |
this.init(); | |
}; | |
THREE.GPUParticleSystem.prototype = Object.create( THREE.Object3D.prototype ); | |
THREE.GPUParticleSystem.prototype.constructor = THREE.GPUParticleSystem; | |
// Subclass for particle containers, allows for very large arrays to be spread out | |
THREE.GPUParticleContainer = function ( maxParticles, particleSystem ) { | |
THREE.Object3D.apply( this, arguments ); | |
this.PARTICLE_COUNT = maxParticles || 100000; | |
this.PARTICLE_CURSOR = 0; | |
this.time = 0; | |
this.offset = 0; | |
this.count = 0; | |
this.DPR = window.devicePixelRatio; | |
this.GPUParticleSystem = particleSystem; | |
this.particleUpdate = false; | |
// geometry | |
this.particleShaderGeo = new THREE.BufferGeometry(); | |
this.particleShaderGeo.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'positionStart', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'startTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'velocity', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'turbulence', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'color', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT * 3 ), 3 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'size', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); | |
this.particleShaderGeo.addAttribute( 'lifeTime', new THREE.BufferAttribute( new Float32Array( this.PARTICLE_COUNT ), 1 ).setDynamic( true ) ); | |
// material | |
this.particleShaderMat = this.GPUParticleSystem.particleShaderMat; | |
var position = new THREE.Vector3(); | |
var velocity = new THREE.Vector3(); | |
var color = new THREE.Color(); | |
this.spawnParticle = function ( options ) { | |
var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' ); | |
var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' ); | |
var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' ); | |
var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' ); | |
var colorAttribute = this.particleShaderGeo.getAttribute( 'color' ); | |
var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' ); | |
var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' ); | |
options = options || {}; | |
// setup reasonable default values for all arguments | |
position = options.position !== undefined ? position.copy( options.position ) : position.set( 0, 0, 0 ); | |
velocity = options.velocity !== undefined ? velocity.copy( options.velocity ) : velocity.set( 0, 0, 0 ); | |
color = options.color !== undefined ? color.set( options.color ) : color.set( 0xffffff ); | |
var positionRandomness = options.positionRandomness !== undefined ? options.positionRandomness : 0; | |
var velocityRandomness = options.velocityRandomness !== undefined ? options.velocityRandomness : 0; | |
var colorRandomness = options.colorRandomness !== undefined ? options.colorRandomness : 1; | |
var turbulence = options.turbulence !== undefined ? options.turbulence : 1; | |
var lifetime = options.lifetime !== undefined ? options.lifetime : 5; | |
var size = options.size !== undefined ? options.size : 10; | |
var sizeRandomness = options.sizeRandomness !== undefined ? options.sizeRandomness : 0; | |
var smoothPosition = options.smoothPosition !== undefined ? options.smoothPosition : false; | |
if ( this.DPR !== undefined ) size *= this.DPR; | |
var i = this.PARTICLE_CURSOR; | |
// position | |
positionStartAttribute.array[ i * 3 + 0 ] = position.x + ( particleSystem.random() * positionRandomness ); | |
positionStartAttribute.array[ i * 3 + 1 ] = position.y + ( particleSystem.random() * positionRandomness ); | |
positionStartAttribute.array[ i * 3 + 2 ] = position.z + ( particleSystem.random() * positionRandomness ); | |
if ( smoothPosition === true ) { | |
positionStartAttribute.array[ i * 3 + 0 ] += - ( velocity.x * particleSystem.random() ); | |
positionStartAttribute.array[ i * 3 + 1 ] += - ( velocity.y * particleSystem.random() ); | |
positionStartAttribute.array[ i * 3 + 2 ] += - ( velocity.z * particleSystem.random() ); | |
} | |
// velocity | |
var maxVel = 2; | |
var velX = velocity.x + particleSystem.random() * velocityRandomness; | |
var velY = velocity.y + particleSystem.random() * velocityRandomness; | |
var velZ = velocity.z + particleSystem.random() * velocityRandomness; | |
velX = THREE.Math.clamp( ( velX - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); | |
velY = THREE.Math.clamp( ( velY - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); | |
velZ = THREE.Math.clamp( ( velZ - ( - maxVel ) ) / ( maxVel - ( - maxVel ) ), 0, 1 ); | |
velocityAttribute.array[ i * 3 + 0 ] = velX; | |
velocityAttribute.array[ i * 3 + 1 ] = velY; | |
velocityAttribute.array[ i * 3 + 2 ] = velZ; | |
// color | |
color.r = THREE.Math.clamp( color.r + particleSystem.random() * colorRandomness, 0, 1 ); | |
color.g = THREE.Math.clamp( color.g + particleSystem.random() * colorRandomness, 0, 1 ); | |
color.b = THREE.Math.clamp( color.b + particleSystem.random() * colorRandomness, 0, 1 ); | |
colorAttribute.array[ i * 3 + 0 ] = color.r; | |
colorAttribute.array[ i * 3 + 1 ] = color.g; | |
colorAttribute.array[ i * 3 + 2 ] = color.b; | |
// turbulence, size, lifetime and starttime | |
turbulenceAttribute.array[ i ] = turbulence; | |
sizeAttribute.array[ i ] = size + particleSystem.random() * sizeRandomness; | |
lifeTimeAttribute.array[ i ] = lifetime; | |
startTimeAttribute.array[ i ] = this.time + particleSystem.random() * 2e-2; | |
// offset | |
if ( this.offset === 0 ) { | |
this.offset = this.PARTICLE_CURSOR; | |
} | |
// counter and cursor | |
this.count ++; | |
this.PARTICLE_CURSOR ++; | |
if ( this.PARTICLE_CURSOR >= this.PARTICLE_COUNT ) { | |
this.PARTICLE_CURSOR = 0; | |
} | |
this.particleUpdate = true; | |
}; | |
this.init = function () { | |
this.particleSystem = new THREE.Points( this.particleShaderGeo, this.particleShaderMat ); | |
this.particleSystem.frustumCulled = false; | |
this.add( this.particleSystem ); | |
}; | |
this.update = function ( time ) { | |
this.time = time; | |
this.particleShaderMat.uniforms.uTime.value = time; | |
this.geometryUpdate(); | |
}; | |
this.geometryUpdate = function () { | |
if ( this.particleUpdate === true ) { | |
this.particleUpdate = false; | |
var positionStartAttribute = this.particleShaderGeo.getAttribute( 'positionStart' ); | |
var startTimeAttribute = this.particleShaderGeo.getAttribute( 'startTime' ); | |
var velocityAttribute = this.particleShaderGeo.getAttribute( 'velocity' ); | |
var turbulenceAttribute = this.particleShaderGeo.getAttribute( 'turbulence' ); | |
var colorAttribute = this.particleShaderGeo.getAttribute( 'color' ); | |
var sizeAttribute = this.particleShaderGeo.getAttribute( 'size' ); | |
var lifeTimeAttribute = this.particleShaderGeo.getAttribute( 'lifeTime' ); | |
if ( this.offset + this.count < this.PARTICLE_COUNT ) { | |
positionStartAttribute.updateRange.offset = this.offset * positionStartAttribute.itemSize; | |
startTimeAttribute.updateRange.offset = this.offset * startTimeAttribute.itemSize; | |
velocityAttribute.updateRange.offset = this.offset * velocityAttribute.itemSize; | |
turbulenceAttribute.updateRange.offset = this.offset * turbulenceAttribute.itemSize; | |
colorAttribute.updateRange.offset = this.offset * colorAttribute.itemSize; | |
sizeAttribute.updateRange.offset = this.offset * sizeAttribute.itemSize; | |
lifeTimeAttribute.updateRange.offset = this.offset * lifeTimeAttribute.itemSize; | |
positionStartAttribute.updateRange.count = this.count * positionStartAttribute.itemSize; | |
startTimeAttribute.updateRange.count = this.count * startTimeAttribute.itemSize; | |
velocityAttribute.updateRange.count = this.count * velocityAttribute.itemSize; | |
turbulenceAttribute.updateRange.count = this.count * turbulenceAttribute.itemSize; | |
colorAttribute.updateRange.count = this.count * colorAttribute.itemSize; | |
sizeAttribute.updateRange.count = this.count * sizeAttribute.itemSize; | |
lifeTimeAttribute.updateRange.count = this.count * lifeTimeAttribute.itemSize; | |
} else { | |
positionStartAttribute.updateRange.offset = 0; | |
startTimeAttribute.updateRange.offset = 0; | |
velocityAttribute.updateRange.offset = 0; | |
turbulenceAttribute.updateRange.offset = 0; | |
colorAttribute.updateRange.offset = 0; | |
sizeAttribute.updateRange.offset = 0; | |
lifeTimeAttribute.updateRange.offset = 0; | |
// Use -1 to update the entire buffer, see #11476 | |
positionStartAttribute.updateRange.count = - 1; | |
startTimeAttribute.updateRange.count = - 1; | |
velocityAttribute.updateRange.count = - 1; | |
turbulenceAttribute.updateRange.count = - 1; | |
colorAttribute.updateRange.count = - 1; | |
sizeAttribute.updateRange.count = - 1; | |
lifeTimeAttribute.updateRange.count = - 1; | |
} | |
positionStartAttribute.needsUpdate = true; | |
startTimeAttribute.needsUpdate = true; | |
velocityAttribute.needsUpdate = true; | |
turbulenceAttribute.needsUpdate = true; | |
colorAttribute.needsUpdate = true; | |
sizeAttribute.needsUpdate = true; | |
lifeTimeAttribute.needsUpdate = true; | |
this.offset = 0; | |
this.count = 0; | |
} | |
}; | |
this.dispose = function () { | |
this.particleShaderGeo.dispose(); | |
}; | |
this.init(); | |
}; | |
THREE.GPUParticleContainer.prototype = Object.create( THREE.Object3D.prototype ); | |
THREE.GPUParticleContainer.prototype.constructor = THREE.GPUParticleContainer; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment