Showcase and discover digital art at yex

Follow Design Stacks

Subscribe to our free newsletter to get all our latest tutorials and articles delivered directly to your inbox!

Constructing 3D Classes

Constructing 3D Classes

This section describes how to construct classes that you can use to create 3D objects and environments in Flash. As you examine the code for each class, keep in mind the new features of ActionScript and see how I implement them. You will probably notice a few unfamiliar features of the ActionScript 2.0 syntax; I’ll discuss those features along the way.

Drawable

The first piece of code to write is actually an interface, and a very simple one at that. Despite its very short length and simplicity, this interface actually plays an important role later on. The Drawable interface consists of one function declaration:

interface com.lo9ic.Drawable {
function draw():Void;
}

All classes that get drawn to the screen implement this interface. Why you do this becomes clear later in the Scene class. Note: The example files are located in the com/lo9ic directory, so the fully qualified includes “com.lo9ic.”

Style

The Style class contains styling information for rendering the Drawable classes. This class consists of five private properties, including line weight, line color, line alpha, fill color, and fill alpha; and methods for accessing and setting them:

class com.lo9ic.Style {
private var $linealpha, $lineweight, $fillalpha : Number;
private var $linecolor, $fillcolor : String;

function Style() {
$lineweight = 0;
$linealpha = 100; $fillalpha = 100;
$linecolor = "0x000000"; $fillcolor = "0x000000";
}

public function set linealpha(a:Number):Void {
$linealpha = a;
}

public function set lineweight(a:Number):Void {
$lineweight = a;
}

public function set linecolor(a:String):Void {
$linecolor = a;
}

public function set fillalpha(a:Number):Void {
$fillalpha = a;
}

public function set fillcolor(a:String):Void {
$fillcolor = a;
}

public function get linealpha():Number {
return $linealpha;
}

public function get lineweight():Number {
return $lineweight;
}

public function get linecolor():String {
return $linecolor;
}
public function get fillalpha():Number {
return $fillalpha;
}

public function get fillcolor():String {
return $fillcolor;
}
}

This code implements a new feature of ActionScript 2.0 referred to as an implicit getter/setter function. An example of this function is one of the many functions that contain get or set immediately following the keyword() function. Implicit getter/setter functions allow you to make the properties of an object private and write functions that get and set the value of the properties, following good object-oriented coding practice. The advantage to using implicit getter/setter functions is that you can change the internal representation of a property without breaking other code that relies on the presence of those properties.

Notice that there is an extra function in the class that has exactly the same name as the class itself. This function is referred to as the constructor function. In ActionScript 2.0, constructor functions initialize the value of member properties and are executed whenever a new instance of the class is instantiated.

Node

Nodes are points in 3D space to which the endpoints and control points of lines, curves, and polygons are attached. Rather than belonging exclusively to one 3D object, nodes are often shared to increase the efficiency of a 3D engine. For example, you can define each face of a cube by four points. Because there are six faces in a cube, you need 24 points to define all faces. However, if the faces are allowed to share points, then only eight are required, reducing the number 3D points that the engine has to rotate to one-third the original number.

Before constructing the Node class, let’s take note of the properties and methods that it has to implement. First, the Node class contains x, y, and z properties that correspond to the three coordinates of 3D space. Second, each node is rotated using quaternions as I mentioned in the previous section; this means you have to write a Rotate method that takes a quaternion as an argument. Finally, a function that projects a 3D point to a 2D screen using a projection factor completes the class.

Here is the final code for the Node class:

import com.lo9ic.Quaternion;

class com.lo9ic.Node {
private var $x, $y, $z, $xp, $yp, $zp:Number;
private var $q1, $q2, $q3:Quaternion;

function Node(a:Number, b:Number, c:Number) {
$x = a ? a : 0; $xp = $x;
$y = b ? b : 0; $yp = $y;
$z = c ? c : 0; $zp = $z;
}

public function get x():Number {
return $xp;
}

public function get y():Number {
return $yp;
}

public function get z():Number {
return $zp;
}

public function set x(a:Number):Void {
$x = a;
}

public function set y(a:Number) :Void {
$y = a;
}

public function set z(a:Number):Void{
$z = a;
}

public function rotate(q:Quaternion):Void {
$q1 = q.copy();
$q1.invert();
$q2 = new Quaternion();
$q2.fromPoint($x, $y, $z);
$q3 = q.copy();
$q2.concat($q1);
$q3.concat($q2);
$xp = $q3.x;
$yp = $q3.y;
$zp = $q3.z;
}

public function project(a:Number):Void {
$xp = a*$xp/($zp-a);
$yp = a*$yp/($zp-a);
$zp = $zp;
}
}

Quaternion

The Quaternion class creates and stores axis and angle information. It contains properties and getter/setter functions for each of the four coordinates of the quaternion. It also contains methods that set the angle and rotation of the quaternion—one using the three coordinates of a point and one using an axis and an angle. The three additional functions for concatenating, inverting, and copying the quaternion are used for rotation and should look familiar from the rotate() method in the Node class.

Here is the final code for the Quaternion class:

class com.lo9ic.Quaternion {
private var $x, $y, $z, $w:Number;

function Quaternion(a:Number, b:Number, c:Number, d:Number) {
$x = a ? a : 0;
$y = b ? b : 0;
$z = c ? c : 0;
$w = d ? d : 1;
}

public function get x():Number {
return $x;
}

public function get y():Number {
return $y;
}

public function get z():Number {
return $z;

}
public function get w():Number {
return $w;
}

public function fromPoint(a:Number, b:Number, c:Number):Void
{
$x = a;
$y = b;

$z = c;
$w = 0;
}

public function fromAxisAngle(a:Number, b:Number,
c:Number, d:Number):Void
{
var ca = Math.cos(d/2); var sa = Math.sin(d/2);
var m = Math.sqrt(a*a + b*b + c*c);
$x = a/m * sa;
$y = b/m * sa;
$z = c/m * sa;
$w = ca;
}

public function concat(q:Quaternion):Void
{
var w1 = $w; var x1 = $x; var y1 = $y;
var z1 = $z;
var w2 = q.w; var x2 = q.x; var y2 = q.y;
var z2 = q.z;
$w = w1*w2 - x1*x2 - y1*y2 - z1*z2
$x = w1*x2 + x1*w2 + y1*z2 - z1*y2
$y = w1*y2 + y1*w2 + z1*x2 - x1*z2
$z = w1*z2 + z1*w2 + x1*y2 - y1*x2
}

public function invert():Void {
$x = -$x;
$y = -$y;
$z = -$z;
}

public function copy():Quaternion {
return new Quaternion($x, $y, $z, $w);
}
}

Line

The first class that uses the Drawable interface is the Line class. Instances of Line define a line in 3D space using two nodes as endpoints. The class also contains style and clip properties for the visual display. Just as you have done before, you have to create getter/setter functions for each property to access the private variables:

import com.lo9ic.Node;
import com.lo9ic.Style;
import com.lo9ic.Drawable;

class com.lo9ic.Line implements Drawable {
private var $startnode, $endnode:Node;
private var $style:Style;
private var $clip:MovieClip;

function Line(a:Node, b:Node) {
$startnode = a ? a : new Node();
$endnode = b ? b : new Node();
$style = new Style();


}
public function set startnode(a:Node):Void {
$startnode = a;
}

public function set endnode(a:Node):Void {
$endnode = a;
}

public function get startnode():Node {
return $startnode;
}

public function get endnode():Node {
return $endnode;
}

public function set style(a:Style):Void {
$style = a ? a:$style;
}

public function get style():Style {
return $style;
}

public function set clip(a:MovieClip):Void {
$clip = a;

}

public function draw():Void {
$clip.clear();
$clip.moveTo($startnode.x, $startnode.y);
$clip.lineStyle($style.lineweight,
$style.linecolor, $style.linealpha);
$clip.lineTo($endnode.x, $endnode.y);
$clip.swapDepths(10000+($endnode.z+$startnode.z)/2);
}
}

The draw() method of the Line class uses the drawing API to draw a line with the attributes specified in the Style property inside the movie clip referenced by the Clip property. The last line of code modifies the depth of the target clip so that it appears to be in front of other objects with lower average z values. This stacks objects that are closer to the viewer in a 3D environment on higher layers, which makes the scene more realistic.

Curve

Extend the Line class to create the Curve class because it shares so many of the same methods and properties. Like the Line class, the Curve class has a Drawable object, so implement the Drawable interface as well. Adding a property and getter/setter methods for the control point completes the new methods and properties for the class. As you did with Line, define a draw() method to complete the Drawable interface:

import com.lo9ic.Node;
import com.lo9ic.Style;
import com.lo9ic.Line;
import com.lo9ic.Drawable;

class com.lo9ic.Curve extends Line implements Drawable {
private var $controlnode : Node;

function Curve(a:Node, b:Node, c:Node) {
$startnode = a ? a : new Node();
$endnode = b ? b : new Node();

$controlnode = c ? c : new Node();
}

public function set controlnode(a:Node):Void {
$controlnode = a;
}

public function get controlnode():Node {
return $controlnode;
}

public function draw():Void {
$clip.clear();
$clip.moveTo($startnode.x, $startnode.y);
$clip.lineStyle($style.lineweight,
$style.linecolor, $style.linealpha);
$clip.curveTo($controlnode.x,
$controlnode.y, $endnode.x, $endnode.y);
$clip.swapDepths(10000+($endnode.z+$startnode.z+
$controlnode.z)/3);
}
}

Polygon

A polygon is a flat surface in 3D space that uses nodes for its corners. In 3D environments, polygons are often patched together to create more complex surfaces and solid objects. Like Line and Curve, the Polygon class has a Style and Clip property and getter/setter methods, and implements the Drawable interface:

import com.lo9ic.Drawable;
import com.lo9ic.Node;
import com.lo9ic.Style;

class com.lo9ic.Polygon implements Drawable {
private var $nodes:Array;
private var $style:Style;
private var $clip:MovieClip;
private var $i, $j, $z:Number; function Polygon() {
$nodes = new Array();
$style = new Style();
}
public function addNode(a:Node, b:Boolean) {
$nodes.push({node:a, control:b});
}
public function set style(a:Style):Void {
$style = a ? a:$style;
}

public function get style():Style {
return $style;
}

public function set clip(a:MovieClip):Void {
$clip = a;
}

public function draw():Void {
$i = 1;
$j = 1;
$z = 0;
$clip.clear();
$clip.moveTo($nodes[0].node.x, $nodes[0].node.y);
$clip.lineStyle($style.lineweight,
$style.linecolor, $style.linealpha);
$clip.beginFill($style.fillcolor, $style.fillalpha);
while( $i <= $nodes.length) {
$j = $i%$nodes.length;
if(!$nodes[$j].control && !$nodes[$i-1].control) {
$clip.lineTo($nodes[$j].node.x, $nodes[$j].node.y);
} else if ($nodes[$i-1].control) {
$clip.curveTo($nodes[$i-1].node.x,
$nodes[$i-1].node.y, $nodes[$j].node.x,
$nodes[$j].node.y);
}
$z += $nodes[$j].node.z;
$i++;
}
$clip.endFill();
$clip.swapDepths(10000+$z/$nodes.length);
}
}

Instead of having endpoint or control point properties, the Polygon class uses an array of nodes to define corners. The addNode() method provides a way to add nodes to this array, optionally allowing you to specify whether the node is a control point. The draw() method loops through this array, connecting the nodes and filling the area in between to form a surface with straight and curved boundaries. The implementation of the draw() method for the Polygon class is a little more complex than either Line or Curve because of the varying number of points a polygon can have.

Scene

The Scene class, the final one you need to learn about, manages the rendering and rotation of all objects in a 3D environment. This class contains two arrays for managing nodes and drawable objects, a quaternion for rotation, and the property “f” that is the projection factor I mentioned earlier in the Node class section. The Scene class itself is not rendered, but the objects within it are, so it makes sense to have Scene implement the Drawable interface as well. The addNode() and addObject() methods allow you to add nodes, lines, curves, and polygons to the scene; the draw() method loops through the node and object arrays, calling the Draw method on each node or object in the scene:

import com.lo9ic.Style; 
import com.lo9ic.Quaternion;
import com.lo9ic.Node;
import com.lo9ic.Drawable;

class com.lo9ic.Scene implements Drawable {
private var $f:Number;
private var $nodes:Array;
private var $objects:Array;
private var $quaternion:Quaternion;
private var $clip:MovieClip;
private var $i:Number;

function Scene() {
$f = 300;
$nodes = new Array();
$objects = new Array();
$quaternion = new Quaternion();
}

public function get f():Number {
return $f;
}

public function set f(a:Number):Void {
$f = a;
}

public function set quaternion(a:Quaternion):Void {
$quaternion = a;
}

public function get quaternion():Quaternion {
return $quaternion;
}

public function set clip(a:MovieClip):Void {
$clip = a;
}

public function get clip():MovieClip {
return $clip;
}

public function addNode(a:Node):Void {
$nodes.push(a);
}

public function addObject(a:Drawable):Void {
$objects.push(a);
}

public function draw():Void {
$i = 0;
while($i<$nodes.length) {
$nodes[$i].rotate($quaternion);
$nodes[$i].project($f);
$i++;
}
$i = 0;
while($i<$objects.length) {
$objects[$i].draw();
$i++;
}
}
}

By using an interface for the line, curve, and polygon classes, you simplify the process of adding objects to the scene with a single function that takes an object of type Drawable as the argument. The implementation of the interface for these classes also has the effect of reducing the number of loops required in the draw() method.

Comments