The blog of Mike (such as it is).
Posts tagged After Effects CS5
3D Land & Water Coding Experiment
Nov 30th
Have you ever played the video game called From Dust? It contains intriguing land, water, and lava simulation. Since first playing it, I have been fascinated with the results of Ubisoft’s very interesting simulation system. I developed an inspired guess as to how the basic algorithm and technique might work. During some holiday downtime, I put code to the concept and came up with the following:
The algorithm steps through a pair of 2 Dimensional arrays containing the height of land and water at each coordinate in the grid. During each step, it determines if and where the water would run downhill, then adjusts the old and new values accordingly. The resolution of the grid was limited by the performance of Flash, which I could no doubt have improved dramatically by using more advanced stage drawing techniques. After running and adjusting the simulations in Flash, I switched the movie to export a JPG image of each frame. That sequence was then run through After Effects (frame blend) to smooth the adjustments. The final sequences were then applied as separate Y-axis displacement maps on “land” and “water” objects in Lightwave 3D.
In the first of the two animations (mountain springs running downhill into a valley), a trio of simple “water emitters” put out a finite amount of water at the tops of the hills, and the algorithm runs it downhill to pool at the bottom. The land was generated with high values at the edges, and low values at the center, with a touch of randomization of the terrain (a “valley”).
In the second (sea mountain rising), a flat land mass with water sitting on top (an “ocean”) is affected by a strong “land emitter” which pushes a mountain out of the sea floor. The water is reacting by being displaced downhill from the land. The circular wave of water was the natural product of the algorithm I created, which seems to indicate being on the right track for how the video game works internally.
Why generate the frames in Flash AS3? Frankly, that choice was due to my own limitations as a programmer. Also, my algorithm fails to ever resolve the water to a natural state of a flat surface.
I now have an even greater sense of awe for what the Ubisoft team achieved with their amazing video game. For example, the game contains dynamic water foam textures which give realistic cues to how the water runs downhill and around obstacles.
The Flash AS3 source code is below. I realize my coding techniques are horrible, and not even object oriented. Had this been for anything other than a proof-of-concept fun project, many loose ends would be wrapped up. There was nothing else in the Flash movie, as it creates all needed movieclips dynamically. If you want to try it, just create a new Flash (ActionScript v3) document and paste the below into the frame 1 actions. It reflects the coding for the “mountain rising out of the sea” effect.
import flash.display.MovieClip;
import flash.events.MouseEvent;
import flash.events.KeyboardEvent;
import flash.geom.Matrix;
//import com.adobe.images.JPGEncoder; // external library used for saving frames
stop();
var frameNum:int = 99;
var resX:int = 40;
var resY:int = resX;
var paintStrength:Number = 1;
var tileSpacing:int = 0;
var tileWidth:int = stage.stageWidth/(resX+(resX*tileSpacing));
var tileHeight:int = stage.stageHeight/(resY+(resY*tileSpacing));
var alreadyEmitted:Number = 0;
var maxEmitted:Number = 0;
var edgeValue:Number = 0;
var maxBoundary:int = resX-1;
var drawValue:Number = paintStrength;
var editMode:String = "water";
var hideLand:Boolean = false;
var hideWater:Boolean = false;
var waterInit:Number = 0.1;
var landInit:Number = 0.05;
var updateTimer:Timer = new Timer(250);
updateTimer.addEventListener(TimerEvent.TIMER, advanceFrame);
updateTimer.start();
// used to capture frames in non-realtime
//stage.addEventListener(KeyboardEvent.KEY_DOWN, advanceFrame);
function advanceFrame(e:TimerEvent):void{
updateMap(null)//commands
}
var parent_mc:MovieClip = new MovieClip();
stage.addChild(parent_mc);
var landMap:Array = new Array();
var landHeight:Number = 0;
landMap.push(new Array());
for (var yInit:int=0; yInit<resY; yInit++) {
var newRow:Array = new Array();
//newRow.splice(0,newRow.length);
newRow[0] = new Array();
for (var xInit:int=0; xInit<resX; xInit++) {
landHeight = landInit;//+(Math.random()*.1);
newRow[0].push(landHeight);
}
landMap[0].push(newRow);
}
var waterMap:Array = new Array();
waterMap.push(new Array());
for (yInit=0; yInit<resY; yInit++) {
var waterRow:Array = new Array();
//newRow.splice(0,newRow.length);
waterRow[0] = new Array();
for (xInit=0; xInit<resX; xInit++) {
waterRow[0].push(waterInit);
}
waterMap[0].push(waterRow);
}
function calculate_new(x:int, y:int):void {
var newVal:Number = 0;
var newDelta:Number = 0;
var proposedX:int = 0;
var proposedY:int = 0;
var minLevel:Number = 100;
var minX:int = 0;
var minY:int = 0;
var curLand:Number;
var curWater:Number;
var checkLand:Number;
var checkWater:Number;
curLand = landMap[0][y][0][x];
curWater = waterMap[0][y][0][x];
for ( var neighborX:int=-1; neighborX<=1; neighborX++) {
for ( var neighborY:int=-1; neighborY<=1; neighborY++) {
proposedX = x+neighborX;
proposedY = y+neighborY;
if (
(proposedX<0) || (proposedY<0) ||
(proposedX>maxBoundary) || (proposedY>maxBoundary)
) {
//trace('skipping edge');
}
else {
checkLand = landMap[0][proposedY][0][proposedX];
checkWater = waterMap[0][proposedY][0][proposedX];
if (neighborX==0 && neighborY==0) {
//trace('skipping self');
}
else {
if ((checkLand+checkWater)<minLevel) {
minLevel = checkLand+checkWater;
minX = proposedX;
minY = proposedY;
//trace('new tallness found: ' + minLevel);
}
}
}
}
}
compare_cells(x, y, minX, minY);
}
function compare_cells(curX:int, curY:int, checkX:int, checkY:int):void {
var delta:Number;
var curLand:Number = landMap[0][curY][0][curX];
var curWater:Number = waterMap[0][curY][0][curX];
var checkLand:Number = landMap[0][checkY][0][checkX];
var checkWater:Number = waterMap[0][checkY][0][checkX];
var idealLevel:Number = 0;
// if there is water here
if (curWater > 0) {
//if this water should fall
if ((curLand+curLand) > (checkLand+checkWater) ) {
// get the goal average height between the two including water
idealLevel = (curLand + curWater + checkLand + checkWater)/2;
if (idealLevel > curLand) { idealLevel = curLand};
delta = curLand + curWater - idealLevel;
delta *= 0.3;
waterMap[0][curY][0][curX] -= delta;
waterMap[0][checkY][0][checkX] += delta;
}
}
}
function updateMap(eventObject:MouseEvent):void {
frameNum++;
// clean up stage from last update
if(parent_mc.numChildren!=0){
var cond:int = parent_mc.numChildren;
while( cond -- ) {
parent_mc.removeChildAt( cond );
}
}
var shading:Number = 0;
for (var k in waterMap[0]) {
for (var i in waterMap[0][k][0]) {
var square:Shape = new Shape();
var squareMC:MovieClip = new MovieClip();
calculate_new(i,k);
if (waterMap[0][k][0][i]>0.001 ) { // water is on this spot
shading = get_water_color(k,i);
}
else { // land is showing on this spot
shading = get_land_color(k,i);
}
square.graphics.beginFill(shading);
square.graphics.drawRect(0, 0, tileWidth, tileHeight);
square.graphics.endFill();
squareMC.addChild(square);
squareMC.x = (i * (tileWidth + tileSpacing + tileSpacing)) + tileSpacing;
squareMC.y = (k * (tileHeight + tileSpacing + tileSpacing)) + tileSpacing;
parent_mc.addChild(squareMC);
}
}
//save_screenshot(); // used to capture frames in non-realtime
// water emmitters
emit_water();
progress_land();
trace(frameNum);
}
function get_land_color(y:int, x:int):Number {
var color:Number = (int)(landMap[0][y][0][x] * 0xFF);
if (color < 0) { color = 0 }
if (color > 255) { color = 255 }
color = (color << 16) | (color << 8) | color; // maps to grey of RGB
return(color);
}
function get_water_color(y:int, x:int):Number {
var color:Number = 0;
if (waterMap[0][y][0][x] >0)
color = (waterMap[0][y][0][x] + landMap[0][y][0][x]) * 0xFF;
if (color < 0) { color = 0 }
if (color > 255) { color = 255 }
// the below line draws the water in greyscale, otherwise shades of blue
// as in an RGB value, the blue is in the least significant digits
//color = (color << 16) | (color << 8) | color; // maps to grey of RGB
return(color);
}
/*
function save_screenshot():void {
var jpgSource:BitmapData = new BitmapData (stage.stageWidth, stage.stageHeight);
jpgSource.draw(stage);
var jpgEncoder:JPGEncoder = new JPGEncoder(100);
var jpgStream:ByteArray = jpgEncoder.encode(jpgSource);
var file:FileReference = new FileReference();
file.save(jpgStream, 'riseLand-'+frameNum+'.jpg');
}
*/
function emit_water():void {
if (alreadyEmitted < maxEmitted) {
if (waterMap[0][int(resY*.30)][0][int(resX*0.95)]<.3) {
waterMap[0][int(resY*.30)][0][int(resX*0.95)] += .2;
alreadyEmitted += .2;
}
if (waterMap[0][int(resY*.10)][0][int(resX*0.45)]<.3) {
waterMap[0][int(resY*.10)][0][int(resX*0.45)] += .1;
alreadyEmitted += .1;
}
if (waterMap[0][int(resY*.85)][0][int(resX*0.15)]<.3) {
waterMap[0][int(resY*.85)][0][int(resX*0.15)]+= .05;
alreadyEmitted += .05;
}
}
}
var landAdded:Number = 0;
var landMax:Number = 200;
var cur_landshift:Number = 0;
var max_landshift:Number = 100;
function progress_land():void {
var proposedX:int;
var proposedY:int;
// these are the land emitters in the center.
if (landAdded < landMax) {
landMap[0][int(resY*.5)][0][int(resX*0.5)]+= 2;
landAdded += 2;
}
cur_landshift++;
if (cur_landshift > max_landshift) { return; }
for (var k in landMap[0]) {
for (var i in landMap[0][k][0]) {
for ( var neighborX:int=-1; neighborX<=1; neighborX++) {
for ( var neighborY:int=-1; neighborY<=1; neighborY++) {
proposedX = i+neighborX;
proposedY = k+neighborY;
if (
(proposedX<0) || (proposedY<0) ||
(proposedX>resX-1) || (proposedY>resY-1)
) {
//trace('skipping land edge');
}
else {
shift_land(i, k, proposedX, proposedY);
}
}
}
}
}
}
function shift_land(curX:int, curY:int, checkX:int, checkY:int):void {
var delta:Number = landMap[0][curY][0][curX] - landMap[0][checkY][0][checkX];
delta *= .5;
landMap[0][curY][0][curX] -= delta;
landMap[0][checkY][0][checkX] += delta;
}
Work Stuff: Motion Graphics for Splash Coupons
Oct 25th
These two videos were created to help the Splash Coupons sales force. The videos were available online, and also on iPads the sales team took to appointments. After Effects was my tool of choice. It was my first project getting to use After Effects after completing training. I’m looking forward to creating better and better results in future projects.
One feature in these videos is the “traveling line effect” in the background of both videos (most visible in 2nd video). The lines were created in Illustrator using an “expanded” “blend”. This was then brought into After Effects, where a 3D camera was attached to a variation of its own path. After Effects did an amazing job of rendering the vector artwork transformed in 3D space. The voiceover was edited in Adobe Soundbooth CS5. (I’ve now upgraded to Adobe Audition as part of Adobe CS5.5).
Credits: Shana Rose (voice performance), Various iStockPhoto.com Photographers, Mike Randrup (all other roles)
This was the full product introduction video. It contains a call-to-action, directing users to an online calendar that was on the original page (but is not on this blog page).
Work Stuff: Loading Animation for native iPhone App
Oct 3rd

Earlier this year, I worked on the graphics used in a Native iPhone App (now live on the App Store) for www.SplashCoupons.com. The app lists offers for home improvement savings for people in the North Dallas, Texas area. When the App first launches, it downloads the latest set of coupons from a data server via XML. During this process, which can take a couple of seconds, I wanted to display a visually interesting animation. So out came the storyboard, and a new After Effects project was born.
Conceptually, the animation was designed to show a user that “coupons” are going “into their iPhone”. The After Effects project was relatively simple. The coupon image assets I created were animated along paths from off screen into the pulsing iPhone in the middle. The screen of the iPhone has an AJAX-style busy cursor superimposed on it. Two layers of After Effects “Particle Playground” effects were blending with a glow effect. Since the particle filter naturally emits from the center outward, I time reversed the particle composition so the particles would flow into the iPhone. There is a colored glow in the center, and concentric circles pulsing inward. The last element, “droplet” pieces of the Splash Coupons logo, fly outward from the phone. The color decisions were based on the style guide for the Splash Coupons logo and brand.
The end deliverable for the animation was an 18 frame looping sequence. It didn’t take up much memory or room in the IPA file, and loops to run as long as needed. I also provided several other button graphics for the user interface.
Hopefully it makes for a nicer wait as the XML downloads in the background. At any rate, it was fun to create.