Let's Build a 3D Graphics Engine: Colors
Welcome! This is the sixth part in our Let's Build a 3D Graphics Engine series covering the basics of 3D graphics systems. This time we are going to be talking about color and how to add it into our existing classes. We are also going to create a few useful functions to make handling lighting easier, which is what our next and final part will be about.
Recap
Adding color to our objects is not going to be too huge of an undertaking, so the only two class that we are going to focusing on heavily are our point class and our camera class. As a refresher, here is what they look like:
1 |
Point Class |
2 |
{
|
3 |
Variables:
|
4 |
num tuple[3]; //(x,y,z) |
5 |
Operators:
|
6 |
Point AddVectorToPoint(Vector); |
7 |
Point SubtractVectorFromPoint(Vector); |
8 |
Vector SubtractPointFromPoint(Point); |
9 |
Null SetPointToPoint(Point); |
10 |
Functions:
|
11 |
drawPoint; //draw a point at its position tuple |
12 |
}
|
13 |
|
14 |
Camera Class |
15 |
{
|
16 |
Vars:
|
17 |
int minX, maxX; |
18 |
int minY, maxY; |
19 |
int minZ, maxZ; |
20 |
array objectsInWorld; //an array of all existent objects |
21 |
Functions:
|
22 |
null drawScene(); //draws all needed objects to the screen |
23 |
}
|
So far, our theoretical engine has almost all of the basics in place, including:
-
Point
andVector
classes (the building blocks of our engine). - Transformation functions for our points.
- A
Camera
class (sets our viewport, and culls points outside of the screen). - Three classes for rasterizing (line segments, circles, and polygons).
Now let's add some color!
Color for Everyone!
Our engine is going to handle colors by storing their values within its Point
class. This allows each point to have its own individual color, making lighting and shading calculations much simpler (for people, at least - sometimes it is less efficient to code an engine this way). When figuring out a scene's lighting or shading, we can easily provide the function with a list of points, and then churn through each of them, using their distance from the light to alter their color accordingly.
One of the most common ways to store color in programming is to use red, green, and blue values to create the actual desired color (this is typically called additive color mixing). By storing a value of 0-255 in each of these color segments, you can easily create a wide variety of colors. (This is how most APIs determine color, so for compatibility reason it makes sense to use this method).
Then, depending on the graphics API that you are using, you can pass these values in either decimal form (255,0,0
), or in hexadecimal form (0xFF0000
or #FF0000
). We're going to use decimal format in our engine since its a bit easier to work with. Also, if your graphics API does use hexadecimal values, then it likely has a function for converting from decimal to hexadecimal, so this shouldn't be a problem.



To get our color implementation started we are going to add in three new variables to our Point class: red
, blue
, and green
. There is nothing too outlandish going on quite yet, but here is what our Point
class's new outline could look like:
1 |
Point Class |
2 |
{
|
3 |
Variables:
|
4 |
num tuple[3]; //(x,y,z) |
5 |
num red, green, blue; //could be abbreviated to r,g,b if desired |
6 |
Operators:
|
7 |
Point AddVectorToPoint(Vector); |
8 |
Point SubtractVectorFromPoint(Vector); |
9 |
Vector SubtractPointFromPoint(Point); |
10 |
Null SetPointToPoint(Point); |
11 |
Functions:
|
12 |
drawPoint; //draw a point at its position tuple |
13 |
}
|
That is all we need to store our point's color. Now we just need to adjust our camera's draw function so that it uses the specified color.
This is going to change drastically depending on which graphics API you are using, but they should all have a similar function to this:
1 |
object.setColor(red, green, blue) |
If your graphics API happens to use hexadecimal values for color instead of decimal, then your function would look similar to this:
1 |
object.setColor(toHex(red,green,blue)) |
That last bit uses a toHex()
function (again, the function names will differ from API to API) to convert an RGB value into a hexadecimal value so that you don't have to.
After having made these changes, you should now be able to have colored points within your scene. To take it a step further, we are going to adjust each of our rasterization classes so that our entire shape can be colored.
To add this to our classes, we simply have to add in color handling to their constructor functions. This might look like:
1 |
lineSegment::constructor(startX, startY, endX, endY, red, green, blue) |
2 |
{
|
3 |
this.startX = startX; |
4 |
this.startY = startY; |
5 |
this.endX = endX; |
6 |
this.endY = endY; |
7 |
this.red = red; |
8 |
this.green = green; |
9 |
this.blue = blue; |
10 |
}
|
Then, we just need to modify its return points function so that it sets each point in its array to have the specified color. The new function would look like this:
1 |
function returnPointsInSegment() |
2 |
{
|
3 |
//create a list to store all of the line segment's points
|
4 |
var pointArray = new Array(); |
5 |
//set this function's variables based on the class's starting and ending points
|
6 |
var x0 = this.startX; |
7 |
var y0 = this.startY; |
8 |
var x1 = this.endX; |
9 |
var y1 = this.endY; |
10 |
//define vector differences and other variables required for Bresenham's Algorithm
|
11 |
var dx = Math.abs(x1-x0); |
12 |
var dy = Math.abs(y1-y0); |
13 |
var sx = (x0 & x1) ? 1 : -1; //step x |
14 |
var sy = (y0 & y1) ? 1 : -1; //step y |
15 |
var err = dx-dy; //get the initial error value |
16 |
//set the first point in the array
|
17 |
pointArray.push(new Point(x0,y0,this.red,this.green,this.blue)); |
18 |
|
19 |
//Main processing loop
|
20 |
while(!((x0 == x1) && (y0 == y1))) |
21 |
{
|
22 |
var e2 = err * 2; //hold the error value |
23 |
//use the error value to determine if the point should be rounded up or down
|
24 |
if(e2 => -dy) |
25 |
{
|
26 |
err -= dy; |
27 |
x0 += sx; |
28 |
}
|
29 |
if(e2 < dx) |
30 |
{
|
31 |
err += dx; |
32 |
y0 += sy; |
33 |
}
|
34 |
//add the new point to the array
|
35 |
pointArray.push(new Point(x0, y0,this.red,this.green,this.blue)); |
36 |
}
|
37 |
return pointArray; |
38 |
}
|
Now, every point within the line segment should be the same color that was passed into the line segment. You can use this method to set colors up in your other rasterizing classes as well, and soon your scene will come alive with color!
Let's put our new features into action by making a program to show them off.
Playing With 16.7 Million Colors
Using additive color mixing, we can easily create over 16.7 million different colors using just the simple (r,g,b
) notation. We are going to create a program that takes advantage of this vast number of colors.
Using key presses, we are going to allow the user to control an object's red, green, and blue values individually, allowing them to make it into any color that they would like.
The specifications for our program are as follows:
- Draw an object to the screen.
- If the user presses A then lower the object's red value; if they press Q then raise it.
- If the user presses S then lower the object's green value; if they press W then raise it.
- If the user presses D then lower the object's blue value; if they press E then raise it.
- Redraw the object after its color has been updated.
- Make sure to cap the color values, preventing them from dropping below 0 or rising above 255.
With all of that in mind, let's take a look at what a basic outline of our program might look like:
1 |
main{ |
2 |
|
3 |
//setup for your favorite Graphics API here
|
4 |
//setup for keyboard input (may not be required) here
|
5 |
|
6 |
var camera = new Camera(); //create an instance of the camera class |
7 |
|
8 |
//set the camera's view space
|
9 |
camera.minX = 0; |
10 |
camera.maxX = screenWidth; |
11 |
camera.minY = 0; |
12 |
camera.maxY = screenHeight; |
13 |
camera.minZ = 0; |
14 |
camera.maxZ = 100; |
15 |
|
16 |
//store our colors so that they can be manipulated
|
17 |
var red, green, blue; |
18 |
|
19 |
//draw initial object and set it to a variable
|
20 |
|
21 |
while(key != esc) { |
22 |
if(key press = 'a') |
23 |
{
|
24 |
if(red > 0) |
25 |
{
|
26 |
red --; |
27 |
object.red = red; |
28 |
//redraw object
|
29 |
}
|
30 |
}
|
31 |
if(key press = 'q') |
32 |
{
|
33 |
if(red < 255) |
34 |
{
|
35 |
red ++; |
36 |
object.red = red; |
37 |
//redraw object
|
38 |
}
|
39 |
}
|
40 |
if(key press = 's') |
41 |
{
|
42 |
if(green > 0) |
43 |
{
|
44 |
green --; |
45 |
object.green = green; |
46 |
//redraw object
|
47 |
}
|
48 |
}
|
49 |
if(key press = 'w') |
50 |
{
|
51 |
if(green < 255) |
52 |
{
|
53 |
green ++; |
54 |
object.green = green; |
55 |
//redraw object
|
56 |
}
|
57 |
}
|
58 |
if(key press = 'd') |
59 |
{
|
60 |
if(blue > 0) |
61 |
{
|
62 |
blue --; |
63 |
object.blue = blue; |
64 |
//redraw object
|
65 |
}
|
66 |
}
|
67 |
if(key press = 'e') |
68 |
{
|
69 |
if(blue < 255) |
70 |
{
|
71 |
blue ++; |
72 |
object.blue = blue; |
73 |
//redraw object
|
74 |
}
|
75 |
}
|
76 |
}
|
77 |
}
|
Now we can play around with our object and make it into any color that you desire!
Check out the demo here - repeatedly press the Q, W, E, A, S, and D keys to change the color of the square.
Conclusion
With color added into our engine, we've got everything that we need in place to finally handle some lighting. In the next article, we will be looking at creating lighting sources, and creating some functions to allow those sources to affect our points' colors. The depth that lighting adds to an engine is extremely satisfying, so make sure that you check it out!