Home Labs Galleries PetitOn

Using Sandy 3D Flash Library

Part 7, Working with Sprites

In the earlier chapters of this series, we have used primitives in all experiments. They are all sub classes to Object3D. This is true for the sprites as well, even though they are not 3D objects in the true sense. No lines are drawn between 3D points and no distorions are calculated for their textured skins. The rational for using sprites are that we save on computing resources, and avoid the construction of complicated forms.

You may want to download the resources for this part of the tutorial here. It may take a while to download, as there are some rather heavy fla files included this time. The total is a bit over 3 MB.

What are sprites?

According to Understanding Sandy by the eminent author of Sandy, a Sprite2D is - "A false 3D object" that - "... allows us to include pre-rendered graphical elements (images) in a true 3D space, and to produce a simple perspective effect on them. As a result a Sprite always shows the same face no matter what position the camera is in. Its image always faces us."

There is also a Sprite3D which, by the same eminent source, is described as - "Another false 3D object". and which like the Sprite2D - "...  allows us to display a pre-rendered element. But this object can render more interesting things, since it comprises a skin .. containing 360 images, which allows it to reproduce a true 3D effect with pre-rendered elements."

Elegantly put, now doubt!
So a sprite is an Object3D, but not a 3D object, just to make that clear ;)

The Sprite2D will contain one bitmap, that always faces the camera, while the more elaborate, and presumably heavier Sprite3D, will present a different bitmap to the viewer  depending on the view angle.

Sprite2D and the Camera3D

We are going to create a Sprite2D and check if it really shows the same Face to the camera. The sprite will by default be placed in the center of the world, but can of course be translated to another point, by using the usual TransformGroup and a Transform3D.

This time we'll just create the sprite and let it stay in the origin of the global coordinate system. Here is the createScene() function.

function createScene( bg:Group ):Void {
	//Create a texture skin to use
	var skin:Skin = new TextureSkin( BitmapData.loadBitmap( "silhuette" ) );
	// Create the sprite
	var sprite:Object3D = new Sprite2D( 1.5 );
	// Apply the skin to the sprite.
	sprite.setSkin( skin, true );
	bg.addChild( sprite );
}

We create a textured skin by loading a bitmap "silhuette" from the library.
Then we create an instance of Sprite2D, which is a sub class of Object3D.
The constructor takes one optional argument, which is a scale factor, with a default value of 1. Finally we apply the skin to the sprite, and add it to the root group of our world.

Now to see the true character of the Sprite2D, we want to move the camera around, without loosing sight of the sprite. We can do this by adding some sliders to the Stage  for moving the camera. The init method calls setUpControls() to set up the event handlers.

function setUpControls(){
	var xCh:Object = new Object();
	xCh.onChange = function(evt:Object){
		xTrans.text = evt.value;
		setPosition();
	}
	xSlider.addListener(xCh);
	
	var yCh:Object = new Object();
	yCh.onChange = function(evt:Object){
		yTrans.text = evt.value;
		setPosition();		
	}
	ySlider.addListener(yCh);
	
	var zCh:Object = new Object();
	zCh.onChange = function(evt:Object){
		zTrans.text = evt.value;
		setPosition();		
	}
	zSlider.addListener(zCh);
}

It sets up onChange event handlers for three sliders on Stage, which updates a text field presenting its value, and calls the setPosition() function to update the camera position.

// Set the camera position in global coordinates
// Look towards the origin
function setPosition(){
	cam.setPosition( xSlider.value, ySlider.value, zSlider.value - 500);
	cam.lookAt( 0, 0, 0 );
}

We make sure to look at the sprite at the origin, using the lookAt() function. At least it should be there, because we didn't ask it to move anywhere else.

The "silhuette" is a PNG image, whit transparent background in the library, a bit bigger than is shown here. If everything works as it should, the sprite is facing ( sic ! ) the viewer, wherever the camera is. It is however not at a fixed position in relation to the camera. When we move the camera towards the sprite ( positive z direction ), it seems to grow, and when we move away it shrinks.

No matter how you move your camera, that stubborn guy always shows you his profile. Neat, isn't it ?

[ As a side note, you might have noticed that the coordinate planes are visible from both sides, which the were not in earlier examples. The reason is that I have added back skins to the planes and disabled back face culling. I'm learning ;)]

The honored author of Sandy has a slightly more advanced demo example on the Sandy web site, with a more relevant use of the Sprite2D - watch the demo and get the source!

Rotational symmetry

One reason to use a Sprite2D may be to present a more or less complicated body with rotational symmetry. For a real 3D object the positions and angles for a lot of vertices and skinned faces must be calculated and drawn, which requires a lot of CPU power.
If we can use a sprite to fake the 3D body, we avoid the construction and we demand less power form the computer, by using a pre rendered bitmap.

Let's see how to use a Sprite2D to give the illusion of a 3D body with cylindrical symmetry, a barbershop pole!

I have drawn the pole in the Flash IDE, and converted it to a MovieClip. I placed an instance called "pole" in the first frame, but outside the visible area of the Stage.

function createScene( bg:Group ):Void {
	// Create the sprite
	var sprite:Object3D = new Sprite2D( 1.5 );

	//Create and set the skin
	var bitmap:BitmapData = new BitmapData(pole._width, pole._height,true, 0x00000000);
	bitmap.draw(pole);
	var skin:Skin = new TextureSkin( bitmap );	
	sprite.setSkin( skin );

	// Apply some transform
	var tg:TransformGroup = new TransformGroup();
	var trans:Transform3D = new Transform3D();
	trans.translate(-30,18,-40);
	tg.setTransform(trans);

	tg.addChild(sprite);
	bg.addChild( tg );
}

After creating a Sprite2D, here with a scale factor of 1.5, we have to craft a skin for it. We can create a BitmapData with the same size as the MovieClip and draw the "pole" clip into that. The result is a bitmap, which we pass as texture to the TextureSkin constructor.

To make the scene a little more interesting, we translate the sprite, using a TransformGroup.

To move our camera around the scene, we want to add some controls for that.
This can be done in a lot of ways, but a simple approach is to take advantage of the camera's moveSideways(  ) function. It moves the camera in a direction perpendicular to its line of sight, i.e. along its local local x axis. Than if we call the lookAt( 0, 0, 0 ) for each sideways movement, the camera will turn, and the next sideways movement will go in a slightly different direction. If the movements are small, we will rotate the camera in a circle around the global y axis. ( It is not a perfect circle, rather a spiral )

Using the moveUpwards(  ) function we can also move around the x axis. Here is the control scheme. We listen to the world's onRenderEVENT and let the camRotate() function handle the camera. To prevent the camera from running wild, we listen for the onMouseDown event and toggle a Boolean flag for the running state.

function setUpControls(){
	world.addEventListener(World3D.onRenderEVENT, this, camRotate );
	// Toggle the running status flag
	Mouse.addListener(screen);
	onMouseDown = function(){
		running = !running;
	}
}

// Rotate and look at the origin
function camRotate(){
	if( running ){ // Move only if running == true
		cam.moveSideways( (_xmouse-200)/10 );
		cam.moveUpwards( (_ymouse - 100)/10);
		cam.lookAt( 0, 0, 0 );
	}
}

The camera rotation will be proportional to the distance of the mouse pointer to the center of the screen. If it is near the center, the motion will stop.

Fairly convincing, if you stay close to the horizontal plane. We'll have to accept ,that the bottom of the pole is straight, and that the stripes on the pole looks the same from every angle. This may not be important, if the viewer is occupied with other interesting things in your world, such as the barbershop itself, and the cars and people passing by ;)

Moving sprites

As we have seen above, sprites can be moved around in the world, using a Transfrom3D translation. If you try to use a rotational transform, strange things will happen. We can also use position interpolators, for continuous movements, as we shall see.

The portrait of Magritte was traced in Flash MX and mounted in a MovieClip, to give it some transparency. It was the just dragged onto the Stage. I left the "pole" sprite in position for reference. What is new in the createScene function, is an Array of Sprite2D and an Array of position interpolators, for the raining gentlemen.

Here is the addition to the createScene() function

// Create another skin for the raining gentlemen
var skin:Skin = new TextureSkin( bitmap );
var skinM:Skin = new TextureSkin( BitmapData.loadBitmap("magritte") );
// Create the raining men and add them to the root Group
createSprites( bg, skinM );

A new TextureSkin is made, using the "magritte" bitmap, again a PNG image. It was first traced and stripped of its original background in Flash, then converted to a PNG image with a transparent background in PaintShop Pro. All images are embedded in the SWF, but you could easily load external images as well.

The interesting news here lies within the createSprites() function.

function createSprites( bg:Group, skin:Skin){
   for( var i = 0; i < 15; i++){
 	sprites[i] = new Sprite2D();
	sprites[i].setSkin(skin);

	var tg:TransformGroup = new TransformGroup();
	var xStart:Number = randRange(-300, 300);
	var yStart:Number = randRange(120, 150);
	var zStart:Number = randRange(-600, 600);
	var zStop:Number = randRange(-450, 600);		
	var dur:Number = randRange( 75, 150 );

	var easing = new Ease();
	transforms[i] = new PositionInterpolator( easing.create(),dur, 
				new Vector(xStart,yStart,zStart), 
				new Vector(xStart,-200,zStop));
	transforms[i].addEventListener( InterpolationEvent.onEndEVENT, this, loop );
	tg.setTransform(transforms[i]);
	tg.addChild( sprites[i] );
	transforms[i].pause();
	bg.addChild( tg );
   }
}

A number of new sprites with the "magritte" skin are created and stored in the global "sprites" Array.

For each sprite we also create a TransformGroup and set a  PositionInterpolator with a linear Ease function ( the default ). We have to reach it from an event handler, so we add it to a global "transforms" Array.

To make the raining gentlemen with the "stiff upper lips" a little more vivid, we randomize the start and end positions, as well as the duration of the interpolations, within boundaries. The randRange() function should have been included in the Math class, in  my opinion, but I found it in the Flash MX help file.

We add the each sprite to its TransformGroup, and add that to the world's root Group. We also listen for the interpolator's onEndEVENT and set a loop() function as event handler. The interpolators are immediately paused, to wait for user interaction to start the raining.

By the way, here is the randRange() function, which gives us a random number within the specified interval.

function randRange(min:Number, max:Number):Number {
    var randomNum:Number = Math.floor(Math.random() * (max - min + 1)) + min;
    return randomNum;
}

Now we have to take care of the looping and the user interaction. Here is the loop.

function loop( e ){
	var trf:PositionInterpolator = e.getTarget();
	trf.setDuration(randRange( 75, 150 ));
	trf.redo();
}

When an interpolator gets to the end of its interval, it fires an onEndEVENT, and the event handler is passed an event object, from which we can extract the source of the event, the target. To further randomize the behavior, we set a new random duration, and order the interpolator to start all over. So the interpolator will move its sprite back  into the sky, and let it fall again, as long as user doesn't stop it.

The user will start and stop the camera movements as before, but also the rain.
Here is the slightly changed control set up

function setUpControls(){
	world.addEventListener(World3D.onRenderEVENT, this, camRotate );
	Mouse.addListener(screen);
	onMouseDown = function(){
		running = !running;
		moveSprites();
	}
}

A mouseDown event from the screen will toggle the running flag and call the new moveSprites(), which will pause or resume all interpolators.

function moveSprites(){
	for ( var i = 0; i < sprites.length; i++){
		running? transforms[i].resume( ):transforms[i].pause();
	}
}

If we are running, all transforms will resume their work, otherwise they will be paused.

You could certainly elaborate on this movie. There should be fluffy sailing clouds over a Magritte blue sky in the background. And oh, I know, the gentlemen should have umbrellas, and maybe green apples suspended in the air, hiding part of their faces.
I'll leave that for you to tinker with.

But first a steaming hot ...( isn't that the ugliest cup? :)

Could I MovieSkin a sprite?

Hey, I'm happy you asked!
A MovieSkin is a TextureSkin, so indeed you could.

Lets first create MovieClip in the library. Anything that moves will do. Here I opened the empty MovieClip and imported an animated GIF image, I had in store directly to the Stage. The nice thing when you import a GIF animation, is that Flash gets all the images out, and places them one into each key frame. We immediately get a looping MovieClip animation, ready to place on the Stage. Just for fun, I drew a TV set as a frame for the animated image. I kept the user interaction with the camera and tuned the createImage

function createScene( bg:Group ):Void {
	// Create the sprite
	var sprite:Object3D = new Sprite2D( 2 );
	//Create and set the skin
	var mc:MovieClip = this.attachMovie("teve", "teve", this.getNextHighestDepth());
	teve._alpha = 0x000000;
	var skin:Skin = new MovieSkin( teve );
	sprite.setSkin( skin );

	// Translate it a bit
	var tg:TransformGroup = new TransformGroup();
	var trans:Transform3D = new Transform3D();
	trans.translate(-150,18,-40);
	tg.setTransform(trans);
	tg.addChild(sprite);
	bg.addChild( tg );
}

The MovieClip in the library is exported for ActionScript with the id "teve", and we can give the instance on Stage the same name. We set its alpha = 0, to hide the clip on  Stage. Then we just pass the clip to constructor of a new MovieSkin, and apply the skin to the sprite. As in earlier cases, we shift the sprite a bit off side from the origin.

Eadweard Muybridge ( 1830 1904) was a dedicated and hard working photographer, who besides portrait and social photography, made serious studies of animal and human bodies in motion. The dancing girl above, like many other motion series, has in the era of the Internet, been digitized and converted to GIF animations.

Thanks Eadweard! What you did was great, I only wish I had your glass plates to work with ;)

Enough already? Or should we take a shot at the Sprite3D? I must admit I'm dubious about that creature. Think 360 images, one for each degree of view angle, oh my!

A sprite with many faces

A Sprite3D requires a MovieSkin with a MovieClip containing 360 frames, with something visible in each frame. For each degree in the angle of view, it will show a corresponding frame to the camera. If we are planning to use images, this means we have to create one image per frame. That's a lot!

Many animation applications can export a series of images, so we may not have to handcraft each image. For this experiment, I hovered the web for a free to use GIF animation, with 360 frames, as I know that Flash MX would resolve it into separate images for me. Such animations are really hard to find, so I finally settled for an MPEG movie from Nasa's Visible Earth, the rotate_320.mpg, which shows the full rotation of the home planet.

You can import it to the Stage in Flash MX and export it as GIF or PNG images. I had to do a lot of experimenting to get the size I wanted, and to set the background transparent, which I did in Jasc Animation Shop. If you are interested in the long story, you can read it here.

I ended up with 240 bitmaps, which I imported into a MovieClip in the library. To make good on the requirement of 360 frames, I simply copied the first 120 frames, and added them to the end of the MovieClip's time line. This is far from ideal, as we will see.

Here is the new createScene() function

function createScene( bg:Group ):Void {
	// Create the sprite
	var sprite:Object3D = new Sprite3D( 0, 5  );

	//Create and set the skin
	var mc:MovieClip = this.attachMovie("earth", "Earth", this.getNextHighestDepth(),{_x:-100, _y:-100});
	var skin:MovieSkin = new MovieSkin( mc );
	sprite.setSkin( skin );

	var tg:TransformGroup = new TransformGroup();
	var trans:Transform3D = new Transform3D();
	trans.translate(40,10,0);
	tg.setTransform(trans);
	tg.addChild(sprite);
	bg.addChild( tg );
}

The Sprite3D constructor optionally takes two arguments. The first is an offset into the time line of the MovieClip, pointing to the frame corresponding to 0 degree view angle, the second is a scale factor for the sprite.

The MovieSkin is created by attaching the "earth" MovieClip from the library to the Stage, somewhere outside the viewable area, and passing a reference to the clip to the MovieSkin constructor.

To enhance the view, I have scaled the sprite by a factor 5, and given it a slight translation.
I also restricted the camera movement in the global y direction, as it tends to get running wild when you get closer to the zenith. Here is my modified camRotate() function.

function camRotate(){
  if( running ){
	cam.moveSideways( (_xmouse-200)/10 );
	if( Math.abs(cam.getPosition().y) < 500 )
		cam.moveUpwards( (_ymouse - 100)/10);
	cam.lookAt( 0, 0, 0 );
  }
}

Now the camera cannot get higher above or below ground, than 500, and we can study the rotating planet without stress.

When you rotate the camera in a plane parallel to the "ground" or zx plane, a new frame of the skin is presented for each degree of rotation. As you can see if you move the camera straight upwards, you have the same view of the planet. In other words, the view angle in the horizontal plane determines, what frame of the skin is presented to the camera.

I mentioned above, that having just 240 frames, and merely copying the first 120 to fill the last frames, wasn't such a good idea. We can easily see that the planet surface makes a giant jump at the 360 degree point of rotation.

By the way, you might have noticed a spot of light reflection on the earth. This has nothing to do with the Sandy light source, which is safely shut off.
The light comes from Nasa.

Now, couldn't we have done all this using the MovieClip in a MovieSkin on a Sprite2D?
Yes we could. The Sprite3D is a Sprite2D with a MovieSkin, but then the earth would have rotated independent of the viewing angle, and we would have had to invent the clever one-frame-a-degree scheme ourselves. Thanks Thomas!

So that'll be all for now, in anticipation of what comes next.
Keep experimenting and have fun!