After I finished my last 3d tutorial I felt that I should write one more because I did not want to end this series with something as unpractical as solid objects with backface culling and dynamic lighting. This one will be much more relevant. The great thing about this one is that we really are not going to be talking about any major math stuff so it should be much easier. This time I am going to talk about movement in 3d and creating a really simple "3d Flash world" for you to walk around in. But first I wanted to go through that really basic 3d sample file I gave out in my first tutorial. Even if you downloaded the file from the last tutorial download this new one because I improved quite a few things. So if you have downloaded the file go ahead and open it up. On the stage you will see that I have a line, a movie clip with the text "3d stuff in here...", and some buttons. The line movie clip connects all the vertices of the box, the buttons control the rotations, and the other movie clip has the main actions. Let me first explain how my movie is structured. The movie clip with the main actions only rotates points that I defined at the beginning. The line movie clip then grabs those points and connects them. Thats the gist of it...pretty easy. Now let me explain the actions in the "3d stuff in here..." movie clip. I set up my box vertices in a certain order. It would be hard to explain so here is a picture: |
![]() |
I have an array, "pos", with elements 1 through 8 in it, which hold the positions of each point. Then inside each of those elements in the "pos" array I have another array with three elements. Element zero holds the x-position of the point, element one holds the y-positions, and element two holds the z. Next I have one more array called "ppos", which holds the x- and y-positions of the points when the perspective is added. Those are the numbers that the line script will grab and connect. Now lets get into the actual script. The very first thing I have is a pretty long initializing script all within an 'onClipEvent (load)' handler. In there I had this first: var lineConnectString = new String ("122334415667788515263748");
for (var j = 0; j <= 11; j++) {
_root.line.duplicateMovieClip ("line"+j, j);
_root["line"+j].pointSets = lineConnectString.substr (j * 2,2);
}
Those few lines takes the string, "lineConnectString" and sends every
two numbers to a duplicate of the line movie. The line movie clip then
uses those numbers to get the coordinates of the points which it is
supposed to connect. The way I came up with the "lineConnectString" is by
looking at the picture from above. The script inside the line movie clip
is pretty basic so I won't cover that. var width = 50;
var height = 50;
var depth = 50;
pos = new Array ();
pos[1] = new Array (-width, -height, -depth);
pos[2] = new Array (width, -height, -depth);
pos[3] = new Array (width, height, -depth);
pos[4] = new Array (-width, height, -depth);
pos[5] = new Array (-width, -height, depth);
pos[6] = new Array (width, -height, depth);
pos[7] = new Array (width, height, depth);
pos[8] = new Array (-width, height, depth);
ppos = new Array ();
for (var a = 1; a <= 8; a++) {
ppos[a] = new Array ();
}
This sets up the vertices for the cube. You could change around the
width, height, and depth variables to something different and make a
rectangular box. The variable "pos" is a two-dimensional array which holds
the x-, y-, and z-positions of the eight vertices. The "ppos" is also a
two-dimensional array which holds the x- and y-positions of the points
with the perspective added in. These are the coordinates which the
line script will be retrieving. There may have been a better way to do
this but I was not too concerned about it. dtr = Math.PI / 180;
a = b = c = 0;
function sineCosine (a, b, c) {
sa = Math.sin (a * dtr);
ca = Math.cos (a * dtr);
sb = Math.sin (b * dtr);
cb = Math.cos (b * dtr);
sc = Math.sin (c * dtr);
cc = Math.cos (c * dtr);
}
sineCosine ();
d = 300;
centerx = 275;
centery = 200;
The variable "dtr" is for translating from degrees to radians. Flash
likes radians, but it is much easier to work with degrees. If you want to
rotate a point by one degree you simply increment a variable by one,
easily done with the "++" operator. However if you wanted to rotate a
point by the equivalent of one degree in radians you would need to
increment an angle by 0.0174532925199433. Thats a little odd so I just do
things in degrees and then translate to radians when I need to. The
variables a, b, and c are the angle increments at which the points are
going to be rotating. I set them to be zero initially. for (var j = 1; j <= 8; j++) {
// multiply positions by a x-rotation matrix
rx1 = pos[j][0];
ry1 = pos[j][1] * ca + pos[j][2] * -sa;
rz1 = pos[j][1] * sa + pos[j][2] * ca;
// multiply new positions by a y-rotation matrix
rx2 = rx1 * cb + rz1 * sb;
ry2 = ry1;
rz2 = rx1 * -sb + rz1 * cb;
// multiply new positions by a z-rotation matrix
rx3 = rx2 * cc + ry2 * -sc;
ry3 = rx2 * sc + ry2 * cc;
rz3 = rz2;
// set arrays to new positions
pos[j][0] = rx3;
pos[j][1] = ry3;
pos[j][2] = rz3;
// add perspective
per = d / (d+rz3);
tempx = rx3 * per;
tempy = ry3 * per;
// set perspective array
ppos[j][0] = centerx + tempx;
ppos[j][1] = centery - tempy;
}
I left my comments in the script above but I'll still explain it. The
above is the main script which rotates all the points around. I put
everything in a "for" loop so I can rotate all eight points. The first
nine lines rotate the points around the x-, y-, and z-axes. We have
already derived the equations so that should look familiar. If you have
not already, read this
to learn why those equations work. After the rotations I then set the
array "pos" equal to the rotated points to update everything. That right
there is pretty much all there is to the 3d part of the
script. // given (x,y,z) of a point (xp,yp) is the point with perspective
x*d
xp = -----
(z+d)
y*d
yp = -----
(z+d)
Now the reason I set a variable called "per" first is because that is
the part that both the perspective equations have in common. All you do is
multiply "per" by the "x" and "y" of a point and it is the same as the
above two equations. This is also a small optimization because it gets rid
of doing the same thing twice which includes a costly division. Then the
next two lines I set the perspective arrays for the lines and add in the
"centerx" and "centery" variables to center the box with Flash's stage. If
I didn't add those two variables to the points' positions the box would be
in the top-left hand corner. // translate points by Tx, Ty, and Tz fx = rx3 + Tx; fy = ry3 + Ty; fz = rz3 + Tz; // set arrays to new positions pos[j][0] = fx; pos[j][1] = fy; pos[j][2] = fz; // add perspective per = d / (d+fz); tempx = fx * per; tempy = fy * per; // set perspective array ppos[j][0] = centerx + tempx; ppos[j][1] = centery - tempy; Once again pretty simple. Also something to remember is that my scripts
could most likely be optimized more. Although I do not think you would see
much difference in speed you could simplify it. The reason I wrote it the
way I did was so it would be easy to read. |
![]() |
That would be easy. But, what I was having trouble with is being able to rotate the box around and getting the ball to move in the same direction. Example: lets say the box was exactly how it is above and a ball was bouncing inside it. However, the ball was only bouncing back and forth, hitting the front face of the box, bouncing off, and then hitting the back face, and coming back. Now lets say that you move around and look at the box from the side (note: I am not talking about rotating the box but instead you)...if you were to look at the box from the side the ball would be bouncing side to side now instead of back and forth right? Well I really did not know how to do it. You wouldn't believe some of the things I was thinking of to do this...I don't even want to talk about because it was some pretty stupid stuff. Well, in any case I finally figured it out and I was shocked at how simple it was. To do this we are not going to rotate points in the same fashion as we did before. In all the other files hitherto we have rotated a point by increments right? Meaning, if you created a file with the 3d stuff in a continuos loop and you rotated a point by "a" degrees the point would be constantly rotating because "a" only represents an increment. This time, however, if you set "a" equal to a number other than zero it will rotate the point around "a" degrees only once. So if you want a continuos rotation you are going to have to keep incrementing "a." Here, I made another file to demonstrate what I was talking about: you can see it here and download it here. If you look at the script there are only a few changes I made. The first was in the initializing script. I added this line: inca = incb = incc = 0; Those variables are used for incrementing the rotation angles. After that I changed the way I set up my function for calculating the sine and cosine of our angles. In the previous file the rotation angles were hardly ever changing. The only time you needed to calculate sine and cosine was when you pressed one of the buttons. This time, however, if there is any rotation at all you need to calculate sine and cosine. Here are the functions I used to optimize this part: function angleA () {
sa = Math.sin (a * dtr);
ca = Math.cos (a * dtr);
}
function angleB () {
sb = Math.sin (b * dtr);
cb = Math.cos (b * dtr);
}
function angleC () {
sc = Math.sin (c * dtr);
cc = Math.cos (c * dtr);
}
// initialize
angleA ();
angleB ();
angleC ();
Using those functions you only need to get the sine and a cosine of an angle when that particular angle is changing. Complementary to the above functions I add this just before the "for" loop: if (inca != 0) {
a += inca;
angleA ();
} if (incb != 0) {
b += incb;
angleB ();
} if (incc != 0) {
c += incc;
angleC ();
}
First you check if there is any change in the rotation angles. If there is you increment the angle and call the function for that angle. So now we are only using Flash's math objects when it is absolutely necessary. Although I did not do it in my file a look-up table may be profitable here. Just something to try if you want. The last thing I changed in the main script was that I took out the three lines of script that looked like this: pos[j][0] = rx3; pos[j][1] = ry3; pos[j][2] = rz3; Those lines were used to update the "pos" array. It was also those
lines that made a, b, and c only increment rotation angles. Since we do
not use those lines anymore the points are never really moving. We
temporarily rotate them and plot them, but we never update the array so
the points don't ever change. And finally, I changed the button actions to
increment the increment variables (inca, incb, and incc) instead of the
rotation angles. _root.ball.bounds = new Array (width, height, depth); That takes the width, height, and depth of the box and sends it to the ball script. The actions in the ball uses those values a lot and I didn't want to have to type out the long path every time. Next I added one more element to the "pos" array for the ball: pos[9] = new Array (0, 0, 0); This will make the ball start at the origin. You could change it to
whatever you want though. Something very important to note here is that if
you wanted to add more points to rotate around you would need to make the
ball's position *last* in the "pos" array. The main reason is because if
you did not do that the script inside the ball would get messed up. You
will see why in a minute. // how many points you are rotating including the ball numTimes = 9; _root.ball.n = numTimes; And the last thing changed in the main script is when you are adding in the perspective: per = d / (d+rz3);
tempx = rx3 * per;
tempy = ry3 * per;
if (j != numTimes) {
// set perspective array
ppos[j][0] = originx + tempx;
ppos[j][1] = originy - tempy;
} else {
_root.ball._x = originx + tempx;
_root.ball._y = originy - tempy;
_root.ball._xscale = _root.ball._yscale = (_root.ball.r*2)*per;
}
The first three lines of that is nothing new because you always need to
do that stuff. However the next part is different. You have a conditional
statement checking if the "for" loop increment variable is not equal to
the total number of points you are rotating. Why? Because if "j"
equals "numItems" that means the coordinates we just rotated are the
ball's coordinates. If "j" does not equal "numItems" you are only rotating
one of the box's vertices. So? Well if you are messing with one of
the box's vertices you only need to update the "ppos" array for the lines.
If you are messing with the ball's coordinates you need to do some "set
properties" to render the ball. Easy right? vel = new Array (1, 3, 5); bpos = new Array (); r = this._width/2; The first variable is an array called "vel" which holds the velocities
on all three axes. We will be adding these values to the ball's
coordinates to make it move. Next is another array called "bpos". You
really do not need this but I used just to keep from getting mixed up. In
the rest of the script I set the values of "bpos" equal to the coordinates
of the ball plus the velocities, then I check for collisions using
the "bpos" array, and finally update the "pos" array in the movie clip
"td". And that "r" variable is just the radius of the ball. I used that in
the main script for the rotations too. for (var c = 0; c <= 2; c++) {
bpos[c] = _root.td.pos[n][c] + vel[c];
if (bpos[c] + r > bounds[c]) {
vel[c] *= -1;
bpos[c] = bounds[c] - r;
} else if (bpos[c] - r < -bounds[c]) {
vel[c] *= -1;
bpos[c] = -bounds[c] + r;
}
_root.td.pos[n][c] = bpos[c];
}
This is the script which makes all the movement. Instead of dealing
with all the axes separately I put everything in a "for" loop. Since all
the variables we are dealing with are arrays its easy to do it in a loop
because we can access the elements with the increment variable. When "c"
is one we are dealing with the x-axis, when "c" is two the y-axis, and
when "c" is three the z-axis. Probably one of the most unpractical uses for Flash yet one of the
coolest. This effect has become somewhat fashionable, popularized by sites
like Vox Angelica as will as
others. Even though you may be "awed" at first, hopefully after reading
some of this you will see how to do stuff like that. for (var d = 1; d <= 20; d++) {
tree.duplicateMovieClip ("tree"+d, d);
}
b = 0;
Tz = 0;
h = 30;
dtr = Math.PI / 180;
function sineCosine (b) {
_root.sb = Math.sin (_root.b * _root.dtr);
_root.cb = Math.cos (_root.b * _root.dtr);
}
sineCosine (b);
The first three lines do the obvious, which is to duplicate the trees.
Then I initialize a few constants. The variable "b" is the rotation
increment, "Tz" is used for translating along the z-axis (forward and
backward movement), "h" is going to be your height while walking around,
and "dtr" is for changing degrees into radians. The next few lines are for
the function which calculates the sine and cosine of the rotation angle,
once again *only* used when needed. onClipEvent (load) {
rotationInc = 2;
translateInc = 10;
}
onClipEvent (keyDown) {
if (Key.isDown (Key.LEFT)) {
_root.b = -rotationInc;
_root.sineCosine (b);
} if (Key.isDown (Key.RIGHT)) {
_root.b = rotationInc;
_root.sineCosine (b);
} if (Key.isDown (Key.UP)) {
_root.Tz = -translateInc;
} if (Key.isDown (Key.DOWN)) {
_root.Tz = translateInc;
}
}
onClipEvent (keyUp) {
if (Key.getCode() == Key.LEFT) {
_root.b = 0;
_root.sineCosine (b);
} if (Key.getCode() == Key.RIGHT) {
_root.b = 0;
_root.sineCosine (b);
} if (Key.getCode() == Key.UP) {
_root.Tz = 0;
} if (Key.getCode() == Key.DOWN) {
_root.Tz = 0;
}
}
Its kind of long but there's nothing to it. The first part initializes
the increments for the rotation and translation. The next 'onClipEvent ()'
checks where one of the keys are pressed. If a key is pressed you set
either "b" or "Tz" on the main stage equal to one of the increments and
call the 'sineCosine' function if needed. Then, the last 'onClipEvent ()'
checks for when a key is released. When a key is released the the
increment for that key is set to zero on the main stage and the
'sineCosine ()' function is called if needed. if (this._name == "tree") {
this._visible = false;
}
d = 600;
xctr = 275;
yctr = 200;
x = random (2000) - 1000;
z = random (2000) - 1000;
size = 50;
The first three lines remove the tree movie clip that we duplicated to
get it out of the way. The "d" variable is once again used for
perspective. But, notice how this time I used a much larger number. Since
we are dealing with a larger scale of 3d here I used a larger value. If I
didn't use such a big value you would have a "fisheye" type look to
everything. The next two variables are used to make up for the fact that
(0,0) is at the top left hand corner, again. The variables "x" and "z" are
initialized to a random number between 1000 and -1000. Those values will
be the coordinates where the tree is placed. There is no "y" value because
trees are placed out in front of you (z-axis) and to the sides of you
(x-axis), not in the air (y-axis). And the last variable, "size", is used
to scale the tree. You could use any number you wanted to depending on how
big you wanted the trees. // Y rotation x = _root.cb * x - _root.sb * z; z = _root.sb * x + _root.cb * z; // Translate and make first person viewpoint z += _root.Tz - d; The first two lines should look familiar since it is only a rotation around the y-axis. However the last line may not look so ordinary. That line takes care of the translation and makes your view point first person. I'll explain one thing at a time. Translating of the z-axis is like walking forward and backward...that is all adding "Tz" does to the trees. But, why is that I subtract "d" from the z coordinate too? Remember that "d" represents the distance from your eye to the origin. Well in real life there is no distance from your eye to the origin because your eye is the origin. Therefore you need to temporarily shift the tree's z-position towards you by "d" units, render everything, and then put it back. That will give you the first person type view. Lets look at the rest of the script: // if the object is behind you don't render
if (z < -1*d) {
this._visible = false;
} else {
pers = d / (z + d);
xp = x * pers;
scale = Math.abs (pers*size)
// if the object is off the stage don't render
if ((xctr + xp) + scale/2 < 0 || (xctr + xp) - scale/2 > 550) {
this._visible = false;
} else {
this._visible = true;
yp = _root.h * pers;
this._x = xctr + xp;
this._y = yctr + yp;
this._width = this._height = scale;
this.swapDepths (-z);
}
}
// change back from first person
z += d;
I wanted this script to do as few actions as possible to add a little
bit more speed to the movie. The first thing I do is check if the object
is behind you. I discussed this in my last tutorial
on 3D so I'm not going to say much about it except that an object is
behind you if its z-position is less than the negative of "d". So if an
object is behind you its visibility is set to 0 (makes it invisible) and
you can skip the rest of the script. Otherwise you run it through one more
conditional statement checking if the object is on the stage. Before you
can check if an object is on the stage or not you need get its x-position
relative to the stage (with perspective that is) and its current scale.
Once that is done there is the last conditional which keeps you from doing
more calculations than needed. onClipEvent (load) {
wid = this._width / 2;
}
onClipEvent (enterFrame) {
this._x -= _root.b*10;
if (this._x+wid < 550) {
this._x = 550;
}
if (this._x-wid > 0) {
this._x = 0;
}
}
All that script does is move the mountains side to side depending on
what "b" equals, then it checks if the mountains go off the stage. If they
do you reset them. The bad thing about the script is that I had to do some
guess and check kind of stuff. You see that I multiply "b" by 10 in the
first line of the "enterFrame" clip event? I just messed with numbers for
that until I could get it to look right. I know, bad, bad, bad...but I
couldn't help it. I still don't know how to do that the right
way. |