Graphics 101: The range [0, 1]
Computer Graphics uses numbers in the range [0, 1]... a lot. The red, green, and blue color values in a pixel are always described as having values in the range [0, 1]. The alpha value of a pixel is also always in the range [0, 1]. The depth value in the Z buffer is also always in the range [0, 1]. The values in a normalized direction vector are also always in the range [0, 1]. This range shows up a lot. It also shows up in the values of coordinates before they are translated and scaled for display on the screen.
This mystical range has some interesting properties. It is easy to make the claim (backed up by too many math books) that there are as many real numbers in [0, 1] as there are in (-∞, ∞). And, you can map any other range to [0, 1] with a simple bit a math.
Any number X, in the range [min, max] can be mapped to another unique number Y in the range [0, 1] by:
Y = (X – min) / (max – min)
For example let min = 5 and max = 10 and X = 7 then
Y = (7 – 5) / (10 – 5)
Y = (2) / (5)
Y = 0.4
Just for grins and giggles using that equation to map the range (-∞, ∞) onto [0, 1]. I can almost convince myself it works :-)
It turns out that a it is really easy to do math on numbers in the range [0, 1] using a very few bits and just integer arithmetic. That is not such a big deal now days, but look back 40 years, or even 10 years (from 2009) and you'll see that memory and hardware were very much more expensive then. To give you an example, back around 1989 I was working a workstation with an E&S graphics subsystem that cost roughly $100,000. Last week I saw a much better graphics system in a bin at Goodwill for selling $14.95. Oddly enough a lot of the weird nasty details of computer graphics on modern systems are the result of trying to save thousands of dollars on hardware decades ago.... Hardware that now days costs only a few cents.
Any way, the whole point of this gripe is that so many people get the next step wrong. I've seen it done wrong in articles, in text books, and all over the place on the net.
The next step is figuring out how to represent the range [0, 1] in binary. This is a particular sore spot for me because figuring it out cost me a lot of time. I never was able to find a reference (at that time, around 1990) that got it right. I had to figure it out from looking at circuit diagrams. Maybe it is just so obvious that it doesn't need to be pointed out. But, still...
OK, the range [0, 1] is interesting in that it contains an infinite number of fractions and exactly 2 whole number 0, and 1. It is also always non-negative. Every I reference I had, if it mentioned this problem at all, said that you should use binary fractions. Which is not quite right because a binary fraction can can not represent 1.
Consider a 1 bit binary fraction, it can have exactly 2 values .0 (equal to 0) and .1 (equal to .5 in decimal), no other values are possible. What happens as we add bits to the fraction?
|
Number of Bits |
Range of Values |
Max Value |
|
1 |
.0 - .1 |
0.5000 |
|
2 |
.00 - .11 |
0.7500 |
|
3 |
.000 - .111 |
0.8750 |
|
4 |
.0000 - .1111 |
0.9375 |
You see what is happening? The maximum value you can represent with a fraction, binary or otherwise, is always less than 1. OK, OK, yes if you have an infinitely long fraction, in the limit, you get to 1. But we do not have an infinitely long fraction. Now days we generally have 8, 16, or 24 bits to work with. If you treat all those values as binary fractions you will introduce a lot of error with every bit of arithmetic you do. No matter what you do, error accumulates. If you start off with poor approximations of your initial values and use a representation that is also a poor approximation of the results, well error accumulates very quickly and you get numeric instabilities and pictures that look like shit.
If you really want to get to 1 using a fixed number of bits and binary fractions you have to dedicate a bit to represent 1. All the numbers in the above table have the binary point to the left of all the available bits. That is why I wrote them as .0 instead of 0.0. If I want a true 1 I need to put the binary point 1 bit over from the left so that, for example, a 4 bit number would look like 0.000. That number would leave only 3 bits of fraction.
If we dedicate a bit for 1 then we are wasting 25% of our bits to handle the special case where the number is equal to 1 and using only 3 bits to approximate an infinite number of fractional values. This is a huge waste.
OTOH, think about this, the maximum intensity of red that you can get out of your computer graphics hardware is generated when the red part of the pixel has the largest value that you can store in it. Go, look up how a DAC (Digital to Analog Converter) works. A DAC converts a binary number to a voltage. In this example that voltage controls the intensity of red for a pixel on your screen. The DAC generates its smallest output value when given 0 and its maximum output when given a binary number that is all 1s.
If you had a 3 bit DAC, then 000 yields no output, and 111 yields the maximum output. If you remember that the possible color range is [0, 1] you might notice that 1, the maximum color, value is equal 111 which is the maximum value you can hand to the DAC. But, .111 is not equal to 1. Huh?
Look at the following table. In that table I map the binary value in the range 000 – 111 to the range [0, 1]. I simply define 111 to be equal to 1 and then I distribute the rest of the possible binary values to fractions that equally divide the range.
|
Binary Value |
Decimal Value |
Fractional Value |
|
000 |
0.0000 |
0 |
|
001 |
0.1428 |
1/7 |
|
010 |
0.2857 |
2/7 |
|
011 |
0.4285 |
3/7 |
|
100 |
0.5714 |
4/7 |
|
101 |
0.7142 |
5/7 |
|
110 |
0.8571 |
6/7 |
|
111 |
1.0000 |
1 |
First off, remember that a sequence of binary digits has no meaning until we assign it a meaning.
I find that a lot of people have a hard time with that idea. I sometime find myself facing students who refuse to even thing about the possibility that a symbol only has a meaning because we assign it a meaning. In that case I will erase the board and draw a single straight line and ask the class what I have drawn. They usually run through all the meanings that society has assigned to a straight line, starting with “1” and “l” and then getting more creative. I always answer “no”. Someone will finally ask what I have drawn and I tell them that it is “dried ink on a white board”. Most students get it. More than one student has reported me to my manager complaining that I am clearly insane. Remember what Humpty Dumpty said, “When I Use a Word, It Means Precisely What I Want It To Mean – neither more nor less”.
Lets do the math for one 3 bit value and see what it maps to in [0, 1].
Min = 000 and Max = 111. If X = 101 then
Y = (101 – 000) / (111 – 000)
Converting to decimal
Y = (5 – 0) / (7 - 0)
Y = 5/7
Y = 0.7142
What I have done is to stop thinking of the strings of binary numbers as binary numbers or binary fractions, and start thinking of them as representing a specific fraction that is defined by the map of the binary values onto the range [0, 1].
BTW, you can still use integer arithmetic to operate on these numbers and you get the right answers so long as you remember how to look at the result. Makes life a lot easier.
I wrote a complete 3D pipeline using fixed point and all integer arithmetic for 386/486 processor before I figured this out. It worked a lot better after I rewrote parts of it to work this way.
NOTE:
Numeric intervals are traditionally written using a pair of number inside parentheses and/or square brackets. A range can start with either a parenthesis or a square bracket and end with either a parenthesis or a square bracket. A range that looks like [0, 1) is perfectly valid is are [0, 1], (0, 1], and (0, 0). If there is the a parenthesis next to a number that means the number is out side of the range. If there is a square bracket next to a number the number is part of the range. So, the range (0, 1) does not include either 0 or 1, but does include all the fractional values between the two. OTOH, the range [0, 1] does include both 0 and 1 as well as all possible values between the two.