Feb
2
GM6 D3D from the Ground Up - Chapter 5
2007 @ 10:13 AMChapter 5 - Basic Color and Lighting
Up until now, all of our work has appeared rather bland and not very practical for use in a good 3D game. All of this is about to change once we introduce color and lighting.
What is Color?
It's best to have an understanding of color as it appears in nature. Therefore we can have a better understanding of how to represent it in programming.
A Wave
Color is a wavelength of light that is visible to the human eye. Light whose wavelength is beyond the ends of the spectrum are not visible to the human eye. These include ultraviolet and infrared light, both of which you should be familiar with.
Something you may remember from grade school is that black is the abscense of color while white is the combination of all colors. A white object reflects all wavelengths of light evenly while a black object absorbs them. Therefore, when you perceive the color of an object, it depends upon how many wavelengths of light are absorbed and reflected. A shift in either makes you see a different color.
Color in D3D
Game Maker has a pretty nice list of color constants built in. These include colors like
c_dkgray,
c_teal,
c_silver, etc. However, we can also build our own color.
make_color_rgb(red,green,blue) will create a color with the red, green, and blue amounts specified. In computer graphics, RGB is widely used. Each amount of color is between 0 and 255, for 256 shades of each.
Before you draw a primitive, you can set the color of it using
draw_set_color(color). This affects every primitive drawn until you set the color again. However, the best way to set color is with
d3d_vertex_color(x,y,z,color,alpha) This is just an extension of our familiar
d3d_vertex function.
alpha is transparency. It is a value between 0-255. 0 is completely transparent while 255 is completely opaque.
d3d_primitive_begin(pr_trianglelist);
d3d_vertex_color(x,y,50,c_red,255);
d3d_vertex_color(x+50,y+50,5,c_teal,128);
d3d_vertex_color(x+100,y-50,-100,make_color_rgb(12,197,75),0);
d3d_primitive_end();
You may recognize this code. It was our old triangle example. But now we've extended it to include color. The first vertex's color is set to
c_red. It has an alpha value of 255, so it has no transparencey at all. The next vertex has a color of
c_teal. It has an alpha value of 128, so it is half transparent and half opaque. The last vertex has a color of RGB(12,197,75). This is a dull green color. It has an alpha value of 0. So, it is completely transparent.
Smooth Shading
You may have noticed that the triangle is shaded! Each vertex affects the color of the triangle, as you can see from running the program. The triangle is shaded smoothly from one color to another as it nears the other vertices. However, this is not always desirable. You can disable smooth shading by calling
d3d_set_shading(false). You can reenable it by calling
d3d_set_shading(true). When you disable it, the color of the triangle will be that of the first vertex.
Adding Light to a Scene
Ah, finally, light. The first thing you need to when starting to use light in D3D is call
d3d_set_lighting(true). If you ever need to disable lighting, call
d3d_set_lighting(false). Every light in D3D as an
index. This index is unique to each light. So when you create a light, it is assigned an index that you pass to other functions to modify that individual light.
d3d_set_lighting(true);
d3d_light_define_direction(1,0.5,0.3,1,c_white);
d3d_light_enable(1,true);
[This should be in the Create event of an object.]
This code first enables lighting. Next, it defines a
directional light with a call to
d3d_light_define_direction(index,dx,dy,dz,color). A directional light is an ambient light. It's similar to the sun. The first argument of the function is 1. It's an arbitrary, small positive number that represents this light. For example, we could also do this to make it more readable:
lightSun = round(random(1024)+1);
d3d_light_define_direction(lightSun,0.5,0.3,1,c_white);
d3d_light_enable(lightSun,true);
The next three arguments specify the direction of the light. In our case, it's oriented halway towards the x-axis, 1/3 of the way towards the y-axis and pointed down the z-axis. The last argument is the color of the light.
Point Lights
The next kind of light is a point light. Point lights, used in conjunction with a gloabl directional light, can dramatically increase the realism of a scene. You can visualize a point light as a torch. It is single point where light radiates out from for a certain distance, losing intensity the further it goes.
You can define a point light with a call to
d3d_light_define_point(index,x,y,z,range,color).
Index is the light index, which you should be familiar with. x,y,z is a 3D coordinate specifying the position of the light in space. range is how far the light shines. color is, of course, the color of the light.
Surface Normals
A polygon cannot be shaded correctly without specifying a
surface normal. A surface normal is a 3D normal vector that is perpendicular to the polygon(plane).
Specifying a Normal
To actually give D3D a normal, we use the function
d3d_vertex_normal(x,y,z,nx,ny,nz). x,y,z is the coordinate of the vertex while nx,ny,nz is the vector representing the normal for this vertex. You may have noticed that there is no color for this. To specify a color, we use the super-extended
d3d_vertex_normal_color(x,y,z,nx,ny,nz,color,alpha).
Calculating the Surface Normal
x1 = argument0;
y1 = argument1;
z1 = argument2;
x2 = argument3;
y2 = argument4;
z2 = argument5;
x3 = argument6;
y3 = argument7;
z3 = argument8;
crossX = (y2-y1)*(z3-z1)-(y3-y1)*(z2-z1);
crossY = (z2-z1)*(x3-x1)-(z3-z1)*(x2-x1);
crossZ = (x2-x1)*(y3-y1)-(x3-x1)*(y2-y1);
You can find the normal of a polygon by forming the plane of a polygon from any three points on it. Since Game Maker only allows for triangles, this is easy. We then form the two vectors representing the plane and take their cross products. The resulting vector is perpendicular to the surface of our triangle. However, D3D prefers for all normals to be a
unit normal. This is a surface normal with a length of 1. We can expand our normal calculator to include the conversion to a normal vector:
x1 = argument0;
y1 = argument1;
z1 = argument2;
x2 = argument3;
y2 = argument4;
z2 = argument5;
x3 = argument6;
y3 = argument7;
z3 = argument8;
crossX = (y2-y1)*(z3-z1)-(y3-y1)*(z2-z1);
crossY = (z2-z1)*(x3-x1)-(z3-z1)*(x2-x1);
crossZ = (x2-x1)*(y3-y1)-(x3-x1)*(y2-y1);
length = sqrt(sqr(crossX) + sqr(crossY) + sqr(crossZ));
global.normX = crossX/length;
global.normY = crossY/length;
global.normZ = crossZ/length;
We first find the length of the normal by squaring each component, adding them, and taking the square root of the sum. We then divide each component by the length to get a new normal vector with a length of one.
Vertex Normals
As you can see, you can specify a normal for every vertex. But we only know what the normal of the plane is. Thus we need to do some
normal averaging. When you supply the true normal of a vertex, rather than the whole plane, D3D will smoothly shade it for us. Rather than giving the object a faceted look, it will look smooth and realistic.
To find the vertex normal, you have to average the normals of all triangles that share that vertex. Many model converters will do this job for you. It's too tedious to do by hand, so I won't discuss it here. It will, however, be shown in a later chapter.
<< Previous Chapter Next Chapter >>
This article hasn't been commented yet.