WaterFX - A much faster solution
By Andreas, 22 November, 2010
Yes, the previous water effect I introduced is nice, sure, but it is actually pretty useless as it is eats up way too much processor power. As I still need a fancy effect for my little game I’m planning I had to come up with something else. Simulate water/light distortion IS not an easy task when it comes to processor power but here is an attempt for 800×600. Let’s first check the result:
Neat huh? Let’s dig into the code…
If you checked my previous lab with water you realize that I am still using the DisplacementFilter. What I have done to increase speed is that I’ve focused on the bitmapdata that defines the filter. I decided to create a wave-class that is animated and can be used to draw the bitmapdata. I called this class WaveAnim.as – Let’s get started!
package { import com.greensock.TweenMax; import com.greensock.easing.Linear; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.BlendMode; import flash.display.Sprite; public class WaveAnim extends Sprite { private var bmpd:BitmapData; private var animWidth:Number; private var animHeight:Number; private var animSpeed:Number = 2; private var wave1:Sprite private var wave2:Sprite public function WaveAnim(bmpd:BitmapData,animWidth:Number,animHeight:Number) { super(); this.bmpd = bmpd; this.animWidth = animWidth; this.animHeight = animHeight; wave1 = new Sprite(); wave2 = new Sprite(); // determine amount tiles to cover the whole area AND be able to anim one tile. var numX:int = Math.ceil(animWidth/bmpd.width)+1 var numY:int = Math.ceil(animHeight/bmpd.height)+1 var bmp1:Bitmap var bmp2:Bitmap // creating all tiles and placing them in both wave-sprites. for (var j:int = 0;j< numY;j++) { for (var i:int = 0;i< numX;i++) { bmp1 = new Bitmap(bmpd); bmp2 = new Bitmap(bmpd); bmp1.x = bmp2.x = i*bmpd.width; bmp1.y = bmp2.y = j*bmpd.width; wave1.addChild(bmp1); wave2.addChild(bmp2); } } // TweenMax rocks as usual. Convenient way to put a // colorMatrixFilter on the waves. One RED and one GREEN. TweenMax.to(wave1,0,{colorMatrixFilter:{colorize:0xff0000}}); TweenMax.to(wave2,0,{colorMatrixFilter:{colorize:0×00ff00}}); // now offsetting one wave a little so obvious patterns doesnt appear. wave2.y = -bmpd.height*.3; // using the ADD BlendMode makes the Red channel (wave1) visible // through the green one (wave2) as the sprites channels are added together. wave2.blendMode = BlendMode.ADD; addChild(wave1); addChild(wave2); } public function startAnim(speed:Number=-1) { if (speed != -1)animSpeed = speed; // Tween both waves to scroll horizontally. // Wave1 to the right and Wave2 to the left. // restarts itself when done (reseting initial positions) TweenMax.to(wave1,animSpeed,{startAt:{x:-bmpd.width},x:0,ease:Linear.easeNone}); TweenMax.to(wave2,animSpeed,{startAt:{x:0},x:-bmpd.width,ease:Linear.easeNone,onComplete:startAnim}); } public function stopAnim() { TweenMax.killAll(); } } }
WaveAnim is taking a bitmapdata, copying it into two layers (one green and one red) and scrolling them horizontally towards each other.
The reason why I am colouring the sprites is that the DisplacementFilter will check the colourchannels and red will control all pixels vertically meanwhile the green channel controls the waterpixels vertically (pushes them back and forth).
In this little lab I decided to use this texture, my collegue thought it was way too many waves in it so I added some “blank” areas to calm the water down occassionally.
To make the water a little more alive and also to showcase that you can add anything on top on the WaveAnim I decided to create a simple class extending this Sprite:
The Sprite is animating itself and in the end also removing itself from the displaylist afterwards. Here’s that little class:
package { import com.greensock.TweenLite; import flash.display.Sprite; public class Drop extends Sprite { public function Drop() { super(); scaleX = scaleY = .1; alpha = .7; TweenLite.to(this,2,{alpha:0,scaleX:.6,scaleY:.6,onComplete:destroy}); } public function destroy() { this.parent.removeChild(this); } } }
Now the only thing to do is to put these classes together and use them in a DisplacementFilter. I do that in the document class Wave2.as (as this is my second attempt). I give you the code first and will comment some afterwards:
package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.BitmapDataChannel; import flash.display.BlendMode; import flash.display.MovieClip; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.filters.DisplacementMapFilter; import flash.geom.Matrix; public class Wave2 extends Sprite { static public var WATER_WIDTH:Number = 620; static public var WATER_HEIGHT:Number = 600; private var pondBmp:Bitmap private var waveAnim:WaveAnim private var bWave:BitmapData private var filter:DisplacementMapFilter private var counter:Number = 0; public function Wave2() { // adding the background to the stage. pondBmp = new Bitmap(new Pond(0,0)); addChild(pondBmp); // create the bitmapdata to use as a filtersource. bWave = new BitmapData(WATER_WIDTH,WATER_HEIGHT); //Decomment row below to see what the bitmapdata actually looks like in action. //addChild(new Bitmap(bWave)); filter = new DisplacementMapFilter(bWave,null,BitmapDataChannel.GREEN,BitmapDataChannel.RED,40,-40); // create our new WaveAnim. waveAnim = new WaveAnim(new WaveBmpd(0,0),WATER_WIDTH,WATER_HEIGHT); waveAnim.startAnim(10); // Addin a static image over the pond. var pondTop:Sprite = new TopPond(); addChild(pondTop); // listener for mouseclick – adding a drop in the water. pondTop.addEventListener(MouseEvent.MOUSE_DOWN,mouseIsDown); this.addEventListener(Event.ENTER_FRAME,onTick); } private function mouseIsDown(e:MouseEvent) { // drop where mouse is now! var drop:Drop = new Drop(); drop.x = mouseX drop.y = mouseY waveAnim.addChild(drop); } private function addDrop() { // add a random drop in water. var drop:Drop = new Drop(); drop.x = Math.random()*WATER_WIDTH; drop.y = Math.random()*WATER_HEIGHT; waveAnim.addChild(drop); } private function onTick(e:Event) { // just adding a few drops in the water (every sixth frame); counter+=1; if (counter > 5) { counter = 0; addDrop(); } // draw a new bmpd-copy of the waveAnim and use it in the filter. bWave.lock(); bWave.draw(waveAnim); filter.mapBitmap = bWave; pondBmp.filters = [filter]; bWave.unlock(); // unlocks the bitmapdata again. } } }
For this sample I used this image as a source for the waves:
And on top of all I created a “masked” image that is not affected by the water so it gives the illusion that only the water inside the pond is moving.
If you haven’t discovered TweenMax yet, just go get it at: http://www.greensock.com/ .. its a must-have for all flashdevelopers.
Have fun!