Archive for 'Flash'

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);
        }
    }
}

Euro RSCG & Sony Win Two AIMIA Awards

Euro AIMIA Awards

Hooray! Not much more to add to my previous post on the nominations, except to reiterate – well done to everyone involved at Euro and Sony.

It was the first time I’ve actually attended an AIMIA Awards night and I had a great time – caught up with some old friends, and met a few new acquaintances as well. Woke up with a spectacular hangover the next morning too.

Congratulations also to all the other award winners.

Update: Just added a few photos from the night.

Euro & Michelle/Sony with the first award

On stage receiving the second award

Second award with the presenter

Euro & Michelle

#tweetcoding Part 5 – Snake, Cube, Jellycube & Headless Flasher

My next #tweetcoding entry is Snake, a game which needs no introduction. No game here unfortuntely – just the control mechanism, but I believe this was the first entry to use keyboard input. Use W, A, S and D to control the snake… although due to the code I’ve used which interprets keyboard commands into horizontal and vertical velocities, other keys will also affect movement in unexpected ways. Here’s how it works:

The key codes for W and S (up and down) are 87 and 83, and the key codes for A and D (left and right) are 65 and 68. When a key is pressed, first I check if it is up/down, or left/right by testing whether its keycode is above 80 or not. If below, I subtract 66.5 from the key code, leaving either -1.5 or 1.5, which I use as the X velocity. If above 80, I subtract 85, leaving either -2 or 2, then multiply by .75 to ensure that the X and Y velocities match.

Of course, this means that other keys will also affect either the X or Y velocity at unpredictable speeds.

Snake

if(!i++){c=q=85;stage.addEventListener("keyDown",k)}function k(e){c=e.keyCode}v=h=0;c>80?v=(c-q)*.75:h=c-66.5;ls(9,i);lt(q-(x-=h),q-(y+=v))

Number 12 – my attempt at creating a shaded 3D cube cheats on two levels – 1: it’s not correct perspective 3D but isometric 3D, and 2: the “shading” is achieved with simple parallel lines which converge to mimic a shading effect as each plane turns away from the camera. The isometric effect is simple to produce – just scale the parent sprite/clip (in this case stage/root) by around 50%, and then rotate the children.

Cube

scaleY=.6;g.clear();x=275;y=130;i-=.03;for(q=x;q-=5;){j=m.cos(i)*y;k=s(i)*y;mt(k,j+q);ls(3);lt(-j,k+q);lt(-k,q-j);ls(1);lt(j,q-k);lt(k,j+q)}

Jellycube was a natural progression from my previous entry, but also inspired by Human Target by Melon Dezign. Melon’s style was always a big inspiration for my demogroup Reality, particularly with their de-emphasis on cutting-edge effects in favour of design and humour. However, once again I wasn’t able to achieve my desired outcome – instead of squashing & stretching the cube I was restricted to rotation only.

Jellycube

scaleY=.4;x=275;for(j=y=160;j-=8;){if(!o[j]){o[j]=addChild(new Bitmap(new BitmapData(y,y,1,8e8)));o[j].y=j*2}o[j].rotation=s((j+i++)/999)*x}

My 14th, and final entry is Headless Flasher, an attempt at creating a stick figure running across the screen. This one was originally inspired by my cat Lucy, but it became pretty obvious that the 140 character limit wouldn’t allow four legs and a tail, so I switched over to a human stick figure. Unfortunately there was no room to add code for his head (or blood spurting out of his neck for that matter).

Headless Flasher

l=lt;scaleX=scaleY=-20;g.clear();j=s(i-=.2);k=s(i+1);x=x%650+3;y=(50+k)*6;ls(1);mt(2+j,k);l(1,4);l(2-j,-k);mt(1,4);l(0,7);l(1+j,5);l(j,4-j)

#tweetcoding Part 4 – Bubbles, Sineribbon & Feedback Vortex

Number eight is Bubbles. Of my #tweetcoding entries, this is possibly my favourite. It’s nothing special technically, but I love recreating natural/physical phenomena, and to do so in 140 characters is even better. I was inspired when staring into a glass of beer – unfortunately I was unable to implement a nice amber colour within the constraints.

Love the pseudo-3D parallax effect, random motion, differing colours & transparency. The only thing I would improve is the visual appearance of the bubbles – it’d be nice to add some shading. Otherwise I’m pretty happy with this one.

bubbles

g.clear();for(i=j=550;i--;){!o[i]?o[i]={x:r()*j,y:r()*j,s:r()*9}:p=o[i];with(p){ls(s,j+i,.2);mt(x,y);lt(x+=r()*4-2,y-=r()*p.s);if(y<0)y=j}}

Nine is Sineribbon. This was an attempt to recreate one of my favourite effects, seen here in another classic PC demo – X14 by Orange from 1995. Skip to the 2:50 mark to see the effect or, better still, watch the whole thing. Unfortunately for me, the character constraint was too limiting, so I ended up with what you see here. I might try and revisit this one though.

sineribbon

q=200;g.clear();if(!o.a)o.a=o.b=1;with(o){a+=.03;b-=.07;for(j=550;j--;){p=s(j/q+s(a+j/q)+s(b+j/179))*q/2;ls(1,p+q/2);mt(j,q-p);lt(j,q+p)}}

Ten – Feedback Vortex. Video Feedback goes all the way back to the advent of analogue video back in 1956, although it wasn’t used purposefully until the 1960s and didn’t really take off until decades later. Creating this effect in Flash is simple – just add a bitmap to the stage and, each frame, copy the entire stage into the bitmap, with input image, noise or colour around the bitmap to start the effect off. Over multiple frames, this generates copies of the input image – each one nested within the last. By adjusting the scale and rotation of the bitmap, the copies can be made to spiral, or zoom in and out.

My favourite demoscene example is again from X14 by Orange, at the 2:40 mark.

Credit is due to Quasimondo for the first #tweetcoding entry to use bitmap feedback, but I’m no stranger to the effect either, having implemented it in Director back in the day, and again in Flash when the FP8 beta was released.

feedbackvortex

q=550;g.clear();if(!o.b)addChild(o.c=new Bitmap(o.b=new BitmapData(q,q))).rotation=2;o.c.z=s(i+=.03)*70+30;ls(9,i*q);lt(q,0);o.b.draw(this);

Papervision3D Grass/Fur Effect

To me, “Shadow of the Colossus” represents the pinnacle of PlayStation 2 games. Aside from the fact that it’s one of the most artistic games I’ve ever played, it’s also one of the most technically brilliant games on the platform, pushing the PS2 to its limits with real-time motion blur, HDR rendering, a LOD landscape system, IK & physics system, self-shadowing and, of course, the impressive fur shading. However, the PS2 can’t handle all of those things done “properly”, so some were achieved with ingenious tricks and shortcuts, all covered in this inspiring article on The Making of “Shadow of the Colossus”.

Since reading that article I’ve always wanted to recreate the fur effect, and have now done so using Papervision3D. As I was hoping to use it on a recent project, instead of fur I decided to simulate grass:

Papervision Grass Effect

The effect is achieved by drawing cross-sections of the fur/grass on a set of parallel layers – in this case I have 7 layers which, using 512×512 alpha-transparent textures with 4×4 segments, is pushing the limits of Flash/PV3D. However, because we’re only ever dealing with 7 textures, it can handle potentially infinite blades of grass – the example above has 3000. The grass is generated in real-time – I build the layers up by starting each blade of grass with a certain size & direction, and growing it out on subsequent layers. The effect looks fine when the camera is close to perpendicular or the normal of the planes, but things start to fall apart when the planes and camera approach parallel.

The grass highlights are created with simple gradients, and I’ve also rendered basic shadows to the dirt layer (although they’re barely noticeable with such dense grass). Each blade of grass has a random colour, weighted heavily towards green but straying towards blue/brown in rare cases. Creating the effect of the grass moving in the wind is a simple matter of moving each plane along the X & Z axis, with the motion increasing based on the distance from the base layer.

Being a huge fan of both Ico and SOTC, I’m really looking forward to seeing the next game from Fumito Ueda and Team ICO, and what they can do with the PS3 hardware.