Archive for 'My Work'

Sony Quantum Code Award Wrap-up

This will be my last post on this – I promise. :)

While at my previous role at Massive, I had the privilege to work with some incredibly talented people on some fairly heavily-awarded projects, such as V8 Supercars, the Sydney to Hobart Yacht Tracker and the BigPond Movies Viiv application.

After leaving Massive I didn’t think I could top that, but one of the first projects I worked on at Euro late last year has come pretty close. Here’s what we’ve picked up to date (I might be missing a few more):

Awards

Nominations

Water For Life Microsite using Papervision3D

Water4Life Homepage

Euro has just launched an integrated campaign for the NSW Government called “Water For Life”, which aims to inform the public about new water sources, and ways in which we can use our existing sources more efficiently. The campaign presents a “day-in-the-life” view of a water drop’s journey through our water system, and includes TV commercials, printed and online advertising, and a microsite.

The aim of the microsite was to unify the journeys presented in each TV commercial, and provide a little more depth of information without becoming too heavy on details. We wanted to reuse and maintain the visual style of the TVCs, with the almost anthropomorphic focus on the water drop itself, so we decided to build a set of 3D scenes where people could discover additional information on water efficiency at various points on the drop’s journey.

Water4Life Bathroom Scene

We used Papervision3D to render the scenes, which were partly modelled in 3D Studio Max and textured using a combination of texture-baking in 3DS and plenty of post-production in Photoshop. Then additional elements were added directly in Papervision, such as the refractive water drop, depth-of-field particles, tree decals, skyboxes, lens flares, etc.

Water4Life Map Scene

I’m really happy with the final result, as well as the performance of the 3D scenes thanks to some optimisation tricks and techniques. I’m planning on doing a series of posts going into further detail on some of the effects and optimisation techniques, including source code where possible. If there’s anything specific you’d like me to cover, leave a comment.

Easter Egg a Bastard

A couple of weeks back just prior to the Easter weekend Euro RSCG launched another campaign for the Australian Democrats. Called “Easter Egg a Bastard” – pick a politician’s website, or enter your own URL and then egg the page.

easter_egg

I’m happy with the way I’ve seamlessly combined timeline animation with scripted animation. The bunny and egg are controlled using timeline animation right up until the egg hits the slingshot pouch. From then on it’s all ActionScript.

The timeline animation is triggered from ActionScript using good old gotoAndPlay(), and when each animation finishes, the bunny’s timeline calls stop() and then broadcasts an event back out to the view class controlling the bunny. The bunny SWF is embedded at compile time which would normally cause any ActionScript to be stripped out, so I used this method of preserving timeline code in embedded assets. Read the comment on that post though – I also found that waiting one frame often wasn’t enough. I simply added a check for the existence of loader.content in the enterFrame handler before continuing.

I also love the feel of the slingshot physics – it took a fair bit of tweaking to get right. The feeling of depth as you pull back on the slingshot is faked. No Z dimension – the pouch & egg are just scaling based on their Y position. This meant that I didn’t need to do any converting back and forth between 3D and 2D coordinates to draw the rubber strips. Once the egg leaves the slingshot however, proper 3D takes over and the egg is positioned & scaled based on its Z coordinate. When budget or schedule (or both!) are tight, it’s essential to keep things simple and not over-engineer.

Quantum Code Nominated for a Webby

Another (potential) success for Euro & Sony’s Quantum Code campaign – nominated for a Webby in the Integrated Mobile Experience category.

You can choose your pick for best of the Net by voting at the Webby People’s Voice site.

Flash Smoke Effect using DisplacementMapFilter

Inspired by David Lenaerts’ awesome smoke simulation using Alchemy, I wondered if a similar effect – pushing smoke around with the mouse – could be achieved using Flash’s DisplacementMapFilter. As with David’s simulation, click to add smoke and move the mouse to create wind.

displacement_smoke

There’s a great explanation of how DisplacementMapFilter works over at Emanuele Feronato’s blog, so I won’t repeat it here. The smoke effect is on the left, and on the right is the displacement bitmap that gets applied to the smoke image on each frame. To simulate wind, as the mouse is moved I add or subtract from the red and green channels using ColorTransform. The faster the mouse is moved, the more I add or subtract. To simulate the wind settling down, I apply a BlurFilter and another ColorTransform to cause the displacement map to converge towards 0x80C000 (no horizontal movement, slight upwards movement).

While not nearly as impressive as David’s more realistic simulation, I’m pretty happy with what can be achieved with simple bitmap displacement. I experimented with adding Perlin noise to prevent the smoke from getting too static when the mouse isn’t moving and got some really cool results, but have decided to keep things simple for now. Maybe in a future post.

Here’s the source. It was built in FlexBuilder as an ActionScript project, but wouldn’t be hard to convert to compile in the Flash IDE.

package
{
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.filters.BlurFilter;
    import flash.filters.DisplacementMapFilter;
    import flash.geom.ColorTransform;
    import flash.geom.Point;
    import flash.geom.Rectangle;

    [SWF(width='1024', height='512', frameRate='30')]
    public class DisplacementSmoke extends Sprite
    {
        internal static var BITMAP_WIDTH:Number = 512;
        internal static var BITMAP_HEIGHT:Number = 512;
       
        internal static var WIND_SIZE:Number = 80;
        internal static var SMOKE_SIZE:Number = 10;
       
        internal var smokeBitmap:Bitmap;
        internal var displacementBitmap:Bitmap;
        internal var drawing:Boolean = false;
        internal var xPos:Number;
        internal var yPos:Number;
        internal var displacementFilter:DisplacementMapFilter;
        internal var blurFilter:BlurFilter = new BlurFilter(3, 3, 1);
        internal var windColorTransform:ColorTransform;
        internal var smokeColorTransform:ColorTransform = new ColorTransform(1, 1, 1, 1, 20, 20, 20);
        internal var dampingColorTransform:ColorTransform = new ColorTransform(.97, .96, 0, 1, 5, 9, 0, 0);
        internal var heatColorTransform:ColorTransform = new ColorTransform(1, 1, 1, 1, 0, 2, 0, 0);
        internal var bitmapRect:Rectangle = new Rectangle(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
        internal var rect:Rectangle;
        internal var p:Point = new Point(0, 0);
       
        public function DisplacementSmoke()
        {
            smokeBitmap = new Bitmap(new BitmapData(BITMAP_WIDTH, BITMAP_HEIGHT, false, 0x000000));
            displacementBitmap = new Bitmap(new BitmapData(BITMAP_WIDTH, BITMAP_HEIGHT, false, 0x80C000));
           
            addChild(smokeBitmap);
            addChild(displacementBitmap);
            displacementBitmap.x = BITMAP_WIDTH;
           
            stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);

            displacementFilter = new DisplacementMapFilter(displacementBitmap.bitmapData, p, 1, 2, 5, 5);
            smokeBitmap.filters = [displacementFilter];

            addEventListener(Event.ENTER_FRAME, enterFrameHandler);
        }
       
        internal function mouseDownHandler(event:MouseEvent):void
        {
            drawing = true;
        }
       
        internal function mouseUpHandler(event:MouseEvent):void
        {
            drawing = false;
        }
       
        internal function enterFrameHandler(event:Event):void
        {
            var i:Number;
            var j:Number;

            var mx:Number = stage.mouseX;
            var my:Number = stage.mouseY;
                       
            var dx:Number = mx - xPos;
            var dy:Number = my - yPos;
            var d:Number = Math.sqrt(dx*dx + dy*dy);
            var step:Number = .6 - Math.min(.5, d/100);
           
            var xp:Number;
            var yp:Number;
           
            windColorTransform = new ColorTransform(1, 1, 1, 1, -dx*.5, -dy*.5);
           
            for(i=0; i<1; i+=step)
            {
                xp = xPos + dx*i;
                yp = yPos + dy*i;
               
                xp = Math.max(xp, WIND_SIZE/2);
                xp = Math.min(xp, BITMAP_WIDTH - WIND_SIZE/2);
               
                yp = Math.max(yp, WIND_SIZE/2);
                yp = Math.min(yp, BITMAP_HEIGHT - WIND_SIZE/2);
               
                if(!(xPos==0 && yPos==0))
                {              
                    rect = new Rectangle(xp - WIND_SIZE/2 + Math.random()*10-5, yp - WIND_SIZE/2 + Math.random()*10-5, WIND_SIZE, WIND_SIZE);
                    displacementBitmap.bitmapData.colorTransform(rect, windColorTransform);
               
                    if(drawing)
                    {
                        displacementBitmap.bitmapData.colorTransform(rect, heatColorTransform);
                       
                        for(j=0; j<4; j++)
                        {
                            rect = new Rectangle(xp - Math.random()*SMOKE_SIZE, yp - Math.random()*SMOKE_SIZE, SMOKE_SIZE, SMOKE_SIZE);
                            smokeBitmap.bitmapData.colorTransform(rect, smokeColorTransform);
                        }
                    }
                }
            }
           
            displacementBitmap.bitmapData.colorTransform(bitmapRect, dampingColorTransform);
            displacementBitmap.bitmapData.applyFilter(displacementBitmap.bitmapData, bitmapRect, p, blurFilter);          
           
            xPos = mx;
            yPos = my;
           
            smokeBitmap.bitmapData.draw(smokeBitmap);
        }
    }
}