3D Lighting

While the last example is pretty close to perfect in terms of rendering, it still kind of lacks something. Its a bit flat. OK, OK, you already know where Im heading with this, because it says so right in the section title, so lets add some 3D lighting.

Like backface culling, the specifics behind 3D lighting can get pretty complex and math intensive . I dont really have the space to get into a detailed discussion of all the finer points, but a quick web search will turn up more information on the subject than you could probably read in a lifetime. What Im going to give you here are the basics, along with some functions you can use and adapt as needed.

First of all, you need a light source. A light source at its simplest has two properties: location and brightness. In more complex 3D systems, it may also be able to point in a certain direction, have a color of its own, have falloff rates, conical areas, etc. But all that is beyond the scope of what you are doing here.

First youll create the light as a simple generic object:

 var light:Object = new Object(); 

Then, in the init function, youll position it and assign its brightness:

 function init() {       light.x = -200;       light.y = -200;       light.z = -200;       light.brightness = 100;       ... 

Two things important to note here. One is that the position is used to calculate the angle of the light only. The strength of the light you are creating does not fall off with distance. Thus changing the x, y, and z to ˆ 2,000,000 or down to ˆ 20 would make no difference in terms of how brightly the object was lit. Only the brightness property will change that characteristic of the light. Also, brightness must be a number from 0 to 100. If you go outside of that range, you can wind up with some odd results. Later Ill show you how you can validate this value and make sure it is within range before using it.

Now, what the light source is going to do is change the brightness of the color of a polygon, based on the angle of the light that is falling on that polygon. So if the polygon is facing directly at the light, it will display the full value of its color. As it turns away from the light, it will get darker and darker . Finally, when it is facing completely away from the light source, it will be completely in shadow and colored black.

So, what you need is a function that will take a particular polygon, looks at the base color of that polygon and the angle and brightness of the light, and returns an adjusted color. Here is that function:

 function getTriangleColor(tri:Object):Number {       var pointA:Object = points[tri.a];       var pointB:Object = points[tri.b];       var pointC:Object = points[tri.c];       var lightFactor:Number = getLightFactor(pointA, pointB, pointC);       var red:Number = tri.col >> 16;       var green:Number = tri.col >> 8 & 0xff;       var blue:Number = tri.col & 0xff;       red *= lightFactor;       green *= lightFactor;       blue *= lightFactor;       return red << 16  green << 8  blue; } 

This function gets passed a reference to a single triangle object. It then gets references to the three point objects that make up that triangle and passes them to another function, getLightFactor . Youll see that function in a moment. For now, you just need to know that it returns a number from 0.0 to 1.0. This is how much you need to alter the color of that particular triangle, 1.0 being full brightness, and 0.0 being black.

Now you cant just multiply the triangles color by this lightFactor . That will give you a completely random color, not just a darker version of the original. What you need to do is separate out the red, green, and blue values of that color, multiply each one of those by the lightFactor , and then join them back together again.

Separating and joining color component values is fully covered in Chapter 4, and you use those exact techniques here to come up with a new color, which is returned by the function.

Now, how do you come up with this lightFactor ? Lets look at the next function:

 function getLightFactor(ptA:Object, ptB:Object, ptC:Object):Number {       var ab:Object = new Object();       ab.x = ptA.x - ptB.x;       ab.y = ptA.y - ptB.y;       ab.z = ptA.z - ptB.z;       var bc:Object = new Object();       bc.x = ptB.x - ptC.x;       bc.y = ptB.y - ptC.y;       bc.z = ptB.z - ptC.z;       var norm:Object = new Object();       norm.x =   (ab.y * bc.z) - (ab.z * bc.y);       norm.y = -((ab.x * bc.z) - (ab.z * bc.x));       norm.z =   (ab.x * bc.y) - (ab.y * bc.x);       var dotProd:Number = norm.x * light.x +                            norm.y * light.y +                            norm.z * light.z;       var normMag:Number = Math.sqrt(norm.x * norm.x +                                      norm.y * norm.y +                                      norm.z * norm.z);       var lightMag:Number = Math.sqrt(light.x * light.x +                                       light.y * light.y +                                       light.z * light.z);       return (Math.acos(dotProd / (normMag * lightMag)) / Math.PI)               * light.brightness / 100; } 

Now, that is quite a function, huh? To fully understand all thats going on here, youd have to have a good grasp on advanced vector math, but Ill try to walk through the bare basics of it.

First of all you need to find the normal of the triangle. This is a vector that is perpendicular to the surface of the triangle, as depicted in Figure 17-5. Imagine you had a triangular piece of wood and you put a nail through the back of it so it stuck out directly through the face. That nail would represent the normal of that surface. If you study anything about 3D rendering and lighting, you are going to see all kinds of references to normals.

image from book
Figure 17-5: The normal is perpendicular to the surface of the triangle.

You can find the normal of a surface by taking two vectors that make up that surface and calculating their cross product . A cross product of two vectors is a new vector, which is perpendicular to those two. The two vectors you will use will be the lines between points A and B, and points B and C. Each vector will be held in an object with x, y, and z properties.

 var ab:Object = new Object(); ab.x = ptA.x - ptB.x; ab.y = ptA.y - ptB.y; ab.z = ptA.z - ptB.z; var bc:Object = new Object(); bc.x = ptB.x - ptC.x; bc.y = ptB.y - ptC.y; bc.z = ptB.z - ptC.z; 

Then you calculate the normal, which is another vector. Youll call this object norm . The following code computes the cross product of the vectors ab and bc :

 var norm:Object = new Object(); norm.x =   (ab.y * bc.z) - (ab.z * bc.y); norm.y = -((ab.x * bc.z) - (ab.z * bc.x)); norm.z =   (ab.x * bc.y) - (ab.y * bc.x); 

Again, I dont have the space to cover the details of why this is calculated this way, but this is the standard formula for calculating a cross product. If you are interested in how this is derived, you can check any decent reference on linear algebra.

Now you need to know how closely that normal aligns with the angle of the light. Another bit of vector math goodness is called the dot product , which is the difference between two vectors. You have the vector of the normal, and the vector of the light. The following calculates that dot product:

 var dotProd:Number = norm.x * light.x +                      norm.y * light.y +                      norm.z * light.z; 

As you can see, dot products are a bit simpler than cross products!

OK, you are almost there! Next, you calculate the magnitude of the normal, and the magnitude of the light, which you might recognize as the 3D version of the Pythagorean theorem:

 var magN:Number = Math.sqrt(N[0] * N[0] +                             N[1] * N[1] +                             N[2] * N[2]); var lightMag:Number = Math.sqrt(light.x * light.x +                                 light.y * light.y +                                 light.z * light.z); 

Note that this lightMag variable is calculated every time a triangle is rendered, which allows for a moving light source. If you know that the light source is going to be fixed, you could create this variable at the beginning of the code and calculate it one time in the init function. This would add quite a bit of efficiency to the file. Finally, you take all these bits youve just calculated, and put them into the magic formula:

 return (Math.acos(dotProd / (normMag * lightMag)) / Math.PI)               * light.brightness / 100; 

Basically, dotProd is one measurement and normMag * lightMag is another. Dividing these two gives you a ratio. Recall from our discussion in Chapter 3 that the cosine of an angle gives you a ratio, and the arccosine of a ratio gives you an angle. So using Math.acos here on this ratio of measurements gives you an angle. This is essentially the angle at which the light is striking the surface of the polygon. It will be in the range of 0 to Math.PI radians (0 to 180 degrees), meaning its either hitting head on or completely from behind.

Dividing this angle by Math.PI gives you a percentage, and multiplying that by the percentage of brightness gives you your final light factor, which you use to alter the base color.

Remember I told you that the brightness of the light needs to be from 0 to 100. Here is where you can validate that if you are worried about someone setting it too high or low. Just before the return statement you could place lines like:

 light.brightness = Math.min(light.brightness, 100); light.brightness = Math.max(light.brightness, 0); 

These will guarantee that the value stays in range.

OK, now all this was just to get a new color for the surface! Implementing it in your existing code is actually pretty easy. It goes right in the renderTriangle function. Where previously you were just using the base color of the triangle like so:

 beginFill(tri.col, 100); 

now you get the modified color and use that:

  var col:Number = getTriangleColor(tri);  beginFill(  col  , 100); 

To wrap things up, here is the full and final code for ch17_03.fla :

 var points:Array = new Array(); var triangles:Array = new Array(); var fl:Number = 250; var vpX:Number = Stage.width / 2; var vpY:Number = Stage.height / 2; var zOffset:Number = 400;  var light:Object = new Object();  init(); function init() {  light.x = -200;   light.y = -200;   light.z = -200;   light.brightness = 100;  points[0] =   {x: -50, y:-250, z:-50};       points[1] =   {x:  50, y:-250, z:-50};       points[2] =   {x: 200, y: 250, z:-50};       points[3] =   {x: 100, y: 250, z:-50};       points[4] =   {x:  50, y: 100, z:-50};       points[5] =   {x: -50, y: 100, z:-50};       points[6] =   {x:-100, y: 250, z:-50};       points[7] =   {x:-200, y: 250, z:-50};       points[8] =   {x:   0, y:-150, z:-50};       points[9] =   {x:  50, y:   0, z:-50};       points[10] =  {x: -50, y:   0, z:-50};       points[11] = {x: -50, y:-250, z: 50};       points[12] = {x:  50, y:-250, z: 50};       points[13] = {x: 200, y: 250, z: 50};       points[14] = {x: 100, y: 250, z: 50};       points[15] = {x:  50, y: 100, z: 50};       points[16] = {x: -50, y: 100, z: 50};       points[18] = {x:-200, y: 250, z: 50};       points[17] = {x:-100, y: 250, z: 50};       points[19] = {x:   0, y:-150, z: 50};       points[20] = {x:  50, y:   0, z: 50};       points[21] = {x: -50, y:   0, z: 50};       triangles[0] =  {a:0, b:1,  c:8,  col:0xffcccc};       triangles[1] =  {a:1, b:9,  c:8,  col:0xffcccc};       triangles[2] =  {a:1, b:2,  c:9,  col:0xffcccc};       triangles[3] =  {a:2, b:4,  c:9,  col:0xffcccc};       triangles[4] =  {a:2, b:3,  c:4,  col:0xffcccc};       triangles[5] =  {a:4, b:5,  c:9,  col:0xffcccc};       triangles[6] =  {a:9, b:5,  c:10, col:0xffcccc};       triangles[7] =  {a:5, b:6,  c:7,  col:0xffcccc};       triangles[8] =  {a:5, b:7,  c:10, col:0xffcccc};       triangles[9] =  {a:0, b:10, c:7,  col:0xffcccc};       triangles[10] = {a:0, b:8,  c:10, col:0xffcccc};       triangles[11] = {a:11, b:19,  c:12,  col:0xffcccc};       triangles[12] = {a:12, b:19,  c:20,  col:0xffcccc};       triangles[13] = {a:12, b:20,  c:13,  col:0xffcccc};       triangles[14] = {a:13, b:20,  c:15,  col:0xffcccc};       triangles[15] = {a:13, b:15,  c:14,  col:0xffcccc};       triangles[16] = {a:15, b:20,  c:16,  col:0xffcccc};       triangles[17] = {a:20, b:21,  c:16,  col:0xffcccc};       triangles[18] = {a:16, b:18,  c:17,  col:0xffcccc};       triangles[19] = {a:16, b:21,  c:18,  col:0xffcccc};       triangles[20] = {a:11, b:18,  c:21,  col:0xffcccc};       triangles[21] = {a:11, b:21,  c:19,  col:0xffcccc};             triangles[22] = {a:0,  b:11, c:1,  col:0xffcccc};       triangles[23] = {a:11, b:12, c:1,  col:0xffcccc};       triangles[24] = {a:1,  b:12, c:2,  col:0xffcccc};       triangles[25] = {a:12, b:13, c:2,  col:0xffcccc};       triangles[26] = {a:3,  b:2,  c:14, col:0xffcccc};       triangles[27] = {a:2,  b:13, c:14, col:0xffcccc};       triangles[28] = {a:4,  b:3,  c:15, col:0xffcccc};       triangles[29] = {a:3,  b:14, c:15, col:0xffcccc};       triangles[30] = {a:5,  b:4,  c:16, col:0xffcccc};       triangles[31] = {a:4,  b:15, c:16, col:0xffcccc};       triangles[32] = {a:6,  b:5,  c:17, col:0xffcccc};       triangles[33] = {a:5,  b:16, c:17, col:0xffcccc};       triangles[34] = {a:7,  b:6,  c:18, col:0xffcccc};       triangles[35] = {a:6,  b:17, c:18, col:0xffcccc};       triangles[36] = {a:0,  b:7,  c:11, col:0xffcccc};       triangles[37] = {a:7,  b:18, c:11, col:0xffcccc};       triangles[38] = {a:8,  b:9,  c:19, col:0xffcccc};       triangles[39] = {a:9,  b:20, c:19, col:0xffcccc};       triangles[40] = {a:9,  b:10, c:20, col:0xffcccc};       triangles[41] = {a:10, b:21, c:20, col:0xffcccc};       triangles[42] = {a:10, b:8,  c:21, col:0xffcccc};       triangles[43] = {a:8,  b:19, c:21, col:0xffcccc}; } function onEnterFrame():Void {       var numPoints:Number = points.length;       for (var i:Number=0;i<numPoints;i++) {             var point:MovieClip = points[i];             var angleY:Number = (_xmouse - vpX) * .001;             var cosY:Number = Math.cos(angleY);             var sinY:Number = Math.sin(angleY);             var angleX:Number = (_ymouse - vpY) * .001;             var cosX:Number = Math.cos(angleX);             var sinX:Number = Math.sin(angleX);             var x1:Number = point.x * cosY - point.z * sinY;             var z1:Number = point.z * cosY + point.x * sinY;             var y1:Number = point.y * cosX - z1 * sinX;             var z2:Number = z1 * cosX + point.y * sinX;             point.x = x1;             point.y = y1;             point.z = z2;             var scale:Number = fl / (fl + point.z + zOffset);             point.xPos = vpX + point.x * scale;             point.yPos = vpY + point.y * scale;       }       clear();       triangles.sort(triSort);       var numTriangles:Number = triangles.length;       for(var i:Number =0;i<numTriangles;i++)       {             renderTriangle(triangles[i]);       } } function renderTriangle(tri:Object):Void {       var pointA:Object = points[tri.a];       var pointB:Object = points[tri.b];       var pointC:Object = points[tri.c];       if(isBackFace(pointA, pointB, pointC))       {             return;       }  var col:Number = getTriangleColor(tri);   beginFill(col, 100);  moveTo(pointA.xPos, pointA.yPos);       lineTo(pointB.xPos, pointB.yPos);       lineTo(pointC.xPos, pointC.yPos);       lineTo(pointA.xPos, pointA.yPos);       endFill(); } function isBackFace(pointA:Object, pointB:Object, pointC:Object):Boolean {       // see http://www.jurjans.lv/flash/shape.html       var cax:Number = pointC.xPos - pointA.xPos;       var cay:Number = pointC.yPos - pointA.yPos;       var bcx:Number = pointB.xPos - pointC.xPos;       var bcy:Number = pointB.yPos - pointC.yPos;       return cax * bcy > cay * bcx; } function triSort(triA:Object, triB:Object):Number {       var zA:Number = Math.min(points[triA.a].z, points[triA.b].z);       zA = Math.min(zA, points[triA.c].z);       var zB:Number = Math.min(points[triB.a].z, points[triB.b].z);       zB = Math.min(zB, points[triB.c].z);       if(zA < zB)       {             return 1;       }       else       {             return -1;       } }  function getTriangleColor(tri:Object):Number   {   var pointA:Object = points[tri.a];   var pointB:Object = points[tri.b];   var pointC:Object = points[tri.c];   var lightFactor:Number = getLightFactor(pointA, pointB, pointC);   var red:Number = tri.col >> 16;   var green:Number = tri.col >> 8 & 0xff;   var blue:Number = tri.col & 0xff;   red *= lightFactor;   green *= lightFactor;   blue *= lightFactor;   return red << 16  green << 8  blue;   }   function getLightFactor(ptA:Object, ptB:Object, ptC:Object):Number   {   var ab:Object = new Object();   2      ab.x = ptA.x - ptB.x;   ab.y = ptA.y - ptB.y;   ab.z = ptA.z - ptB.z;   var bc:Object = new Object();   bc.x = ptB.x - ptC.x;   bc.y = ptB.y - ptC.y;   bc.z = ptB.z - ptC.z;   var norm:Object = new Object();   norm.x =   (ab.y * bc.z) - (ab.z * bc.y);   norm.y = -((ab.x * bc.z) - (ab.z * bc.x));   norm.z =   (ab.x * bc.y) - (ab.y * bc.x);   var dotProd:Number = norm.x * light.x +   norm.y * light.y +   norm.z * light.z;   var normMag:Number = Math.sqrt(norm.x * norm.x +   norm.y * norm.y +   norm.z * norm.z);   var lightMag:Number = Math.sqrt(light.x * light.x +   light.y * light.y +   light.z * light.z);   return (Math.acos(dotProd / (normMag * lightMag)) / Math.PI)   * light.brightness / 100;   }  

In addition to the new functions and other changes I mentioned, I also made all the triangles the same color, as you see in Figure 17-6, as I think it shows off the effect of the lighting much better.

image from book
Figure 17-6: 3D solid with backface culling, depth sorting, and 3D lighting


Foundation ActionScript. Animation. Making Things Move
Foundation Actionscript 3.0 Animation: Making Things Move!
ISBN: 1590597915
EAN: 2147483647
Year: 2005
Pages: 137
Authors: Keith Peters

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net