package; import flash.events.Event; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.BitmapDataChannel; import flash.filters.BlurFilter; import flash.display.Loader; import flash.display.Sprite; import flash.geom.Point; import flash.geom.Matrix; import flash.geom.Matrix3D; import flash.geom.Vector3D; import flash.geom.ColorTransform; import flash.utils.ByteArray; import flash.utils.Endian; import flash.net.URLRequest; import flash.Vector; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.text.TextFormat; class PerlinParticleEffects extends Sprite { private static inline var WIDTH : Int = 800; private static inline var HEIGHT : Int = 450; private static inline var DEPTH : Int = 400; private static inline var CUBE_WIDTH : Int = 160; private static inline var CUBE_HEIGHT : Int = 160; private static inline var CUBE_DEPTH : Int = 80; private static inline var R_SCALE : Float = CUBE_DEPTH/0xFF; private static inline var G_SCALE : Float = CUBE_HEIGHT/0xFF; private static inline var B_SCALE : Float = CUBE_WIDTH/0xFF; private static inline var CUBE_XSCALE : Float = CUBE_WIDTH/2; private static inline var CUBE_YSCALE : Float = CUBE_HEIGHT/2; private static inline var CUBE_ZSCALE : Float = CUBE_DEPTH/2; private static inline var CUBE_WU : Int = Std.int(CUBE_WIDTH/2); private static inline var CUBE_WL : Int = CUBE_WU-CUBE_WIDTH; private static inline var CUBE_HU : Int = Std.int(CUBE_HEIGHT/2); private static inline var CUBE_HL : Int = CUBE_HU-CUBE_HEIGHT; private static inline var CUBE_DU : Int = Std.int(CUBE_DEPTH/2); private static inline var CUBE_DL : Int = CUBE_DU-CUBE_DEPTH; private static inline var FOCAL : Float = 433.0126953125; private static inline var CX : Int = Std.int(WIDTH/2); private static inline var CY : Int = Std.int(HEIGHT/2); private static inline var PIXELS : Int = WIDTH*HEIGHT; private static inline var PIXEL_BYTES : Int = PIXELS*4; private static inline var SCALE_DOWN : Int = 4; private static inline var SCALE_UP : Int = 0x2; private static inline var MOVEMENT : Float = 0.15; private static inline var PERLIN_GEN_WIDTH : Int = Std.int(CUBE_WIDTH/SCALE_DOWN); private static inline var PERLIN_GEN_HEIGHT : Int = Std.int(CUBE_HEIGHT/SCALE_DOWN); private static inline var PERLIN_SIZE_WIDTH : Int = Std.int(PERLIN_GEN_WIDTH*SCALE_UP); private static inline var PERLIN_SIZE_HEIGHT : Int = Std.int(PERLIN_GEN_HEIGHT*SCALE_UP); private static inline var PERLIN_POINTS : Int = PERLIN_SIZE_WIDTH * PERLIN_SIZE_HEIGHT; private static inline var PARTICLE_START : Int = PIXEL_BYTES; private static inline var PARTICLES : Int = 1024*12; private static inline var PARTICLE_PARTS : Int = 6; private static inline var PARTICLE_RANDOMS = PARTICLES * PARTICLE_PARTS; private static inline var PARTICLE_BYTES : Int = PARTICLE_RANDOMS * 4; private static inline var SPACE_LOOKUP_START : Int = PARTICLE_START + PARTICLE_BYTES; private static inline var SPACE_VALUES : Int = 0xFF; private static inline var SPACE_PARTS : Int = 3; private static inline var SPACE_BYTES : Int = SPACE_VALUES * SPACE_PARTS * 4; private static inline var DATA_START : Int = SPACE_LOOKUP_START + SPACE_BYTES; private static inline var _ox : Int = DATA_START+4; private static inline var _oy : Int = _ox+4; private static inline var _oz : Int = _oy+4; private static inline var _tx : Int = _oz+4; private static inline var _ty : Int = _tx+4; private static inline var p00: Int = _ty+4; private static inline var p01: Int = p00+4; private static inline var p02: Int = p01+4; private static inline var p10: Int = p02+4; private static inline var p11: Int = p10+4; private static inline var p12: Int = p11+4; private static inline var p20: Int = p12+4; private static inline var p21: Int = p20+4; private static inline var p22: Int = p21+4; private static inline var p32: Int = p22+4; private static inline var pz: Int = p32+4; private static inline var _w: Int = pz+4; private static inline var _x: Int = _w+4; private static inline var _y: Int = _x+4; private static inline var _z: Int = _y+4; private static inline var _r: Int = _z+4; private static inline var _g: Int = _r+4; private static inline var _b: Int = _g+4; private static inline var DATA_END : Int = _b + 4; private static inline var BYTES : Int = DATA_END; // magic number from joa's code for color transform private static inline var F0 : Float = 220 / 44100; // instance props private var fdisplay : TextField; private var textField : TextField; private var noiseData : BitmapData; private var noiseMap : BitmapData; private var bitmapData : BitmapData; private var bitmap : Bitmap; private var scaleMatrix : Matrix; private var octaves : Array; private var phase0 : Float; private var _matrix : Matrix3D; private var p0 : Point; private var p1 : Point; private var p2 : Point; private var p3 : Point; // next 4 aren't used, but just incase we want 8 octaves saves some typing! private var p4 : Point; private var p5 : Point; private var p6 : Point; private var p7 : Point; // effect stuff private var filter: BlurFilter; private var colorTransform: ColorTransform; private var origin: Point; // timing vars private var nowtime : Float; private var nowfps : Float; private var fpssmooth : Float; private var lasttime : Float; function setup() : Void { // three sets of bitmap data // 1: for the perlin noise generator noiseData = new BitmapData( PERLIN_GEN_WIDTH , PERLIN_GEN_HEIGHT , false , 0 ); // 2: to hold a scaled version of the perlin noise (let's us optimize for speed) noiseMap = new BitmapData( PERLIN_SIZE_WIDTH , PERLIN_SIZE_HEIGHT , false , 0 ); // 3: the display bitmap data (and bitmap) bitmapData = new BitmapData( WIDTH , HEIGHT , false , 0 ); bitmap = new Bitmap( bitmapData, flash.display.PixelSnapping.AUTO , false ); addChild(bitmap); // effects (blur and color transform) filter = new BlurFilter( 4, 4, 1 ); origin = new Point(); colorTransform = new ColorTransform( 0.92, 0.96, 0.94 ); // scale matrix to convert smaller perlin noise into bigger perlin noise (speed hack) scaleMatrix = new Matrix(); scaleMatrix.scale( SCALE_UP , SCALE_UP ); // octaves for the perlin noise generation octaves = [ p0 = new Point(), p1 = new Point(), p2 = new Point(), p3 = new Point(), ]; // some vars phase0 = 0; fpssmooth = 50; // start storage createStorage(); // start all our in memory particle values for( i in 0...PARTICLES ) { // instantiate each particles x,y,z values flash.Memory.setFloat( PARTICLE_START + (i*24) , 50 ); flash.Memory.setFloat( PARTICLE_START + (i*24)+4 , 50 ); flash.Memory.setFloat( PARTICLE_START + (i*24)+8 , 50 ); // create a load of random values for each particle (to give us a nicer spread) flash.Memory.setFloat( PARTICLE_START + (i*24)+12 , (Math.random()-Math.random()) *(Math.random()+Math.random()+Math.random()+Math.random()+Math.random()+Math.random()) ); flash.Memory.setFloat( PARTICLE_START + (i*24)+16 , (Math.random()-Math.random()) *(Math.random()+Math.random()+Math.random()+Math.random()+Math.random()+Math.random()) ); flash.Memory.setFloat( PARTICLE_START + (i*24)+20 , (Math.random()-Math.random()) *(Math.random()+Math.random()+Math.random()+Math.random()+Math.random()+Math.random()) ); } // now this is an interesting one, to convert hex rgb to space xyz we need to // divide each r,g,b value by 128, then -1, then * by our scale values to get a position // this is a bit heavy as run 3x per particle per frame.. so we precalc every possible // 0-0xFF value converted to space in advance and store it in fast memory // takes 10ms per million particles to do lookup rather than 198ms for calc at run time. // 20x speed increase createColorSpaceLookupChart(); // start matrix _matrix = new Matrix3D(); updateMatrix( _matrix , -520 , 320 ); } static inline function createStorage() : Void { // make a big byte array and stick it in fast memory var storage : ByteArray = new ByteArray(); storage.endian = Endian.LITTLE_ENDIAN; storage.length = BYTES; flash.Memory.select( storage ); } static inline function updateMatrix( matrix : Matrix3D , mx : Float , my : Float ) : Void { // 3D space transformation matrix, updates based on mouse position flash.Memory.setFloat( _tx , flash.Memory.getFloat(_tx) + ((mx - flash.Memory.getFloat(_tx))/10) ); flash.Memory.setFloat( _ty , flash.Memory.getFloat(_ty) + ((my - flash.Memory.getFloat(_ty))/10) ); matrix.identity(); matrix.appendRotation( flash.Memory.getFloat(_tx), Vector3D.Y_AXIS ); matrix.appendRotation( flash.Memory.getFloat(_ty), Vector3D.X_AXIS ); matrix.appendTranslation( 0, 0, -343 ); //-FOCAL ); flash.Memory.setFloat( p00 , matrix.rawData[ 0x0 ] ); flash.Memory.setFloat( p01 , matrix.rawData[ 0x1 ] ); flash.Memory.setFloat( p02 , matrix.rawData[ 0x2 ] ); flash.Memory.setFloat( p10 , matrix.rawData[ 0x4 ] ); flash.Memory.setFloat( p11 , matrix.rawData[ 0x5 ] ); flash.Memory.setFloat( p12 , matrix.rawData[ 0x6 ] ); flash.Memory.setFloat( p20 , matrix.rawData[ 0x8 ] ); flash.Memory.setFloat( p21 , matrix.rawData[ 0x9 ] ); flash.Memory.setFloat( p22 , matrix.rawData[ 0xa ] ); flash.Memory.setFloat( p32 , matrix.rawData[ 0xe ] ); } static inline function setPZ() : Float { // sets the Z position for a particle flash.Memory.setFloat( pz , FOCAL + flash.Memory.getFloat( _x ) * flash.Memory.getFloat( p02 ) + flash.Memory.getFloat( _y ) * flash.Memory.getFloat( p12 ) + flash.Memory.getFloat( _z ) * flash.Memory.getFloat( p22 ) + flash.Memory.getFloat( p32 ) ); flash.Memory.setFloat( _w , FOCAL / flash.Memory.getFloat(pz) ); return flash.Memory.getFloat(pz); } function runTest( _ : Dynamic ) : Void { // the enter frame code // fps code with a bit of smoothing nowtime = Date.now().getTime(); nowfps = 1000/(nowtime - lasttime); fpssmooth = Std.int(((fpssmooth * 10) + nowfps) / 11); lasttime = nowtime; fdisplay.text = fpssmooth + " fps"; // thanks to Joa Ebert for these nice color transformations :) phase0 += F0; if( phase0 > 1 ) --phase0; var a0: Float = phase0 * 2 * Math.PI; var sin0: Float = Math.sin( a0 ); var cos0: Float = Math.cos( a0 ); colorTransform.redMultiplier = 0.42 + sin0 * sin0 * 0.5; colorTransform.greenMultiplier = 0.46 + cos0 * cos0 * 0.5; colorTransform.blueMultiplier = 0.94; // blur the last frame this.bitmapData.applyFilter( this.bitmapData, this.bitmapData.rect, origin, filter ); // color transform it with the effects above this.bitmapData.draw( this.bitmapData, null, colorTransform ); // get all pixels and store them in a Vector (faster than ByteArray) var b : Vector = this.bitmapData.getVector( this.bitmapData.rect ); // pre-initialize vector before looping - makes it MUCH faster b.length; // store all pixels in fast memory for even faster translation for( i in 0...PIXELS) { flash.Memory.setI32( i*4 , b[i] ); } // update the octaves for our perlin noise p0.x += MOVEMENT; p0.y -= MOVEMENT; p1.x -= MOVEMENT; p1.y += MOVEMENT; p3.x += MOVEMENT; p3.y -= MOVEMENT; p2.x -= MOVEMENT; p2.y += MOVEMENT; // update our Matrix3D with mouse co-ords updateMatrix( _matrix , mouseX , mouseY ); // create perlin noise noiseData.perlinNoise( PERLIN_SIZE_WIDTH , PERLIN_SIZE_HEIGHT , 4, 0x723234, false, false, 7, false, octaves ); // scale up noiseMap.draw( noiseData , scaleMatrix ); // get all our perlin noise as a vector and pass it to a function to recalc every particle shiftParticles( noiseMap.getVector( noiseMap.rect ) ); // ensure position of fast memory is 0 flash.system.ApplicationDomain.currentDomain.domainMemory.position = 0; // lock the screen this.bitmapData.lock(); // write all our new bitmap data back in to the bitmap from fast memory this.bitmapData.setPixels( this.bitmapData.rect , flash.system.ApplicationDomain.currentDomain.domainMemory ); // unlock the screen this.bitmapData.unlock( this.bitmapData.rect ); } static inline function shiftParticles( noise : Vector ) : Void { // pre-initialize vector before looping - makes it MUCH faster noise.length; // what we're going to do is.. // for each particle, pick the pixel of perlin noise // calc the particles xyz position based on rgb values and some values to mix it up // then convert the xyz position from a 3d space to a 2d space using our matrix3d // then convert the xy pixel position to a bytearray offset (our var po) // then check the byte array (pixel) position is on screen // if it is we then get the color value of the exising pixel (with effects added) and add // the color from the perlin noise pixel to the old value (rgb individually so it scales nicely) var po : Int = 0; for( i in 0...PARTICLES ) { moveParticle( PARTICLE_START + (i*24) , noise[i % PERLIN_POINTS] ); if( setPZ() > 0 ) { po = pointToOffset( Std.int( flash.Memory.getFloat(_w) * ( flash.Memory.getFloat( _x ) * flash.Memory.getFloat( p00 ) + flash.Memory.getFloat( _y ) * flash.Memory.getFloat( p10 ) + flash.Memory.getFloat( _z ) * flash.Memory.getFloat( p20 ) ) + CX ), Std.int( flash.Memory.getFloat(_w) * ( flash.Memory.getFloat( _x ) * flash.Memory.getFloat( p01 ) + flash.Memory.getFloat( _y ) * flash.Memory.getFloat( p11 ) + flash.Memory.getFloat( _z ) * flash.Memory.getFloat( p21 ) ) + CY ), WIDTH ); if( po < PIXEL_BYTES && 0 < po ) { flash.Memory.setI32( po , doubleColor( flash.Memory.getI32(po) ) ); } } } } static inline function moveParticle( o : Int , c : UInt ) : Void { // remember that o is the fast memory offset for the start of this particles data // c is the color from the perlin noise we generated // first we want to get the new particle coords from the perlin noise particle // and store them in _x, _y, _z fast mem vars getColorSpace( c ); // we then update the position of the particle using the new value from perlin noise // and our random value to mix it up a bit (a scale of 0-255 doesn't give much spacing flash.Memory.setFloat( o , flash.Memory.getFloat(_x) + flash.Memory.getFloat(o+12) ); flash.Memory.setFloat( o+4 , flash.Memory.getFloat(_y) + flash.Memory.getFloat(o+16) ); flash.Memory.setFloat( o+8 , flash.Memory.getFloat(_z) + flash.Memory.getFloat(o+20) ); // and check the values are all within bounds, if not send it back in the other direction // sanity code from the perlin particle cubes, but still applies if( flash.Memory.getFloat(o) > CUBE_WU ) { flash.Memory.setFloat( o , CUBE_WU+(CUBE_WU-flash.Memory.getFloat(o)) ); } if( flash.Memory.getFloat(o) < CUBE_WL ) { flash.Memory.setFloat( o , CUBE_WL+(CUBE_WL-flash.Memory.getFloat(o)) ); } if( flash.Memory.getFloat(o+4) > CUBE_HU ) { flash.Memory.setFloat( o+4 , CUBE_HU+(CUBE_HU-flash.Memory.getFloat(o+4)) ); } if( flash.Memory.getFloat(o+4) < CUBE_HL ) { flash.Memory.setFloat( o+4 , CUBE_HL+(CUBE_HL-flash.Memory.getFloat(o+4)) ); } if( flash.Memory.getFloat(o+8) > CUBE_DU) { flash.Memory.setFloat( o+8 , CUBE_DU+(CUBE_DU-flash.Memory.getFloat(o+8)) ); } if( flash.Memory.getFloat(o+8) < CUBE_DL ) { flash.Memory.setFloat( o+8 , CUBE_DL+(CUBE_DL-flash.Memory.getFloat(o+8)) ); } // store the new position back in _x,_y,_z (purely to save us doing +4 and +8 lots of times flash.Memory.setFloat( _x , flash.Memory.getFloat(o) ); flash.Memory.setFloat( _y , flash.Memory.getFloat(o+4) ); flash.Memory.setFloat( _z , flash.Memory.getFloat(o+8) ); } // 20ms per 1,920,000 calls static inline function getColorSpace( c : UInt ) : Void { // get scaled space value in lookup chart in fast memory // based on hex byte from color - v fast flash.Memory.setFloat( _x , flash.Memory.getFloat( SPACE_LOOKUP_START + ((c & 0xFF)*12)) ); flash.Memory.setFloat( _y , flash.Memory.getFloat( SPACE_LOOKUP_START + 4 + (((c >> 8) & 0xFF)*12) ) ); flash.Memory.setFloat( _z , flash.Memory.getFloat( SPACE_LOOKUP_START + 8 + (((c >> 16) & 0xFF)*12) ) ); // store r,g,b base values for use later too flash.Memory.setByte( _b , (c & 0xFF) ); flash.Memory.setByte( _g , ((c >> 8) & 0xFF) ); flash.Memory.setByte( _r , ((c >> 16) & 0xFF) ); } static inline function createColorSpaceLookupChart() : Void { for( i in 0...SPACE_VALUES ) { flash.Memory.setFloat( SPACE_LOOKUP_START + (i*12) , ((i/0x80)-1)*CUBE_XSCALE ); flash.Memory.setFloat( SPACE_LOOKUP_START + (i*12)+4 , ((i/0x80)-1)*CUBE_YSCALE ); flash.Memory.setFloat( SPACE_LOOKUP_START + (i*12)+8 , ((i/0x80)-1)*CUBE_ZSCALE ); } } static inline function pointToOffset( vx : Int , vy : Int , vw : Int ) : UInt { return (( (vy-1) * vw ) + vx-1) * 4; } static inline function doubleColor( c : UInt ) : UInt { // add the new color to the old pixel color var r : Int = (( c >> 16 ) & 0xFF) + flash.Memory.getByte(_r); var g : Int = (( c >> 8 ) & 0xFF) + flash.Memory.getByte(_g); var b : Int = (c & 0xFF) + flash.Memory.getByte(_b); if( r > 0xff ) r = 0xff; if( g > 0xff ) g = 0xff; if( b > 0xff ) b = 0xff; // and return the new color return color( r , g , b ); } static inline function color( r : Int , g : Int , b : Int ) : UInt { // convert rgb to hex return ((r << 16) + (g << 8) + b); } function addTextBoxOverlay() : Void { var tf : TextFormat = new TextFormat(); tf.font = 'arial'; tf.size = 10; tf.color = 0xffffff; textField = new TextField(); textField.autoSize = TextFieldAutoSize.LEFT; textField.defaultTextFormat = tf; textField.selectable = false; textField.text = 'nathan at webr3 dot org : 3D Perlin Particle Light Cloud.'; textField.y = HEIGHT - textField.height; textField.opaqueBackground = 0x000000; addChild( textField ); fdisplay = new TextField(); fdisplay.autoSize = TextFieldAutoSize.RIGHT; fdisplay.defaultTextFormat = tf; fdisplay.selectable = false; fdisplay.text = 'fps'; fdisplay.y = HEIGHT - textField.height; fdisplay.x = WIDTH - textField.height; fdisplay.opaqueBackground = 0x000000; addChild( fdisplay ); } public static function main() { var mc:flash.display.MovieClip = flash.Lib.current; var i : PerlinParticleEffects = new PerlinParticleEffects(); mc.addChild( i ); } public function new() { super(); setup(); addTextBoxOverlay(); addEventListener( Event.ENTER_FRAME, runTest ); } }