|
3.) A good amount of Algebra - this will come up all the time. One of
the, if not the ,most important things to get a good grasp of. We
used quite a bit of Algebra in my other tutorials but this one will be
even more so. d (d+z) ---- = ------ y1 ySomething important to note here...why did I use (z+d) instead of just z? Remember that the two triangles are proportional. Triangle B's side that lies parallel to the x-axis is not equal to z. It is equal to the distance between the user's eye and the screen plus z. The variable z only represents the distance along the z-axis from the screen...not the user. Let's take it a step further and try to solve for y1. First multiply both sides by 1/dAnd now you have solved for y1! Pretty easy stuff. Believe it or not getting the perspective equation for the x-coordinate is exactly the same. Exchange a x1 for the y1 and an "x" for the "y" in the above equation and that is all you need for the x-coordinate. With that out of the way you should have a fully functional 3d file. We have covered the coordinate transformations and changed 3d coordinates to a 2d screen. Optimizations are the next step. An obvious optimization would be to not multiply your point's positions by a transformation matrix that you are not going to even use. Meaning if you just want to put some points out on the screen and have it so that you can click and drag the points around then there is not reason to use a z-rotation matrix. You can only drag along the x- and y-axis so there will never be any z-movement. Just that small improvement will probably make all the difference in Flash. The next thing has to do with Flash's math objects. As you could see from my last tutorial the use of transcendental functions are necessary. Actually, to be exact, you need the sine and cosine of three angles (an angle of rotation around each axis...if you are not using the z-rotation matrix then you only have two angles) which means you are using six math functions. That can be a lot of strain on the CPU. The biggest mistake I see when I look at others' sources is that they calculate the sine and cosine of an angle too much. The only time you need to use the math objects is when you are changing the angle...thats assuming that all the points are being rotated by the same angles. Sometimes I see the math objects being used in a "for" loop when the script is rotating a bunch of points...something like ("ax", "ay", and "az" are the rotation angles around each axis): for (var i = 1; i <= numPoints; i++)
{
sinex = Math.sin (ax);
cosinex = Math.cos (ax);
siney = Math.sin (ay);
cosiney = Math.cos (ay);
sinez = Math.sin (az);
cosinez = Math.cos (az);
// and then the rest of the rotation script
}
That is kind of unnecessary since the rotation angles are not going to be changing within the "for" loop. Even if you put sine and cosine part outside of the "for" loop it is not necessary. Just create a function to call every time you increment the angle that way you are not doing anything more than you have to: function sineCosine (ax, ay, az)
{
sinex = Math.sin (ax);
cosinex = Math.cos (ax);
siney = Math.sin (ay);
cosiney = Math.cos (ay);
sinez = Math.sin (az);
cosinez = Math.cos (az);
}Also (this is kind of a small optimization) try to avoid alpha changes for the movie clip that you are rotating. If you have only a few objects on the stage then it is no big deal, but if you have a lot then do not even try it. I know thats not really a code optimization but it will speed up the movie. A huge optimization to make would be to use sine and cosine look-up tables instead of using Flash's math objects. But, changing over to look-ups will only be profitable if you have objects rotating around at different angle increments. If everything uses the same angles for rotations then you can use Flash's math objects only when needed like I described above. However if you need a bunch of objects rotating each at their own angle then thats when a look-up will help. Here is a simple look-up table: sine = new Array ();
cosine = new Array ();That
will create two arrays which hold the values for sine and cosine from 0
degrees to 360 degrees. It is pretty simple. If anything at all you may
not get why I used the "rad" variable. Flash's math objects like to use
radians not degrees. Our look-up tables, however, need to be in degrees so
that we can have nice integers. And if you remember anything from
trigonometry or my "Introduction to Trigonometry" you should know that
there are 2p radians in a full circle. Therefore
2p radians equals 360 degrees right. Something
like: 2p (rad) = 360 (deg)
-- divide both sides by 2 --
p (rad) = 180 (deg)
-- if you divide both sides by 180 you can solve for
So thats where I came up with "rad"...its used to translate from degrees to radians for the math objects. So to access the sines and cosines of your angles (ax, ay, and az) you will need something like this: sinex = sine[Math.round (ax)];
cosinex = cosine[Math.round (ax)];
siney = sine[Math.round (ay)];
cosiney = cosine[Math.round (ay)];
sinez = sine[Math.round (az)];
cosinez = cosine[Math.round (az)];
I had to add in that Math.round () stuff because the array elements are
integers. And even our look-up table could use some optimizations! I only
talk about this because two arrays of 360 elements each is quite a bit of
information for a 206k web plug-in to handle...in other words it could
slow down everything. If you are dealing with a more powerful programming
language this next part is pretty much irrelevant. sin q = cos (90 - q) And let's talk about why those are the way they are. Do you know what a sine and cosine curve looks like? Here is a quick ASCII picture I made: Sine curve: y = sin (x) in degrees:
| ,;'';
| .' '.
| / \
|/___________\______________
| \ /
| \ /
| * *
| ".."
-- Description: this graph only shows one period of the curve. A period
is the amount of range that the curve needs to make one full curve.
Everything else are just duplications that slide down the x-axis.Do
you see any similarities between the sine and cosine curves? They are the
exact same curve except offset from each other a little on the x-axis.
Meaning that if you were to take one curve and slide it down the x-axis
(left or right...does not matter) ninety untis the two curves would be
perfectly aligned. That is basically what our cofunction identities are
stating.Ok so now you know where we got the cofunction identities. Let's tie that into how we are going to optimize the look-ups. First of all, we know how to put sine in terms of cosine so that is major part in optimization. We just need one trigonometric function look-up...I'm going to use a look-up for cosine instead of sine and I'll tell you why in a moment: cosine = new Array ();
rad = Math.PI / 180;
for (var a = 0; a <= 360; a++)
{
cosine[a] = Math.cos (a * rad);
}If we have that we can simply evalulate the sine and cosine
of angles like this: sinex = cosine[90 - Math.round (ax)];
cosinex = cosine[Math.round (ax)];
siney = cosine[90 - Math.round (ay)];
cosiney = cosine[Math.round (ay)];
sinez = cosine[90 - Math.round (az)];
cosinez = cosine[Math.round (az)];But, that poses a few
problems. What if the angle of rotation around the x-axis was something
like 100 degrees. We could evaluate cosine of 100 easily since our look-up
table goes that high. However, when we try to find the sine of 100 since
we have to subtract it from 90 you would get a negative number. We do not
have any places in our cosine array for negative angles. This is
why I chose cosine for our look-up instead of sine...because it is an
even function. You would have known that if you read my
"Introduction to Trigonometry." Its near the end when I discuss the
fundamental identities like the Odd-Even Identities. Well just in case you
forgot the identities stated: sin (-q) = -sin q csc (-q) = -csc q
cos (-q) = cos q sec (-q) = sec q
tan (-q) = -tan q cot (-q) = -cot qI bolded the cosine
identity...do you see the relevance? When evaluating an angle for cosine
it does not make a difference whether it is positive or negative. You will
get the same number. You do not believe me? Get out a graphing calculator
and graph y = cos (x) and y = cos (-x) and the graphs will
overlap. Now we make a little change in the script: sinex = cosine[Math.abs (90 - Math.round (ax))];
cosinex = cosine[Math.round (ax)];
siney = cosine[Math.abs (90 - Math.round (ay))];
cosiney = cosine[Math.round (ay)];
sinez = cosine[Math.abs (90 - Math.round (az))];
cosinez = cosine[Math.round (az)];With that you can evaluate
all angles with one look-up table...well, not all entirely, but all
angles between 0 and 360. So that could be a pretty big
optimization...look-up tables that is. I have never done any benchmark
testing though. You could also use only one array for cosine for values
from 0 to 90 and then use a big string of "if" statements to see which
quadrant the terminal side of the angle lays to see what the ratio should
be. However, I think that a few extra elements in an array would be faster
than having to evaluate a lot of "if's".Also something very important that you may or may not have realized...hardly ever do you need all three rotation matrices. If your main goal is to have some menu items spinning around in 3D then you only need the x-rotation matrix and the y-rotation matrix. The main reason is because how can you drag on the z-axis with 2D movement? If you wanted to create a 3D type world that you could walk around then you only need one rotation matrix...the y-axis rotation. Of course that is with limited play but you need to cut corners every chance you get. In the case of a 3D world you would probably want the type of movement which allowed you to go forward and backward, left, right, and rotate left and right. Well I haven't explained how to translate points yet (you might already know but if you do not I will tell you later on) but with that description in the previous sentence you only need one rotation matrix. If an optimized script becomes a matter of life or death then you could just not multiply your points by a matrix if the angle of rotation is zero. That would not make much difference for a few objects but it could mean all the difference for quite a few. And for my last optimization tip (for right now) a basic culling script. Geometry culling is concerned with removing objects that you cannot see to speed things up. The one we want for right now is called view frustum culling. All it does is remove things that are not in your field of view. The easiest way to check for this if an object is behind you. Remember when we derived the perspective equations? That constant "d"? Well since that is the distance from the screen to you eye and since the z-axis goes into negative numbers as it comes towards you we can simply check if an object's z-postion goes less than "d"...if it does then it is behind you and no need to worry about it. This will save you some CPU usage even though it is a small thing. First of all you have a few less setProperty () actions. Second, the fewer amount of objects on the stage the faster your movie runs. Now that is only a small portion of what the view frustrum culling algorithm is capable of...but, we can't do anymore right now until I tell you about vectors. Once I cover vectors we are going to do more culling algorithms. But let me cover translating points real quick. When you translate a point you are simply moving it in a straight line. A point at (0,0,0) translate positive four units on the x-axis would be at (4,0,0). Same thing for all the axes. If you want a matrix to represent this it would look like this: | 1 0 0 Tx |
| 0 1 0 Ty |
| 0 0 1 Tz |
| 0 0 0 1 |
Uh-oh! There is something tricky going on in there! Yes that is a 4x4
matrix...so theoritically we are dealing with the 4th dimension...scary
eh? It is very simple to understand though. There is no way for me to
graphically show you, and is near impossible to visualize but yet it is
still easy to understand...for now. And no! The 4th dimension is
not time (in this instance...I am not sure in other
applications). | x |
| y |
| z |
| w | <- equal to one
...you will be merely adding on Tx, Ty, and Tz. Something crucial to
remember is that those values are increments. Meaning if you set Tx to one
it will slide the point along the x-axis continually because you keep
adding one to the point's position. | Sx 0 0 0 |
| 0 Sy 0 0 |
| 0 0 Sz 0 |
| 0 0 0 1 |
You do not really need the fourth column and row with that matrix
though. // given points A, B, and C in clockwise order
// vectors made up of the polygon's sides
vec1 = new Array (null, B.x-A.x, B.y-A.y, B.z-A.z);
vec2 = new Array (null, C.x-B.x, C.y-B.y, C.z-B.z);
// find the normal vector with the cross product
crossx = vec1[2]*vec2[3] - vec1[3]*vec2[2];
crossy = vec1[3]*vec2[1] - vec1[1]*vec2[3];
crossz = vec1[1]*vec2[2] - vec1[2]*vec2[1];
normal = new Array (null, crossx, crossy, crossz);
// view point vector
viewVec = new Array (null, 0, 0, 1);
// find the magnitudes of "normal" and "viewVec"
len1 = Math.sqrt (normal[1]*normal[1] + normal[2]*normal[2] +
normal[3]*normal[3]);
len2 = Math.sqrt (viewVec[1]*viewVec[1] + viewVec[2]*viewVec[2] +
viewVec[3]*viewVec[3]);
// find the dot product of the two vectors "normal" and "viewVec"
dot = normal[1]*viewVec[1] + normal[2]*viewVec[2] + normal[3]*viewVec[3];
// find the angle between the two vectors
cosTheta = dot / (len1 * len2);
theta = Math.acos (cosTheta * (Math.PI / 180));
// if theta is greater than ninety then dont worry about it
if (theta > 90) {
this._visible = false;
} else {
// polygon script
}Hopefully
that looks familiar. Also hopefully you see just how bulky and unecessary
most of that is. Thats a CPU eating script and would put your 3D work to
crap! First of all, if your object is centered at (0,0,0) then you could
take away over half of the above script. You could get away with
only this: // given points A, B, and C in clockwise order
// vectors made up of the polygon's sides
vec1 = new Array (null, B.x-A.x, B.y-A.y);
vec2 = new Array (null, C.x-B.x, C.y-B.y);
// find only the z-component of the normal vector
crossz = vec1[1]*vec2[2] - vec1[2]*vec2[1];
// if crossz is greater than zero the polygon is facing away.
if (crossz > 0) {
this._visible = false;
} else {
// polygon script
}Now this is where my inability to write what I am thinking
really ticks me off. Ok so you find the z-component of the perpendicular
vector. Why is it facing away if its greater than zero? Remember that the
z-axis increases as it moves away from you. So if you imagine a point
sitting out in front of a rotating plane (centered at the origin)
perpendicualar to the plane...can you see how the plane would be facing as
soon as the point's z-coordinate goes past the origin into the screen?
Hopefully you can because I am finding it difficult to explain
this.However, if your object is sitting somewhere else, sadly enough, you must use most of the large script from above...but not all. Here is an optimized version and then I'll explain it: // given points A, B, and C in clockwise order
// vectors made up of the polygon's sides
vec1 = new Array (null, B.x-A.x, B.y-A.y, B.z-A.z);
vec2 = new Array (null, C.x-B.x, C.y-B.y, C.z-B.z);
// find the normal vector with the cross product
var crossx = vec1[2]*vec2[3] - vec1[3]*vec2[2];
var crossy = vec1[3]*vec2[1] - vec1[1]*vec2[3];
var crossz = vec1[1]*vec2[2] - vec1[2]*vec2[1];
normal = new Array (null, crossx, crossy, crossz);
// view point vector
viewVec = new Array (null, 0, 0, 1);
// find the magnitudes of "normal" and "viewVec"
len = Math.sqrt (normal[1]*normal[1] + normal[2]*normal[2] +
normal[3]*normal[3]);
// find the angle between the two vectors
cosTheta = crossz / len;
/* If the cosine of theta goes less than 0 then the polygon is facing
the other way. The cosine of an angle greater than 90 and less
than 270 is a negative number so you only need to test the ratio
for its sign. */
if (cosTheta < 0) {
this._visible = false;
} else {
// polygon script
}
As you can see a little better. I was able to eliminate a sqrt () and
an acos (), both very CPU demanding. It is still slow for Flash but that
is as far as I have been able to optimize it (actually I still have one
more that I'll go over at the end). I am open to suggestions though. /* given points A, B, and C in clockwise order for the polygon
and (Lx, Ly, Lz) for the coordinates of the light */
// vectors made up of the polygon's sides
vec1 = new Array (null, B.x-A.x, B.y-A.y, B.z-A.z);
vec2 = new Array (null, C.x-B.x, C.y-B.y, C.z-B.z);
// find the normal vector with the cross product
var crossx = vec1[2]*vec2[3] - vec1[3]*vec2[2];
var crossy = vec1[3]*vec2[1] - vec1[1]*vec2[3];
var crossz = vec1[1]*vec2[2] - vec1[2]*vec2[1];
normal = new Array (null, crossx, crossy, crossz);
// find the center of the polygon
center = new Array ();
center[1] = (A.x + B.x + C.x) / 3;
center[2] = (A.y + B.y + C.y) / 3;
cemter[3] = (A.z + B.z + C.z) / 3;
// direction vector from the light to the polygon
dirVec = new Array (null, center[1]-Lx, center[2]-Ly, center[3]-Lz;
// find the magnitudes of "normal" and "dirVec"
len1 = Math.sqrt (normal[1]*normal[1] + normal[2]*normal[2] +
normal[3]*normal[3]);
len2 = Math.sqrt (dirVec[1]*dirVec[1] + dirVec[2]*dirVec[2] +
dirVec[3]*dirVec[3]);
// find the dot product of the two vectors "normal" and "direVec"
dot = normal[1]*dirVec[1] + normal[2]*dirVec[2] + normal[3]*dirVec[3];
// find the cosine of the angle between the two vectors
cosTheta = dot / (len1 * len2);
// Find the intesity of the light. Both the variables "amb" and
"maxLight" would be defined beforehand.
intesity = amb + (cosTheta * maxLight);
And as you can see from that its pretty slow. You have two square
roots, quite a few divides, and it is just overall messy. I'm going to
present an optimization for both lighting and culling in a minute but I
want to make sure you understand this. You may want to read over it again.
If there is still something really unclear email me and I will add in some
more to help clarify things. |