Three Dimensional Shading
value plane shading light dot distance word product falloff
Explains the dot product and its use in lighting equations.
Hopefully you have read the companion document 3D Rotations, as this one will build upon the concepts presented in my attempt to teach some of the math need to make 3D graphics a reality. This file will cover such important topics as the Dot Product and how routines are best constructed for realtime 3D rotations and planar shading.
Our Friend, The Dot Product
The Dot Product is a neat relation that will allow you to quickly find the angle between any two vectors. It's easiest to explain graphically, so I will exercise my extendedASCII keys.
Two Vectors A & B:
A (Xa, Ya, Za) A = sqrt( (Xa)^2 + (Ya)^2 + (Za)^2 )
B (Xb, Yb, Zb) B = sqrt( (Xb)^2 + (Yb)^2 + (Zb)^2 )
Where Xa, and the others coorispond to some value on their respective Axis's
A
/
/
/
/
\ angle < Angle Theta between vector A and B
\
\
\
B
Cos(angle) = Xa * Xb + Ya * Yb + Za * Zb

A*B
Example:
A (1,2,3) A = sqrt( 1^2 + 2^2 + 3^2) = sqrt(14) = 3.7417
B (4,5,6) b = sqrt( 4^2 + 5^2 + 6^2) = sqrt(77) = 8.7750
Cos(angle) = 1 * 4 + 2 * 5 + 3 * 6 = 4 + 10 + 18 = 32 = 0.9746
  
(3.7417)*(8.7750) 32.8334 32.8334
ArcCos (.9746) = 12.9ø
So, your wondering how this revolutionizes you code, huh? Well, remember our other friend, the Normal vector? You use Normal vectors that define the directions of everything in our 3D world. Let's say that vector A was the Normal vector from my plane, and B is a vector that shows the direction that the light in my scene is pointing. If I do the Dot Product of them, you will get the angle between them, if that angle is >= 90^{o} and <= 270^{o} then no light falls on the visible surface and it doesn't need to be displayed.
Also notice, the way the values of the Cosine orient themselves
90 Cos 000 = 1
Cos 090 = 0
 Cos 180 = 1
Negative  Positive Cos 270 = 0


180  0 An angle between a light and a plane that
 is less than 90 or greater than 270 will
 be visible, so you can check if the Cos(theta)
Negative  Positive is greater than 0 to see if it is visible.


270
How Do You Implement The Code? Easy As Pie.
We will define our points like this:
Dist = sqrt( X^2 + Y^2 + Z^2 )
Precalculate these values and have them handy in your data area. Our planes should look something like this:
You'll notice that I defined the first Normal and then created space for the rest of the possible normals. I'll call this first normal, the Zero Normal. It will have special properties for planes that don't shade and are never hidden.
Well, before I start telling all the tricks to the writing code, let me make sure a couple of points are clear.
In 3D Rotations I said that you could set your view point on the ZAxis and then figure out if planes were visible by the postrotation Normal vectors, if their Z was greater than 0 then display, if not, don't.
That is an easy way to set up the data, and I didn't feel like going into the Dot Product at the time, so I generalized. So, what if you don't view your plane from the ZAxis, the answer is you use the...Dot Product! That's right. The angle will be used now to figure whether or not to display the plane.
I have been mentioning lights and view points as vectors that I can use with the Normal vector from my plane. To work correctly, these vectors for the lights and view should point in the direction that you are looking or the direction that the light is pointing, *NOT* a vector drawn from the origin to the viewer position or light position.
True Normal vectors only state a direction, and should therefore have a unit distance of 1. This will have the advantage of simplifying the math involved to figure you values. Also, for God's sake, precompute your normal, don't do this every time. Just rotate them when you do your points and that will update their direction.
If the Normal's have a length of 1 then A*B = 1 * 1 = 1. So:
Cos(angle) = Xa * Xb + Ya * Yb + Za * Zb

A*B
Is reduced to:
Cos(angle) = Xa * Xb + Ya * Yb + Za * Zb
We eliminated a multiply and a divide! Pat yourself on the back.
You ASM users might be wondering why I defined my Zero Normal as: < 0,0,0,10000h > How does 10000h equal a length of 1 ?
Well, this is a trick you can do in ASM, instead of using floating point values that will be slow on computers without math coprocessors, we can use a double word to hold our value. The high word holds the integer value, and the low word is our decimal. You do all of your computations with the whole register, but only pull the high word when you go to display the point. So, with that under consideration, 10000h = 1.00000. Not bad for integers.
How does the Zero Normal work? Since the X,Y,and Z are all 0, the Cos(angle) = 0, so if you always display when Cos(angle) = 0, then that plane will always be seen.
So, Beyond The Babble...
How To Set Up Your Code
Note also that the Cosine function is weighted toward the extremes. If you want a smooth palette change as the angles change, your palette should weight the gradient accordingly.
A useful little relation for depth sorting is to be able to find the center of a triangle.
E The center C = (D + E + F)/3
^
/ \ Divide each coordinate by (Xd + Xe + Xf)/3 = Xc
/ C \ and do the same for the Y's and Z's if you
/ \ choose to sort with this method. Then rotate
DF that point and use it to depth sort the planes
Phong and Gouraud Shading
Recently, someone asked me about the practicality of realtime phong and gouraud shading. The technique is common to raytracers and requires a great deal of calculation when working with individual rays cast from each pixel, but when only using this for each plane, it is possible. This type of shading involves taking into account the reduced luminosity of light as distance increases. For each light, you define a falloff value. This value should be the distance a which the light will be at full intensity. Then at 2*FallOff you will have 1/2 intensity, 3*FallOff will yield 1/3 and so on. To implement this type of shading, you will need to determine the distance from the light to the center of the plane. If distance < FallOff, then use the normal intensity. If it is greater, divide the FallOff value by the distance. This will give you a scalar value that you can multiple by the shading color that the plane should have. Use that offset and it will be darker since it is further away from the light source.
However, to determine the distance form the light to each plane, you must use a Square Root function, these are inherently slow unless you don't care about accuracy. Also, it would be difficult to notice the use of this technique unless you have a relatively small FallOff value and your objects move about in the low intensity boundaries.
Our Friend, The Dot Product
The Dot Product is a neat relation that will allow you to quickly find the angle between any two vectors. It's easiest to explain graphically, so I will exercise my extendedASCII keys.
Two Vectors A & B:
A (Xa, Ya, Za) A = sqrt( (Xa)^2 + (Ya)^2 + (Za)^2 )
B (Xb, Yb, Zb) B = sqrt( (Xb)^2 + (Yb)^2 + (Zb)^2 )
Where Xa, and the others coorispond to some value on their respective Axis's
A
/
/
/
/
\ angle < Angle Theta between vector A and B
\
\
\
B
Cos(angle) = Xa * Xb + Ya * Yb + Za * Zb

A*B
Example:
A (1,2,3) A = sqrt( 1^2 + 2^2 + 3^2) = sqrt(14) = 3.7417
B (4,5,6) b = sqrt( 4^2 + 5^2 + 6^2) = sqrt(77) = 8.7750
Cos(angle) = 1 * 4 + 2 * 5 + 3 * 6 = 4 + 10 + 18 = 32 = 0.9746
  
(3.7417)*(8.7750) 32.8334 32.8334
ArcCos (.9746) = 12.9ø
So, your wondering how this revolutionizes you code, huh? Well, remember our other friend, the Normal vector? You use Normal vectors that define the directions of everything in our 3D world. Let's say that vector A was the Normal vector from my plane, and B is a vector that shows the direction that the light in my scene is pointing. If I do the Dot Product of them, you will get the angle between them, if that angle is >= 90^{o} and <= 270^{o} then no light falls on the visible surface and it doesn't need to be displayed.
Also notice, the way the values of the Cosine orient themselves
90 Cos 000 = 1
Cos 090 = 0
 Cos 180 = 1
Negative  Positive Cos 270 = 0


180  0 An angle between a light and a plane that
 is less than 90 or greater than 270 will
 be visible, so you can check if the Cos(theta)
Negative  Positive is greater than 0 to see if it is visible.


270
How Do You Implement The Code? Easy As Pie.
We will define our points like this:
STRUC XYZs Xpos dd ? Ypos dd ? Zpos dd ? Dist dd ? ENDS XYZs ;size is 16 bytesThe X,Y,Zpos define a point in 3D space, Dist is the distance from the origin:
Dist = sqrt( X^2 + Y^2 + Z^2 )
Precalculate these values and have them handy in your data area. Our planes should look something like this:
STRUC PlaneSt NumPts db ? ;3 or 4 NormIndex dw ? PtsIndex dw ? dw ? dw ? dw ? ENDS PlaneStThe number of points that in the plane depends on the number your fill routines can handle you must have at least 3 and more than 6 is not suggested. Then we set up our data like this:
MaxPoints = 100 MaxPlanes = 100 PointList XYZs MaxPoints DUP() PlaneList PlaneSt MaxPlanes DUP() NormalList XYZs <0,0,0, 10000h> , MaxPlanes DUP()NonASM User Note:I set up points in a structure that had an X,Y,Z and Distance value. I set up a plane structure that had the number of points the index number of the normal vector for that plane and the index numbers for the points in the plane. The next lines set up arrays of these points in PointList, and the number of points was defined as MaxPoints. An array of planes was created as PlaneList with MaxPlanes as the total number of plane structures in the array. NormalList is an array of the vectors that are normal to the planes, one is set up initially (I'll explain that next) and then one for each possible plane is allocated.
You'll notice that I defined the first Normal and then created space for the rest of the possible normals. I'll call this first normal, the Zero Normal. It will have special properties for planes that don't shade and are never hidden.
Well, before I start telling all the tricks to the writing code, let me make sure a couple of points are clear.
In 3D Rotations I said that you could set your view point on the ZAxis and then figure out if planes were visible by the postrotation Normal vectors, if their Z was greater than 0 then display, if not, don't.
That is an easy way to set up the data, and I didn't feel like going into the Dot Product at the time, so I generalized. So, what if you don't view your plane from the ZAxis, the answer is you use the...Dot Product! That's right. The angle will be used now to figure whether or not to display the plane.
I have been mentioning lights and view points as vectors that I can use with the Normal vector from my plane. To work correctly, these vectors for the lights and view should point in the direction that you are looking or the direction that the light is pointing, *NOT* a vector drawn from the origin to the viewer position or light position.
True Normal vectors only state a direction, and should therefore have a unit distance of 1. This will have the advantage of simplifying the math involved to figure you values. Also, for God's sake, precompute your normal, don't do this every time. Just rotate them when you do your points and that will update their direction.
If the Normal's have a length of 1 then A*B = 1 * 1 = 1. So:
Cos(angle) = Xa * Xb + Ya * Yb + Za * Zb

A*B
Is reduced to:
Cos(angle) = Xa * Xb + Ya * Yb + Za * Zb
We eliminated a multiply and a divide! Pat yourself on the back.
You ASM users might be wondering why I defined my Zero Normal as: < 0,0,0,10000h > How does 10000h equal a length of 1 ?
Well, this is a trick you can do in ASM, instead of using floating point values that will be slow on computers without math coprocessors, we can use a double word to hold our value. The high word holds the integer value, and the low word is our decimal. You do all of your computations with the whole register, but only pull the high word when you go to display the point. So, with that under consideration, 10000h = 1.00000. Not bad for integers.
How does the Zero Normal work? Since the X,Y,and Z are all 0, the Cos(angle) = 0, so if you always display when Cos(angle) = 0, then that plane will always be seen.
So, Beyond The Babble...
How To Set Up Your Code
 Define Data Points, Normals, and Planes
 PreCalculate as many values as possible
 Rotate Points and Normals
 Determine Visible Planes With Dot Product (Save this value if you want to shade)
 Sort Visible Planes Back to Front (Determine Shade From Dot Product)
 Clip Plane to fit scene
 Draw to the screen
 Change Angles
 Goto Rotation
Note also that the Cosine function is weighted toward the extremes. If you want a smooth palette change as the angles change, your palette should weight the gradient accordingly.
A useful little relation for depth sorting is to be able to find the center of a triangle.
E The center C = (D + E + F)/3
^
/ \ Divide each coordinate by (Xd + Xe + Xf)/3 = Xc
/ C \ and do the same for the Y's and Z's if you
/ \ choose to sort with this method. Then rotate
DF that point and use it to depth sort the planes
Phong and Gouraud Shading
Recently, someone asked me about the practicality of realtime phong and gouraud shading. The technique is common to raytracers and requires a great deal of calculation when working with individual rays cast from each pixel, but when only using this for each plane, it is possible. This type of shading involves taking into account the reduced luminosity of light as distance increases. For each light, you define a falloff value. This value should be the distance a which the light will be at full intensity. Then at 2*FallOff you will have 1/2 intensity, 3*FallOff will yield 1/3 and so on. To implement this type of shading, you will need to determine the distance from the light to the center of the plane. If distance < FallOff, then use the normal intensity. If it is greater, divide the FallOff value by the distance. This will give you a scalar value that you can multiple by the shading color that the plane should have. Use that offset and it will be darker since it is further away from the light source.
However, to determine the distance form the light to each plane, you must use a Square Root function, these are inherently slow unless you don't care about accuracy. Also, it would be difficult to notice the use of this technique unless you have a relatively small FallOff value and your objects move about in the low intensity boundaries.
0 Comments
Note: GameDev.net moderates article comments.