Amaan Akram

Shading theory and implementation inside XSI
Orignal author: Amaan Akram


List of translations:
Serbo-Croatian by Jovana Milutinovich
Portuguese by Artur Weber
Translations to other languages by third-parties is actively encouraged

Lesson 1: Vectors

The process of rendering, or generating an image in a 3D graphics program, involves firing out rays from a 3D camera and then tracking what the rays hit, one ray at a time. Such rays are fired atleast once for every pixel that is to be rendered on an image.

So how are these rays represented internally in a 3D software? With Vectors. The reason for choosing vectors to represent such rays is fairly simple. Vectors provide a convenient way to represent direction and distance in a single form, or symbol. So, in firing a single ray from a 3D camera in a given direction, a software can determine if a ray has hit an object, and if it has, then what is the distance from the camera of the object that the ray has hit.

Some of you will remember vectors from math classes from school. Some of you will have happy memories of vectors, others will have nightmarish, and then there might be some who might not have had the chance to study them at all in school. So, for the purpose of bringing everyone at the same level, I am going to cover some basic vector theory in this lesson.

As stated earlier, Vectors represent two quantities with a single symbol. The graphical way of a vector is with an arrow. The tip of the arrow shows the direction, and its length represents the length of the vector in 3D space. This is represented in the figure below, with a vector OQ, where O represented the origin in 3D world space, and Q represents a point that space.



Note that vector OQ is not the same as the vector QO. The length may be the same, but the direction has been inverted.

Also note that two vectors are equal provided they have the same length and direction (parallel). It is not necessary for the two to originate from the same point in 3D space.

For more information on vectors, please visit, read and understand what's written on the following pages.

http://www.netcomuk.co.uk/~jenolive/vect1.html (Some general rules)

http://www.netcomuk.co.uk/~jenolive/vect2.html ( Adding vectors )
http://www.netcomuk.co.uk/~jenolive/vect4.html ( Using components to describe vectors)
http://www.netcomuk.co.uk/~jenolive/trig.html (Basic trig for working out components)
http://www.netcomuk.co.uk/~jenolive/vect5.html ( Finding the magnitude or length of a vector)

Once everyone's read the above, please let me know. We'll then move on to Lesson 2 then. Ofcourse if there is anything that is not clear about vectors after reading the information available on the website above, please ask.
Lesson 2: Vectors & their length

The length of a vector is calculated by using the famous Pythagoras Theorem, which states that the square of the length of the diagonal of a right angle triangle is the sum of the square of the triangle's base and it's height. This I think we have all pretty much studied in school. The only thing that we do different is that we are looking at vectors in 3-dimensional space. So a coordinate will have 3 components, x, y and z. In school, I don't think I went beyond two dimensional spaces.

The above is illustrated in the beautiful diagram below.



OK so we've just looked at finding the length of a vector, which starts at the origin (0,0,0) and ends at some point. But what about finding the length of a vector that is not starting at the origin? Well, that's simple.

Say the vector starts at point A with coordinates (x1, y1, z1), and ends at point B with coordinates (x2, y2, z2). The following picture shows how we handle such cases for calculating vector lengths. (for the purpose of simplicity, I have chosen an example of a vector confined to the XY plane)



as an exercise, can anyone tell me what calculations we would have to do if the vector in the above example was NOT confined to the XY plane, and actually had a Z component as well? Like say, point B was at (5, 4, 8 ) in 3D space?

You must be wondering why are we going through vectors. Well, you will find out when we all write our first Phong shaders! smileBut without understanding vectors, there's little point in moving on.

Lesson 3: Vector Dot Product

OK so far we have covered what vectors are, and how to calculate distances between 2 points in space. Now, we will look at dot products, and how useful they are for rendering in computer graphics.

The addition or subtraction of two vectors leads to another vector. Dot products, on the other hand, lead to just a number--a number which can be very important to shader writing, when used in the right way. In fact, this number holds the key to how shading is done at any given point on the surface of a 3D object. This number is a scalar quantity, which basically means that it's just a number, not a vector.
A dot product between two vectors A and B is denoted by:
A.B --- (read as A dot B)

So, how is the dot product calculated? It's calculated by multiplying two parallel vectors' magnitudes. Recall that we can calculate the magnitude of a vector A with coordinates X, Y, Z by:

Code:

SquareRoot ( X*X + Y*Y + Z*Z)
Also note that the symbol for representing the magnitude of A is |A|



So the dot product of two vectors A and B is given by:

A.B=|A| * |B|

But what happens when the vectors are not parallel? Well, some of you may recall from college vector classes that a vector is comprised of two components, a horizontal component, and a vertical component.

Have a look at the following image



The image shows two vectors, A and B. The vector B is not parallel to A, but as said earler, every vector has two components. In this case, B is at an angle denoted by theta (greek symbol) relative to the vector A (which is parallel to the X axis). So, the component of B parallel to A is denoted by BCos(theta).

So, as the image says, the dot product of two vectors A and B becomes:

A.B=|A| * |B| Cos (theta) ----we take the magnitudes of both vectors here because remember a Dot Product is a scalar quantity and we were multiplying the magnitudes of both vectors to define the dot product

Following is another example of a dot product.



To further your understanding of dot products, have a look at the interactive Java applet at http://www.falstad.com/dotproduct/ . This is a great example of how dot products work and the applet is a very intuitive way of furthering your understanding about dot products.

Lesson 4: Unit vectors & Dot Products

So, here we go.... today we are going to look at unit vectors and how they affect dot product calculations.

So what is a unit vector? It was stated in the first lesson that a unit vector is a vector that has a length of 1 unit. So how do we convert any vector into a unit vector? We take the three components --X, Y, Z -- of the vector and divide each of them with the length of the vector. Have a look at the example below

Vector A = ( X, Y, Z )
Length L = SquareRoot ( X*X + Y*Y + Z*Z)
So, Unit Vector A = ( X/L, Y/L, Z/L )
This method of converting any vector to a unit vector is called Normalization

This unit vector has a very significant impact on dot product calculations. Recall from the previous lesson that a dot product can be calculated from:

A.B=|A| * |B| Cos (theta) -- where theta is the angle between the 2 vectors A and B

But what happens when vectors A and B in the above equation are unit vectors (of length 1)? We know that |A| and |B | are both equal to 1. The dot product formula can be rewritten then as:

A.B=1 * 1 Cos (theta)
simplified : A.B = Cos (theta)

This is a very important result. What it tells us is that the dot product of two unit vectors is simply the cosine of the angle between them. This is the key to unlocking shading calculations in rendering! Now let's go and see how to get the value of the Cosine between two unit vectors A and B. We can now very easily calculate the angle between two vectors simply by calculating the dot product. Now lets reverse the above equation.

Cos (theta) = A.B

but what is A.B? to calculate Cos (theta), we need another way of expressing vectors A and B. This is where the another method of calculating dot products comes in. Have a look at the following.

Vector A = (X1, Y1, Z1)
Vector B = (X2, Y2, Z2)

A.B = (X1*X2) + (Y1*Y2) + (Z1*Z2) (this is just a straight multiplication of each of the components of the two vectors, and adding the result of each multiplication.

So, now we can easily use the above equation to calculate the angle between two vectors.

Code:


Vector A = (X1, Y1, Z1)
Vector B = (X2, Y2, Z2)

Given that vectors A and B have been Normalized:

Cos (theta) = (X1*X2) + (Y1*Y2) + (Z1*Z2)



When two unit vectors are parallel, A.B and cos(theta) will both be equal to 1. (since Cosine of 0 degrees is 1 --see graph of cosine below)
When two unit vectors are perpendicular A.B and cos(theta) will both be equal to 0. (since Cosine of 90 degrees is 0 --see graph of cosine below)

Following is a graphical representation of how the value of A . B (dot product) changes when both A and B are unit vectors.

Since A.B = Cos (theta), the variation in the value of A . B as the angle between them increases from 0 degrees onwards is simply the cosine curve

The Y-axis represents Cos (theta), which is the same as A . B -- the dot product. And the X-axis represents the variation in the angle between the two vectors A and B



this is a very good way of representing how the dot product varies between two unit vectors. This graph also shows that the rate of change of the dot product with respect to the angle between the vectors is a non-linear change

Once again, please visit http://www.falstad.com/dotproduct/ again and play with the Java applet. see how the dot product values change as u play with the magnitude of each of the vectors and the angles between them

OK, I think in the next lesson we will look at 3D graphics, and math (well, OK a bit of math)


Lesson 5: Vectors inside your 3D application, and the Lambert Shading model


OK so now that we all know what dot products are, we can start looking at 3D applications and how they use vectors to shade a polygon.

Each object in a 3D application consists of polygons. Every polygon has a vector that is perpendicular to the plane that contains the polygon. What is a plane? It's just 2D space. One example is the XY plane in your 3D application. Similarly, the XZ plane. Similarly, an ideal polygon is always confined to its own plane.

So, every polygon has a vector stored in it that is perpendicular to the plane of the polygon. This vector is called a Surface Normal, or for simplicity, just a Normal. Have a look at the following image



(This Surface Normal should not be confused with the process of Normalization. The two are completely different things)

This Normal is a critical element in calculating the shading on that polygon. The shading on this polygon will be the brightest when its normal is pointing at a light source. As the polygon turns away from the light source, the normal of the polygon will start pointing away from the light, and the shading on the polygon will decline in brightness. This is what shading is all about in a nutshell. Have a look at the following image.



Say we have a polygon to render. In particular, we have to render a pixel P on that polygon. We know that that pixel on the polygon will have a Normal, say N. The next thing we need is a light in the scene to be able to shade the pixel. So, now assume that there's a light in the scene as well, and there is a vector L that extends from the pixel P out to the light source. And finally, let's say the angle between N and L is theta. (see figure below)



(note the direction of the vectors L and N. They are both point outwards from the pixel P. If the direction of L was reversed, our calcuations would be wrong as the new angle theta would become 180-theta. Check school vector books for more info. For now just keep in mind that angle calculations are done at the tail ends of vectors L and N)

Since we are only interested in finding out the angle between N and L, we can use the dot product formula to calculate the angle. Recall that the dot product is given by

L . N = |L| * |N| * Cos (theta)

Since the dot product (and the angle theta) in the above formula is dependant on the magnitudes of the vectors L and N, we need to remove |L| and |N| from the above equation. The way to do that is to normalize L and N so that |L| and |N| both become 1.

So assuming we have normalized N and L, the normalized vectors are N' and L', respectively. Their components are:

L' = (L' x, L' y, L' z)
N' = (N' x, N' y, N' z)

So the shading at pixel P is defined by:

Cos (theta) = L' . N' -- where L' . N' = (L' x * N' x) + (L' y * N' y) + (L' z * N' z) -- this equation is the Lambertian Shading Model

The above equation is evaluated for every pixel that is on the surface of a polygon to be rendered. As the angle between L' and N' increases from zero to 90 degrees, the shading falls from 1 to 0 (same as in a cosine curve).

Lesson 6: More on vectors and other shader variables

In the last lesson we learned how the Lambert shading model gives us diffuse shading. This shading model is at the heart of many of todays shading algorithms, simply because it provides a fast and easy way to approximate surface diffuse shading.

In this lesson, we will take a look at some other variables and surface normals before we move onto some practical applications.

Earlier I mentioned that to render each pixel, the renderer fires atleast one ray (the camera vector) from the scene's camera. If that ray doesn't hit anything, then no shading calculations are done. However, if the ray does hit a polygon then the relevant shader assigned to that polygon is looked up and shading calculations are done for that point where the camera vector has intersected with the surface of the polygon. This point is often referred to as the Intersection Point. For each pixel that is to be rendered on a polygon, there is always an intersection point. On a very basic level, the data contained within the Intersection Point data structure is simply the X,Y, & Z coordinates of that point in 3D space. So the camera vector V for rendering the pixel with an intersection point P is VP.

Two types of Normals:
It is important to mention that there are two kinds of normals on a polygon. One is the Geometry Normal, and the other is the Interpolated Normal. The following figures show the difference between the two normals.

GEOMETRY NORMALS



A Geometry Normal is always perpendicular to the plane defined by each polygon. This normal is dotted with the light vector of a light in a scene to produce shading for the entire polygon. This shading calculation is done once, and the color is applied to the entire surface of the polygon. This kind of shading method, with the Geometry Normal, produces what is known is Flat Shading. A Flat Shaded object looks like:


So the single geometry normal's direction defines the color (shading) of each polygon.

INTERPOLATED NORMALS

Interpolated Normals are spread out evenly across the polygons of an object. However, their directions are smoothed out over the surface of an object to make the object appear smoother in shading calculations. Have a look at the following figure



The same object that was used to generate the flat shaded version using Geometry Normals is used here again, but the normals on the surface of the object have been smoothed out. In the lower section of the above figure, you can see how the normals are fanned out over the surface. This approximation is calculated using a Normal Interpolation algorithm for Phong shading that we will not get into during this session, as it is beyond the scope of what we are trying to cover here. But it is important to know that when an object is 'smoothed', the internal shading calculations switch from using Geometry Normals to Interpolated Normals.

So, at each intersection point at a pixel that is to be shaded, the Interpolated Normal is looked up and is dotted with a light vector to calculated diffuse shading.

On a side (but interesting) note, bump mapping works with interpolated normals too. All it does is that the Interpolated normals are further perturbed according to the gray-scale intensity of a pixel from a bump map image. Then these perturbed normals are dotted with the camera vector to calculate diffuse shading..


Lesson 7: A video to watch - practical implemention via low-level shading nodes in Mental Ray with XSI

The file is compressed with the WinRAR v3.51. If you are unfortunate enough to not have winrar, then you can download it umm... for free from http://www.rarlabs.com/download.htm . It's a small download, only 990kb. If you already have WinRar installed and it says that the Rar file on the website is corrput,.then you need to upgrade to the latest version of winrar.

The AVI for this lesson can be downloaded from:

http://cgexpanse.com/phpBB2/sessions/shaderWriting/Lesson7.rar

You will also require the (free) Techsmith codec to view avi. This is a small file -- just 170kb. It can be downloaded from:

http://www.techsmith.com/download/tsccdefault.asp

That's all that is required to view this lesson. I apologize for the poor quality of the audio. I kept the quality down to keep the filesize down...


Lesson 8: Surface Illumination

In this lesson, we are just going to quickly look at how the overall illumination on any polygon or surface is calculated. Let's start out by breaking down the different elements that contribute to the illumination on a surface.

1) Ambience
2) Diffuse
3) Specular
4) Reflection

Given the above elements, we can now write down the shading on any polygon as an equation:

Shading = Ambience + Diffuse + Specular + Reflection
note: for all dot product calculations in the above equation, clamp dot product values to above or equal to 0. Dot products can be negative when the angle between two vectors goes higher than 90 and less than 270 degrees Negative values of dot products make no sense for calculating illumination. If they are not clamped to be above 0, the final shading will look umm..strange.

Ambience is simply a user-specified color value. This can be implemented in XSI's render tree via a Color Share node, which can be plugged into the final Material node of an object.

Diffuse shading is simply added to the Ambience shading via a Color Math Basic node, using an Additive operator. Specular and Reflection shading is added in a similar manner.

Since Ambience is very easy to implement, and we have already implemented Diffuse shading, let's now move on to Specular shading. However, before we do that, I'd like to point out that my diffuse calculations are simplified on purpose. I did not look up the color, intensity and other properties of the light. In a more realistic scenario, all such properties may need to be looked up and then multiplied with the light vector.

Specular shading
There is no such thing in real life as specular shading. It is just something that has been invented by computer graphics scientists to help approximate reflection of a light source. All it does is that it creates an illusion of a reflecton lightsource on a surface.

So how do we calculate specular reflection? Well, there are two methods out their. One is to use a reflection vector, and the other is to use a what is called a Half Vector. Both methods are outlined in the figures below.



Figure A represents a situation with a light vector, a camera vector, and a surface normal. I have drawn the reflection vector, which is just a reflection of the light vector about the surface normal.

Figure B shows how I calculate the reflection vector R (there are other ways to do this calculation--I am presenting my own method below. Others may use another method, but the result is the same). I write down the point H, which lies half way between R and L, as an equation involving R and L. I used a method from trignometry that we have all used in school. To calculate the mid point of two points, we just add each component and divide each component by two. The forumla, expressed in terms of R and L is

H= ((Rx+Lx)/2) + ((Ry+Ly)/2) + ((Rz+Lz)/2) -- equation 1 (trignometric form)

This can also be written in vector form

H=(R+L) / 2 -- equation 1.1

As the R is a reflection of L about N, the midpoint H also lies on the normal vector. This comes in very handy to calculate H in another way. Whenever we do a dot product between two normalized vectors, the result is the length of the projection of one vector onto another. In this case, when we do N . L, we find out the length of the projection of L on N. This is very easy to visualize in another way. When L and N are parallel, the angle between them is 0, and the dot product of the two is 1. That means that the length of the projection of L on N is 1 times N (1*N). If the angle between the two is 45 degrees, the dot product gives us 0.707 as the result. This means that the length of the projection of L on N is 0.707 times N (0.707*N). If the angle was 90 degrees, the dot product would be 0 and the length of the projection of L on N would be 0 times N (0*N)--hence zero.

So point H can simply be written as

H=N * (N . L) -- equation 1.2

When we equate the two equations 1.1 and 1.2, we get:

R = 2N * ( N . L) - L

The specular component then is simply the dot product between R and the camera vector C. When the two line up, the camera sees the light's reflection perfectly. As L or C move, the specular strength changes accordingly. The final forumla for specular reflection via this method is:

C . (2N * ( N . L) - L)

Now let's take a look at the half vector method.

This is Jim Blinn's approximation of a specular reflection. The reason this method exists, and is more commonly used, is because it involves fewer computations that the reflection vector method for calculating.

The image below shows two different positions for the light vector L, but with the camera vector and the surface normal unchanged.



In Figure A, the light is positioned in such a way that R lines up perfectly with C. If we calculate the half vector H between L and C, we will find out that H lines up perfectly with the normal N.

In Figure B, I have moved L a bit up, and as a result R has moved away from C, and H has moved away from N. So, given this behaviour of H with N, and its relation with R, we can say that specular reflection can be calculated by just comparing the angle between H and N by using a dot product calculation.

That's it! We now know how to computer specular reflection. The last remaining topic in this discussion is that of just reflection. It's actually quite simple to calculate reflections. We have to calculate the camera vector reflected about the surface normal. This vector is our Reflection Vector of the camera, which we will call RC. (the Reflection Vector in the specular calculation section is the reflection vector of the light, not the camera). What we do with RC to calculate reflection is to evaluate the full shading equation by temporarily assuming that our camera vector is the same as RC. Hence, we substitute any references to the camera vector with RC in the following equation
Reflection = Ambience + Diffuse + Specular + Reflection
Hence, the full equation for calculating shading isShading =Ambience + Diffuse + Specular+ (Ambience + Diffuse + Specular + Reflection)
The bit in green is our reflection component

Normally, raytracers have a user-defined limit for how many times reflection is to be evaluated. In Mental Ray in XSI, this option is available under the Optimization tab of Render Options.




This pretty much wraps up this tutorial. Many thanks for your interest. If you have any questions, please do not hesitate to contact me via my website at www.warpedspace.org To see a working example of how to implement a phong shader (without reflection), please download the following XSI 4.2 scene
http://www.cgexpanse.com/phpBB2/download.php?id=4509
The shader lets you control the following elements
Color
Diffuse (This diffuse acts like it does in Max or Lightwave, NOT like in XSI)
Diffuse Spread (similar to Diffuse Sharpness in Lightwave)
Primary Specular Color
Primary Specular Intensity
Secondary Specular Color
Secondary Specular IntensityThe controls in the flow look like: The two speculars were just an experiment to get metallic shading into the render tree. Below is a sample image rendered with the 2 speculars

To see the full render tree, please see:
http://www.warpedspace.org/wips/My_Phong_Shader/render_tree.jpg (warning!: 2500 pixels wide!) I'd like to urge everyone to play around with the render-tree and low-level XSI nodes. This is a very artist-friendly way of experimenting low-level shading without stepping into the complex world of programming. As an example of what can be achieved with just playing around, have a look at my 'hack-SSS' shader below. It can give visually interesting results that may be useful, and it does it faster than the simplest of phong shaders :)


Click for a larger version -- model courtesy of Mario Ucci


*ACKNOWLEDGEMENT
This tutorial would have been impossible without the help of my friend Andy Nicholas. His lessons in shading theory and how to implement it inside xsi's render tree got me started on this path--for which I am grateful. Please visit his website -- www.andynicholas.com