How To Create A Semi-Realistic Underwater Enviroment In Flash Using Perlin Noise & AS3

Yesterday I made a post about creating a login window for Drupal using php and flash. In that demo I included a personal project I had been working on that called for creating an underwater type of effect. As it has been mine my most recent area of interest I of course decided to use flash and perlin noise to generate this effect. However since the focus of that post was more the actual login method rather than the effect in the background, tonight's post focuses on how to generate the effect I was using in the background.

I am terrible when it comes to knowing where people are going to have issues, and I am always afraid that I don't really explain things that well, so I am trying to be more thorough in this post, as I think that it is stylistically very interesting and somewhat different. I also did more testing when creating this particular effect, since I do plan on eventually using it in a production setting, so I am including some of my findings from that testing in this post as well.

So first things first this is the final effect we are going for. (Click on the image to see an isolated enlarged version of the same animation.)


Helpful Suggestion: Whenever possible try to use vector images in flash files instead of rasterized ones. (I.E. Usually those created in illustrator... yeah I know photoshop can do them too, but that is not its main focus.) Vector graphics are much smaller, and can be easily resized without fear of image degradation.

Also try to stick to a small number of fonts in a project. Large numbers of fonts can easily double the size of your flash documents. (You can see the size of your font files by generating a size report (File > Publish Settings > Flash) when publishing.)

Now overall I think that the effect generated is pretty good, but when you consider that the whole file is only 28kb, can easily be expanded to fit any surface area, and has a reasonable level of processor and memory dependency, I think it looks even better. (Although I must admit being something of an efficiency geek, so you may just have to regard that last comment if you are of a different mindset.)

Image Of Water Demo's Layers and Timeline From Within Flash CS4

This particular animation is made up of several layers, but the primary ones we are going to focus on are those contained in the shark mask and water mask layers. The other layers are certainly important to our final product, but they have little or nothing to do with creating the underwater effect, which is the focus of this article. So here is the breakdown of each layer and what it does:


Shark Mask -This layer is right on top of the nose of the shark and offset from the shark reflection layer. Having the reflection layer right on top of the shark made it hard to see where the glare effect started and ended, by moving it away from the shark and just having the arms (arm, tentacles, antennae, whatever, you know what I mean) covering random portions of its nose, it makes it look much more like little streams of light coming down from above.

Shark Reflection -A number of small semi-transparent gradients that are maneuvered so that they closely mimic the curves of the sharks body. Its instance name is Sg.

Water Mask -Basically there to keep elements like the sharks shadow and the ripple layers from spilling into areas other than our intended inner rectangle.

Shadow -Simply the shark's shadow. I though having it there gave a little more realism to the image, and by using multiply and blur it effectively blocked out light streaks from forming under the sharks body where I don't think they realistically belong.

Image Showing Our Three Active Gradients By Themselves

Ripples -This layer is made up of two different different semi-transparent gradients. One up top with the instance name of Tw (short for top water, I try to give instance names that are short but have a mnemonic to them) and one at the bottom with the instance name Sd (sand). You can see all three of our active gradients in the image to the right. (Click on the image to see their motion without the background or foreground images in the way.)

Using irregular lines, especially along edges that you want to fade out into the background, will help give your final effect a much more natural look. The perlin noise's movement is irregular anyways, but this method helps to break up the conformity of the final motion. Also tapering trailing edges can help you generate a sense of depth to the scene.

It is much much better to use multiple smaller gradients for this effect than one larger one. I had initially created this demo with a single large gradient and I found that by going to smaller gradient sizes I could drastically reduce the CPU load. It is also much better to stick to a single instance of perlin noise and just recycle its effects. You can create variety in the final results by instead varying the dimensions and shapes of the objects your are applying these effects to. (I did some tests with one and two perlin noise generator sets and found that memory and cpu usage were drastically reduced with little change to the quality of the overall aesthetics.)

If you are interested in running similar types of tests, or just want to prove me wrong, I highly recommend Mr. Doob's Hi-Res Stats class. Not only is it small and lightweight, but it is free. It is hard to judge overall end user performance with this utility, but it is wonderfully useful when assessing the benefits to changes in approach or programming methodology.

Image Showing Perlin Layers In Front Of The Background Layer

Background - This was probably the most time consuming layer that I had to work with. Not only is it the largest of the bunch, but it is the workhorse of the group. As much as the motion generated by the perlin layers is important, this layer makes or breaks the visual quality of the final product. Not only are the colors important, as it sets the whole scene pretty much, but where you choose to start and end your various gradients will determine how well the active layers above fade out into your background.

When you open the FLA and examine the background image you will notice it is actually two separate gradient images placed one on top of the other. I did this simply because I was having issues placing my smaller gradients without interfering with the res of the image. I needed to have precise control over where each gradient ended and began so that I could place lighter "transitional" gradients right on the rear edges of my two perlin layer. Since both the active motion layers and the transitional zones are brighter in color than the surround area, using this technique helps create a false horizon to the viewer, and provides for a much more seamless transition from one layer into another.



The code for this project is actually really simple and short. I commented everything out as usual, and I don't think it needs a lot of in depth explanation beyond that.

package
{
	import flash.events.Event;
	import flash.display.MovieClip;
	import flash.display.BitmapData; 
	import flash.display.Bitmap; 
	import flash.display.BlendMode;
	import flash.geom.Point;
	import flash.filters.DisplacementMapFilter;
	import flash.display.DisplayObject;
    //import net.hires.debug.Stats; // Imports class for Mr. Doob's benchmarking utility
 
 
 
	public class Main extends MovieClip
	{
		var dir:Array=[new Point(1,115), new Point(840,120)]; // Sets the motion points for our perlin layers
 
		var waterM:BitmapData; // We render our perlin noise to this
 
		public var Tw:MovieClip; // Gradient layer for the waves at the top
		public var Sd:MovieClip; // Gradient "sand" layer
		public var Sg:MovieClip; // Gradient that acts as a reflection on our shark
 
		public var dmW:DisplacementMapFilter; //  
 
 
		public function Main()
		{
			//addChild( new Stats() );  // Starts Mr. Doob's class if it is installed
			initWater(); // This starts our primary function
		}
 
		function initWater():void {
    		waterM=new BitmapData(844, 133, false, 0 ); // Creates the area that we will draw our perlin noise too
			addEventListener(Event.ENTER_FRAME, renWater); // Starts the function that will generate all of our effects
		}
 
		function renWater(event:Event):void {
			/*This generates the rocking motion back and forth
			* the difference in values determines the perceived
			* direction of the water.  Increase the differences 
			* and you get more of a one directional flow.
			*
			* If they are all equal it creates a motion  
			* similar to what you might see in a pool.
			*
			*Higher values means faster wave motion
			*/
            dir[0].x = dir[0].x + 1; // If higher Motion goes to the left, to the left, all your stuff is in the box to the left. Sorry couldn't help it.
			dir[1].x = dir[1].x - .7; // Motion to the right
			dir[0].y = dir[0].y +.065; // Water comes towards viewer
            dir[1].y = dir[1].y - .15; // Water moves away
 
 
			dmW = new DisplacementMapFilter(waterM, new Point(0, 0), 1, 1, 5, 75); // Assign our perlin noise to our displacement filter
 
			waterM.perlinNoise(200,10,2,2, false, true, 1, true, dir); // Creates our perlin noise. The values you will want to adjust are the 1st, 2nd, and 4th
 
			Tw.filters = [dmW];  // Assigns the displacement map to the top water
			Tw.blendMode = "overlay"; // You can do all of this in the properties tab and it will work, I just find this faster
			Tw.alpha = .45; //Smaller values means more transparent
 
			Sd.filters = [dmW]; // Assigns the displacement map to the "sand"
			Sd.blendMode = "hardlight"; 
			Sd.alpha = 1; // Light is fully visible
 
			Sg.filters = [dmW]; // Assigns the displacement map to the shark glare
			Sg.blendMode = "overlay";
			Sg.alpha = .50; 
		}
 	}
 }

Well that about does it I do believe. Thanks for stopping by and reading through everything. Hopefully I touched on everything, but if you have any other questions feel free to comment. Good luck, and enjoy.

- Andrew N. Price

AttachmentSize
Water_Demo.zip173.57 KB
Water_Demo_CS3.fla665.5 KB

Comments

Post new comment

The content of this field is kept private and will not be shown publicly.

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters (without spaces) shown in the image.