AS Reference  :  Notes Index  :  Resources  :  About/Contact  :  Downloads

3d wireframe object from a series of bezier curves

(Note: To stop the movie from redrawing each frame and eating up all of your processor power, click the reset button -- this sets the object back to its original unrotated position and stops the redraw until render or one of the rotate buttons is pressed again).

Classes to support 3D drawing

A few weeks ago, I took a look at Chad Corbin's article on creating realtime 3d objects in Flash, an example that appeals to me because it shows how to create and rotate something other than a cube. Rotating 3D cubes are nice as they go, but I always thought it would be even more interesting to know how to create something with a curve and then get that curve moving too. Since a shape can be so neatly defined (as a series of quadratic bezier curves and/or line segments) and produced in Flash, I thought it would be wonderful to allow a user to define his/her own shape (a series of curves) and then extend that shape to make a complete 3D object, either by extrusion or by creating a radially symmetrical object (this sample is a simple example of the latter).

So, in usual fashion, I spent a couple of days pulling Chad's code apart to figure out what he was doing and then another few days putting it back together into something I could hopefully use generically for future projects. In the process, I put together some methods of the Math class to create and manipulate a transform matrix, to find a point along a bezier curve, and to perform matrix-matrix and matrix-vector multiplication (necessary for projecting 3D definitions in a 2D space).

Setting the scene

The controlling class in this example is Scene, which includes properties to define the timeline on which the scene will be created, a shapes array which contains the individual shape movieclips, and a perspective factor. After drawing a default shape, the first thing done in the movie is to define a Scene object with this statement

var wireFrame = new Scene(this.mcShow, 300, false, 17);

This creates an object named wireFrame of type Scene which will reside in timeline mcShow, have perspective factor 300, no axes displayed and contain 17 shapes. Each of those shapes will be represented by a movieclip, whose reference is stored in the aShapes array (a property of the Scene object). Those shapes include the original shape-curve (defined by the user, or using the default) lying flat in the xy plane, plus 7 duplicates of it, rotated around the y axis at 45-degree angles, plus 9 horizontal circles along the length of the figure.

Defining a shape

Each shape is defined by 4 anchor points (endpoints, shown as dots in the movie above) and 3 control points (triangles), all of which may be dragged, within certain constraints, by the user. To convert the shape selected by the user into a Flash-usable object, I created an object with these properties:

In the movie, the curve that defines the shape of the 3d object is defined in movieclip mcDraw, and the 3d wireframe object is then created in movieclip mcShow. When a shape has been defined and the user clicks Render, function makeFrame is called. makeFrame translates the points specified in mcDraw into points usable within mcShow, with statements like:

var p0x = mcDraw.p0._x - mcDraw.p3._x;
var p0y = mcDraw.p0._y - offset;
var p1x = mcDraw.p1._x - mcDraw.p3._x;
var p1y = mcDraw.p1._y - offset;
...
var c1x = mcDraw.c1._x - mcDraw.p3._x;
var c1y = mcDraw.c1._y - offset;
where p0x/p0y define the starting point (in the xy plane), p1x/p1y define the endpoint of the first curve in the shape, and c1x/c1y define the control point of the first curve. When those points have been established, the setShapeProps method of wireFrame (an object of type Scene) is called. This passes all the properties of the shape (start point, array of curve definitions, lineweight, etc) to the movieclip that will represent it (saved in wireFrame.aShapes[0]):
// make the vertical lines: #1 position at z=0	
wireFrame.setShapeProps(0, {x:p0x, y:p0y, z:0},
   [ new Curve( {x:p1x, y:p1y, z:0}, {x:c1x, y:c1y, z:0} ),
     new Curve( {x:p2x, y:p2y, z:0}, {x:c2x, y:c2y, z:0} ),
     new Curve( {x:0, y:p3y, z:0}, {x:c3x, y:c3y, z:0} ) ],
   false, 1, 0xffffff, 100, 0, 0);

A curve is an object of the Curve class, defined by two points: the curve's endpoint, and its control point. The statement above sets the first shape in the scene (index=0) to start at a point defined by p0x/p0y/0, then curve to a point at p1x/p1y/0 using c1x/c1y/0 as the control, then curve to point p2x/p2y/0 using c2x/c2y/0 as the control, etc. The "false" parameter specifies that the curve is not closed (so no fill will be applied), and the remaining parameters define the line (white, pixel width 1, alpha 100) and fill (none, but we put color and alpha in anyway to show where a fill would go).

Duplicating the shape

That provides the definition of the first shape, a flat duplicate representation of the user's curve in the xy plane (z=0). Then seven duplicates must be created, each one rotated 45 degrees from the previous one around the y axis (ie, a rotation in the xz plane). This is the code that creates the first of those rotated duplicates:

// #2: original moved 45 degrees around y axis
wireFrame.setShapeProps(1, {x:p0x*cos45, y:p0y, z:p0x*sin45},
    [ new Curve( {x:p1x*cos45, y:p1y, z:p1x*sin45}, 
                 {x:c1x*cos45, y:c1y, z:c1x*sin45} ),
      new Curve( {x:p2x*cos45, y:p2y, z:p2x*sin45}, 
                 {x:c2x*cos45, y:c2y, z:c2x*sin45} ),
      new Curve( {x:0, y:p3y, z:0}, 
                 {x:c3x*cos45, y:c3y, z:c3x*sin45} ) ],
   false, 1, 0xffffff, 100, 0, 0);

where cos45 and sin45 are precalculated variables so that the calculation doesn't need to be done each time. The best way I can think of to picture where the values above came from is to imagine looking down at the object from the top. We want to project into the z plane an amount that is sin(45) times the original x value, and set the x value to cos(45) of what it was before. If you think of how you'd do a 2d rotation in the xy plane around the point (0,0) and then convert that to the xz plane instead (where the y axis represents the point 0,0), that is what's being done in the code to come up with a rotation around the y axis.

The above is repeated 6 more times in code (within function makeFrame) to come up with 8 total shapes, symmetrically placed around the y axis. Notice that we have created the shapes as they would exist in actual 3d space. Next, we need to convert them to their equivalent projections into 2D space, so they can be displayed and look like 3D objects. But before that conversion is done, we need to create the horizontal circles that form the cross pieces of the wireframe object.

Finding points along a bezier curve

Because our initial vertical shape consists of a series of 3 quadratic bezier curves, we needed a way to find points along those curves so we could place horizontal shapes (circles) in those points. This is done by using the math equation that defines a bezier curve as a series of points in time, where the start of the curve is defined by t=0 and the end by t=1. Unfortunately, equidistance in time along the curve is not equivalent to equidistance in space (there are more points in a tightly curved part than a straighter part), but for our purposes of displaying a frame of the object, the lines don't have to be exactly evenly spaced. We decided to divide each curve into 3 sections, drawing circles in the z plane at t=0, t=0.33 and t=0.67. The standard equation for finding a point at time t along a bezier curve is:

// p0 is initial point, c1 is control point, p1 is endpoint
// x(t) = p0x * (1-t) * (1-t) + 2 * c1x * t * (1-t) + p1x * t * t
// y(t) = p0y * (1-t) * (1-t) + 2 * c1y * t * (1-t) + p1y * t * t

which has been optimized for fewer multiply operations by the Man Who Can Leave No Equation Unoptimized, Robert Penner, into this form (here converted into a method which takes and returns x/y objects):

Math.pointOnCurve = function (p0, c1, p1, t) {
   return { 
      x:p0.x + t*(2*(1-t)*(c1.x-p0.x) + t*(p1.x - p0.x)),
      y:p0.y + t*(2*(1-t)*(c1.y-p0.y) + t*(p1.y - p0.y)) 
   };
};

This is called three times for each curve within the makeFrame function.

Projecting into 2D space

The projection from 3D definition to 2D space is where the matrices come in (this is the part of the code that I grabbed and converted from Chad's tutorial). We start by defining the transform matrix (here defined as a property of the Math object) to be an identity matrix (an array of 3 vectors which when used as a multiplier with another matrix leaves the original matrix intact). That is what the initTMatrix method of the Math class does.

Every time we want to redraw our 3D vectors in 2D space (once per frame in this example), we call the redraw function, which sets the transform matrix based on the amount of rotation in each direction (x/y/z) specified by the rotation buttons, and then calls the renderScene method of the wireFrame object. setTMatrix (a Math method) calculates a new transform matrix by multiplying the current transform matrix by a new matrix, determined by xvar/yvar/zvar and a variable I've called spinRate (set to 200 here). renderScene (a method of wireFrame) applies the new transform matrix to each point in each of the shapes to enable its display in 2D space.

Starting the rotation

In this movie, the 3D wireframe object is set to start rotating 5 degrees in each direction by setting xvar, yvar and zvar each to plus or minus five. Rotation may be speeded or slowed in any direction by clicking multiple times on the plus or minus rotation buttons (the effect is cumulative because the transform matrix is changed from its previous value in each frame, not from an initial value). As mentioned above, code on the reset button resets the transform matrix to an identity matrix, draws the object once with no rotation, and stops the rotation by deleting the _root onEnterFrame property. Code and fla for this movie may be downloaded here.

Intro
Flash: What & How
Example Sites
Create
Draw, Edit Shapes
Gradients
More Drawing Tips
Import
A Sample
Animate
Frames, Keyframes
Motion Tweens
More Motion Tweens
Shape Tweens
Masks
Control
Stop/Replay
Movieclips Intro
Movieclip Reference
Site Structure 1
Slideshow Movieclip
Contact Form
Scroll Resume
Preloader
Site Structure 2
Publish
Display Options
Player Detection
Optimize
AS 2.0 Basics
Intro to Syntax
Playhead Commands
Playhead Cmds 2
Coded Tween
onEnterFrame
Intro to Classes
Declare/Assign
Comments, Trace
Simple Data Types
Arrays & Objects
Code Blocks
Operators
Beyond Buttons
Code Structure
Toggle Controls
Group of Buttons
Drag and Hit
Distort Magnifier
Scroll Text
Bee Game
Dart Shooter
Sound Control
Easing Slider
Easing Slider 2
Components Intro
Timers & Delays
Dynamic Content
Intro
Drawing API
Create Text
Attach Movieclips
Easing Slider 3
Easing Slider 4
Load jpg/swf
Sliding Viewer
Preload swf
XML
Easing Slider 5
Server Comm
LoadVars (w/ PHP)
AS - PHP Lookup
Text File
Database 1:LoadVars
Database 2:Remoting
Read from directory
AS 2.0 Classes
Intro
Math
Key
Date
Color
EventDispatcher
New Samples
Pie Chart
Event-model Emailer
Tween Sequence
Fuse Sequence
SVG in Flash
Bitmap Topo
SWF as Data Holder
Two-level Menu
Yahoo! Flash Maps
Class-based Game
ASTB Samples
Disclaimer
3D Outlines
Bounce Collide
Address Book
Save Drawings
Home  :  Notes Index  :  Resources  :  About/Contact  :  Downloads