Seamlessly Combining 2D and 3D in Flash with Planes, Part 2 [AS3] · Sep 12, 07:09 AM

Part 1 of this article made the assumption that there is a camera pointed at the origin that doesn’t move. With this assumption in mind, we are able to greatly simplify the mathematical calculations necessary to combine 2D and 3D elements in Flash. With this knowledge, you can take a 2D element and render that in 3D space, animate it somehow, and then suck it back into 2D space. In applying this concept to more complicated situations where the camera is allowed to move, our assumptions still hold true. Here’s how: imagine a plane that is always situated 1000 units in front of the camera. No matter how you move the camera around, this plane is always parallel to the camera lens, and perpendicular to the direction of the camera.

What we’re actually talking about here is not really a plane, but rather a 3D container in 3D-space, but it helps to visualize this object as a plane. In Papervision3D, we create this container as an instance of a DisplayObject3D

  1. private var static3dMainContainer:DisplayObject3D = new DisplayObject3D("static3dMainContainer");

Now positioning this container is actually pretty simple:

  1. /**
  2. * Call this function every time the camera moves position or rotates
  3. */
  4. public function positionStatic3dContainer():void {
  5. static3dMainContainer.copyTransform(cameraAsCamera3D.transform);
  6. static3dMainContainer.moveForward( _staticDistanceCamera );
  7. }

In the positionStatic3dContainer function above, copyTransform positions the container exactly where the camera is, and moveForward transforms the container in front of the camera. As long as we call positionStatic3dContainer every time the camera moves, our static3dMainContainer will always be positioned in the same place, relative to the camera.

For the purpose of freezing a DisplayObject (which will be described next) it will be useful to add a child DisplayObject3D to static3dMainContainer that remains aligned with the TL-corner of the viewport. We name this child container staticTL3dMainContainer. Lets assume the viewport is scaled to the stage, we can add an event listener for RESIZE:

  1. protected function onStageResize(event:Event = null):void {
  2. static3dRight_X = _stage.stageWidth/2;
  3. static3dLeft_X = -static3dRight_X;
  4. static3dTop_Y = _stage.stageHeight/2;
  5. static3dBottom_Y = -static3dTop_Y;
  6.  
  7. staticTL3dMainContainer.x = static3dLeft_X;
  8. staticTL3dMainContainer.y = static3dTop_Y;
  9. dispatchEvent(new Event(Event.RESIZE));
  10. }

Freezing a 2D DisplayObject in 3D Space

By freezing, we mean taking some sort of 2D display object currently visible somewhere on the stage—like a sprite or movieclip—and temporarily transferring the visual representation of this object to 3D space. Lets say we have a sprite named mysprite. The first step to transferring this object to 3D is to create a PV3D MovieMaterial with this sprite:

    var plane_mat:MovieMaterial = new MovieMaterial(mysprite);

Then create a plane with this material

    var plane = new Plane(plane_mat, mysprite.width, mysprite.height, 4, 4);

The MovieMaterial and Plane objects have a lot of options. For optimization reasons, it is imperative to use the correct options for each object you create. In order to simplify this process, here is a function that combines the creation of a MovieMaterial and a Plane with various options, in one step:

  1. /**
  2. * Render DisplayObject to a plane in 3d space.
  3. * @paramsourceDisplayObject, Sprite, MovieClip, etc...
  4. * @paramswitchesmaterial options. defaults: transparent=F, animated=F, precise=F, doubleSided=F,smooth=F,interactive=F,allowAutoResize=T, all other options also allowed
  5. * @paramsegmentsW
  6. * @paramsegmentsH
  7. * @return new Plane. (remember: use plane.parent to get container)
  8. */
  9. public function attachDOtoPlane(source:DisplayObject, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4 ):Plane {
  10. var initProps:Object = { transparent:false, animated:false, precise:false };
  11. var props:Object = { doubleSided:false, smooth:false, interactive:false, allowAutoResize:true };
  12. var bounds:Rectangle = source.getBounds( viewport );
  13. var prop:String;
  14.  
  15. for (prop in initProps) {
  16. if (switches && switches.hasOwnProperty(prop)) {
  17. initProps[prop] = switches[prop];
  18. }
  19. }
  20.  
  21. var plane_mat:MovieMaterial = new MovieMaterial(source, initProps.transparent, initProps.animated, initProps.precise);
  22.  
  23. // default props
  24. for (prop in props) {
  25. if (switches && switches.hasOwnProperty(prop)) {
  26. plane_mat[prop] = switches[prop];
  27. } else {
  28. plane_mat[prop] = props[prop];
  29. }
  30. }
  31.  
  32. // additional props
  33. for (prop in switches) {
  34. if (! (initProps.hasOwnProperty(prop) || props.hasOwnProperty(prop)) ) {
  35. plane_mat[prop] = props[prop];
  36. }
  37. }
  38.  
  39. return new Plane(plane_mat, source.width, source.height, segmentsW, segmentsH);
  40. }

Using the attachDOtoPlane function would look something like this:

var plane:Plane = attachDOtoPlane(mysprite, {transparent:true, animated:true}, 4, 4);

This creates a plane with 4 horizontal and vertical segments, who’s material is defined by mysprite. Since we set transparent:true and animated:true, the material is transparent and animated.

At this point, the plane has been successfully created but it’s positioning is not the same as the original sprite’s positioning. While our sprite is aligned with a registration point (origin) at it’s TL-corner, by default our plane’s origin is at it’s center. We use the function do3dAlignTL3d from Part 1 of this article to move the origin of the plane to it’s TL corner. This function places the Plane inside of a DisplayObject3D and then transformations should be performed on the container, not the plane.

var newContainer:DisplayObject3D = do3dAlignTL3d(plane, source.width, source.height);

To position the container, we need the x,y coordinates of mysprite in relation to the PV3D viewport. Luckily, there is a built-in function that makes this easy:

var bounds:Rectangle = mysprite.getBounds( viewport );

Finally, we can position the container using these bounds:

var newLocation:Point = TL2dtoTLStatic3d( new Point(bounds.x, bounds.y) );
newContainer.x = newLocation.x;
newContainer.y = newLocation.y;

TL2dtoTLStatic3d is a function from Part 1 of this article that simply sets bounds.y = -bounds.y, while bounds.x is left unchanged. This line is necessary because the y-axis in PV3D is flipped in relation to the y-axis of the stage.

So now, we can put it all together to form the function attachDOtoTLStatic3dPlane:

  1. /**
  2. * Render DisplayObject to a plane in 3d space that does not appear to move and corresponds to original 2d space.
  3. * The plane is placed inside a container that remains aligned with the flash TL 2d coordinate system.
  4. * @paramsourceDisplayObject, Sprite, MovieClip, etc...
  5. * @paramswitches[SEE: attachDOtoPlane]
  6. * @paramsegmentsW[SEE: attachDOtoPlane]
  7. * @paramsegmentsH[SEE: attachDOtoPlane]
  8. * @return new Plane. (remember: use plane.parent to get container)
  9. */
  10. public function attachDOtoTLStatic3dPlane(source:DisplayObject, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4 ):Plane {
  11. var bounds:Rectangle = source.getBounds( viewport );
  12.  
  13. var plane:Plane = attachDOtoPlane(source, switches, segmentsW, segmentsH );
  14.  
  15. var newContainer:DisplayObject3D = do3dAlignTL3d(plane, source.width, source.height);
  16. var newLocation:Point = TL2dtoTLStatic3d( new Point(bounds.x, bounds.y) );
  17.  
  18. newContainer.x = newLocation.x;
  19. newContainer.y = newLocation.y;
  20.  
  21. staticTL3dMainContainer.addChild(newContainer);
  22.  
  23. return plane;
  24. }

And finally, freezeDO simply calls attachDOtoTLStatic3dPlane and keeps track of all frozen DisplayObjects so they can be unfrozen at some later time. (freezeContainersMap is an Object hash):

  1. /**
  2. * Hides a DisplayObject and renders it via the 3d viewport instead
  3. * @paramsourceSource Object
  4. * @paramswitches[SEE: attachDOtoPlane]
  5. * @paramsegmentsW[SEE: attachDOtoPlane]
  6. * @paramsegmentsH[SEE: attachDOtoPlane]
  7. * @returnsnew plane (remember: use plane.parent to get container)
  8. */
  9. private var freezeDO_count:int = 0;
  10. public function freezeDO(source:DisplayObject, switches:Object = null, segmentsW:Number=4, segmentsH:Number=4 ):Plane {
  11. var plane:Plane = attachDOtoTLStatic3dPlane( source, switches, segmentsW, segmentsH );
  12.  
  13. plane.name = "freezeDO-plane-" + freezeDO_count;
  14. (plane.parent as DisplayObject3D).name = "freezeDO-cont-" + freezeDO_count++;
  15.  
  16. source.visible = false;
  17. freezeContainersMap[source] = plane.parent;
  18. return plane;
  19. }

Unfreezing the DisplayObject

Presumably, we performed some cool 3D transformations on the frozen DisplayObject3D or Plane, and eventually returned the 3D object to it’s original transformation. Unfreezing the DisplayObject is rather straightforward:

  1. /**
  2. * Unfreeze the specified DisplayObject
  3. * @paramsourceSource Object that freezeDO() has been called on
  4. */
  5. public function unfreezeDO(source:DisplayObject):void {
  6. removeTLStatic3dPlane(freezeContainersMap[source] as DisplayObject3D);
  7. source.visible = true;
  8. }

  1. /**
  2. * use this to reverse attachDOtoTLStatic3dPlane()
  3. * WARNING: you should not have added any other children to the container (DO3D) of the plane
  4. * @paramcontainer Container or Plane
  5. */
  6. public function removeTLStatic3dPlane(container:DisplayObject3D):void {
  7. if (container is Plane) container = container.parent as DisplayObject3D;
  8.  
  9. for each (var o:Plane in container.children) {
  10. container.removeChild(o);
  11. MaterialManager.unRegisterMaterial(o.material);
  12. }
  13.  
  14. staticTL3dMainContainer.removeChild(container);
  15. }

Putting it all Together: BasicViewPlus

Download BasicViewPlus

All the code presented in Parts 1 and 2 of this series is from the class I use, unexcitedly named BasicViewPlus. This class extends BasicView and provides very similar behavior while adding some utility functions. The constructor for BasicViewPlus adds a stage argument to the BasicView constructor:

public function BasicViewPlus(stage:Stage, viewportWidth:Number = 640, viewportHeight:Number = 480, scaleToStage:Boolean = true, interactive:Boolean = false, cameraType:String = "Target")

As of this writing, you should always set scaleToStage=true, but this will be changed in the future.

The biggest difference from BasicView is that BasicViewPlus sets up the camera differently (as discussed at the beginning of Part 1) in the constructor:

camera.z = -_staticDistanceCamera;
camera.zoom = 2;
camera.focus = _staticDistanceCamera / camera.zoom;

An Example?

In Part 1 I promised that Part 2 would feature an example. Well now the example will have to wait until Part 3…

— Pickle

---

Comment

  1. Are you planning on putting out the third part to this anytime soon? I’ve been looking forward to checking that out for some time. Your posts have really been helpful to me. Thanks. J

    J · Nov 13, 07:48 AM · #

  2. Thanks for the feedback, J. I’ve been jumping around on some different projects and am just waiting for the opportunity to present itself where part 3 will flow out of something I’m working on… probably in the next month or two :)

    Pickle · Nov 13, 12:21 PM · #

Textile Help