diff --git a/debug/app.coffee b/debug/app.coffee index c18f089..6b087bb 100644 --- a/debug/app.coffee +++ b/debug/app.coffee @@ -6,11 +6,24 @@ MeshPhongMaterial MeshNormalMaterial Light + ParticleSystem } = require '../form.coffee' scene = new Scene width: Screen.width height: Screen.height + camera: + orbitControls: true + enableRotate: true + +ps = new ParticleSystem + parent: scene + color: 0xff4444 + particleNoiseTexture: './images/perlin-512.png' + particleSpriteTexture: './images/particle2.png' + + +### new Model path: './models/flamingo/flamingo.json' @@ -45,4 +58,6 @@ new Model curve: 'easeInOutQuart' scene.onClick -> - scene.camera.stateCycle('first', 'second', 'third') \ No newline at end of file + scene.camera.stateCycle('first', 'second', 'third') + +### \ No newline at end of file diff --git a/debug/images/particle2.png b/debug/images/particle2.png new file mode 100644 index 0000000..d0b0251 Binary files /dev/null and b/debug/images/particle2.png differ diff --git a/debug/images/perlin-512.png b/debug/images/perlin-512.png new file mode 100644 index 0000000..11ed410 Binary files /dev/null and b/debug/images/perlin-512.png differ diff --git a/form.coffee b/form.coffee index a783327..d2abd9e 100644 --- a/form.coffee +++ b/form.coffee @@ -6,6 +6,7 @@ require './form/GA.coffee' {Model} = require './form/Model.coffee' {Mesh} = require './form/Mesh.coffee' {Light} = require './form/Light.coffee' +{ParticleSystem} = require './form/ParticleSystem.coffee' module.exports = Scene: Scene @@ -13,6 +14,7 @@ module.exports = Model: Model Mesh: Mesh Light: Light + ParticleSystem: ParticleSystem # MATERIALS diff --git a/form/ParticleSystem.coffee b/form/ParticleSystem.coffee new file mode 100644 index 0000000..b1f0f7a --- /dev/null +++ b/form/ParticleSystem.coffee @@ -0,0 +1,83 @@ +require './lib/GPUParticleSystem.js' + +_ = Framer._ + +{BaseClass} = require './_BaseClass.coffee' +{Animation} = require './_Animation.coffee' +{States} = require './_States.coffee' + +class exports.ParticleSystem extends BaseClass + constructor: (properties={}) -> + super() + + _.defaults properties, + position: new THREE.Vector3() + positionRandomness: .3 + velocity: new THREE.Vector3() + velocityRandomness: .5 + color: 0xaa88ff + colorRandomness: .2 + turbulence: .5 + lifetime: 2 + size: 5 + sizeRandomness: 1 + spawnRate: 15000 + horizontalSpeed: 1.5 + verticalSpeed: 1.33 + timeScale: 1 + particleNoiseTexture: './modules/form/lib/textures/perlin-512.png' + particleSpriteTexture: './modules/form/lib/textures/particle2.png' + + @properties = properties + + @clock = new THREE.Clock + @tick = 0 + @textureLoader = new THREE.TextureLoader() + + @particleSystem = new THREE.GPUParticleSystem + maxParticles: 250000 + particleNoiseTex: @textureLoader.load properties.particleNoiseTexture + particleSpriteTex: @textureLoader.load properties.particleSpriteTexture + + @addToRenderingInstance properties.parent + + Framer.CurrentContext.on 'reset', => + cancelAnimationFrame @animationLoopRequestId + + @animationLoopRequestId = requestAnimationFrame @loop + + addToRenderingInstance: (parent) -> + if parent.scene then parent.scene.add @particleSystem + else parent.add @particleSystem + + loop: () => + + @animationLoopRequestId = requestAnimationFrame @loop + + delta = @clock.getDelta() * @properties.timeScale + @tick += delta + + if @tick < 0 + @tick = 0 + + if delta > 0 + + @properties.position.x = Math.sin( @tick * @properties.horizontalSpeed ) * 20 + @properties.position.y = Math.sin( @tick * @properties.verticalSpeed ) * 10 + @properties.position.z = Math.sin( @tick * @properties.horizontalSpeed + @properties.verticalSpeed ) * 5 + + + for [0..@properties.spawnRate * delta] + @particleSystem.spawnParticle + position: @properties.position + positionRandomness: @properties.positionRandomness + velocity: @properties.velocity + velocityRandomness: @properties.velocityRandomness + color: @properties.color + colorRandomness: @properties.colorRandomness + turbulence: @properties.turbulence + lifetime: @properties.lifetime + size: @properties.size + sizeRandomness: @properties.sizeRandomness + + @particleSystem.update @tick \ No newline at end of file diff --git a/form/lib/GPUParticleSystem.js b/form/lib/GPUParticleSystem.js new file mode 100644 index 0000000..7c59aa5 --- /dev/null +++ b/form/lib/GPUParticleSystem.js @@ -0,0 +1,501 @@ +/* + * 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; diff --git a/form/lib/textures/particle2.png b/form/lib/textures/particle2.png new file mode 100644 index 0000000..d0b0251 Binary files /dev/null and b/form/lib/textures/particle2.png differ diff --git a/form/lib/textures/perlin-512.png b/form/lib/textures/perlin-512.png new file mode 100644 index 0000000..11ed410 Binary files /dev/null and b/form/lib/textures/perlin-512.png differ diff --git a/module.json b/module.json index b102989..b2cdd4d 100644 --- a/module.json +++ b/module.json @@ -16,6 +16,7 @@ "/form/Model.coffee", "/form/Scene.coffee", "/form/Studio.coffee", + "/form/ParticleSystem.coffee", "/form/loaders/Collada.coffee", "/form/loaders/FBX.coffee", @@ -32,7 +33,11 @@ "/form/lib/OBJLoader.js", "/form/lib/TDSLoader.js", "/form/lib/OrbitControls.js", - "/form/lib/three.min.js" + "/form/lib/GPUParticleSystem.js", + "/form/lib/three.min.js", + + "/form/lib/textures/particle2.png", + "/form/lib/textures/perlin-512.png" ], "example": "example.coffee", "thumb": "/marketing/thumb.png"