Site Map Home Page Process Engineer Chart Digitizer Topic Editor Video Timer Photo Music PLC Simulator Android Timer Feedback About GTS Alien Vision Stories: Alien Vision Home TV Antenna Gods Test Trump Expansion Gravity Superbowl Get / Post Sales Taxes Draw Lines Pluto |
Fixed-Point Technique for Drawing Fast Lines
Dateline, Colorado 1998
When I created my Pikes Peak Trailguide software 15 years ago, I needed to draw millions of lines on the screen fast, and they needed to be of assorted styles. There are several common techniques for drawing lines but I didn't like any of them, and I had a hunch that there was a simpler and faster way. It took two weeks to realize my vague hunch, but it was correct - the technique I came up with is far simpler and faster than any of the more popular methods. And it also lends its self very well to modified line styles such as double, dashed or hashed lines. I filed for a patent on this technique, but was turned down because Silicon Graphics had done something similar. But first, let's take a quick look at the general concept of drawing lines on a computer screen. All lines are drawn on a grid of dots using different variations of the point-slope formula to calculate which dots to light up between two end points. For example, a line between point (0,0) and (9,2) would draw a dot at coordinates: (0,0) (1,0) (2,0) (3,1) (4,1) (5,1) (6,1) (7,2) (8,2) (9,2) That is: over 3, down 1, over 4, down 1, over 3. Note that regardless of the direction of the line, the algorithm will always increment then step, increment, step ... The steps will be a single pixel step at a right angle to the increments. Also note that the number of increments can change on each step because of rounding off errors, and accumulating those errors is the real trick to any algorithm. Having said all of that, there are three common ways to draw a line. These examples are written in simplified pascal. They assume the line is moving to the right, and both end points will be plotted. 1. Use the Point-Slope formula on each pixel. This is the first thing that come's to mind when you're in a pinch, but it's horribly slow. The loop increments the X and calculates the Y with complicated floating point math: procedure DrawLine (X1,Y1,X2,Y2) for X := X1 to X2 do Y := X * (Y2-Y1) / (X2-X1); DrawPixel(X,Y); end; 2. Pre-calculate the slope and add it to each point. Any competent programmer would instinctivly pre-calc the loop, and most optimizing compiliers would do it even if the programmer didn't. This is probably ten times faster and it's pretty easy to implement, but it still has floating point math in the loop. procedure DrawLine (X1,Y1,X2,Y2) float S := (Y2-Y1) / (X2-X1); int Y := Y1; for X := X1 to X2 do DrawPixel(X,Y); Y := Round (Y + S); end; 3. Bresenham's Algorithm. This is quite a bit faster, and it's the technique used in most compiliers. But nobody writes this off the top of his head, and even some tutorials I've read didn't seem to know why this works. In reality, the concept is simple: Instead of dividing delta-Y by delta-X to get the slope, Bresenham just keeps adding delta-Y until it excedes delta-X then steps down a pixel each time it does. Notice that there is no floating point math. procedure DrawLine (X1,Y1, X2,Y2); deltaX := X2-X1; deltaY := Y2-Y1; Yacc := deltaX div 2; Y := Y1; for X := X1 to X2 do DrawPixel (X, Y); Yacc := Yacc + deltaY; if Yacc >= deltaX then Y := Y + 1; Yacc := Yacc - deltaX; end; end; end; Those techniques are simple enough, but I needed something faster, more versatile, and would work in RAM. So I took the precalc slope formula above and converted it from floating point numbers to fixed-point fractions, but I did it in a way that is vastly faster than the original.
The Fixed-Point Slope Method
In a computer, floating point numbers are stored as scientific notation in base 2. For example, the number 16 million in scientific notation would be: 1.6x10e7. But in a computer it would be: 1.0x2e24. In a fixed-point format, there is no exponent, only an integer portion and a fraction portion. In my fixed-point scheme, I use a 4-byte integer to store a real number. The most significant word is the 16 bit integer portion, the least significant word is the 16 bit fraction. Each count in the 16 bit fraction is 1/65536, so the value 32768 would be 1/2. Here's the key: if you count upwards through the fractional portion, it will automatically increment the integer portion when the fraction overflows. This allows me to use 16 bit integer math to control the integer or the fraction, and 32 bit integer math to control the whole fixed-point number. It works like magic. The key to a fixed-point number is Pascal's bizarre variant record: FourByte = record case integer of 0 : (A : int-32), { A = All 4 bytes } 1 : (F,I : int-16); { F = LSW, I = MSW } end;The syntax is actually straight forward: The variable Y.A is 32 bits = the whole number. The variable Y.I is 16 bits = the integer portion. The variable Y.F is 16 bits = the fraction portion. This magic will allow me to add the slope to the fraction portion, then plot the integer portion without any math in between. The function would be written in Pascal like this: 1 Procedure DrawLine (X1,Y1, X2,Y2 : int-16); 2 var Slope : int-16; 3 Xpos : int-16; 4 Ypos : FourByte; 5 begin 6 Ypos.F := 0; 7 Ypos.I := Y2-Y1; 8 Slope := Ypos.A div (X2-X1); 9 Ypos.F := 32768; 10 Ypos.I := Y1; 11 for Xpos := X1 to X2 do begin 12 Array [Xpos, Ypos.I]; 13 Ypos.A := Ypos.A + Slope; 14 end; 15 end;Setting up the loop is a little complicated, but note that within the loop there is only a single addition and plot the pixel. Bresenham's had four operations and plot the pixel. This looks too simple to work. Here's what's happening, a line at a time: Lines 6 & 7 load the 16 bit delta-Y into the Most Significant Word of YPos - effectivly multiplying by 65536. Line 8 divides the 32 bit Ypos by the 16 bit delta-X, the result will be a number between 0 and 65535. This is the Slope, assumed to be a fraction less than one. Line 9 starts the Y fraction at 1/2. This will cause the counter to round up as the Slope is added. Line 10 starts the Y integer at the starting pixel value. Line 11 runs the loop from X1 to X2. Line 12 uses the integer portion of the Y value to plot the pixel in a 2-dimensional array. Line 13 adds the 16 bit Slope to the whole Y value. If the result overflows the lower 16 bits, it will automatically increment the upper 16 bits and point to the next pixel. Line 14 repeats the loop. As you can see, the whole key to this technique is to add the 16 bit slope to the lower 16 bits of the Y value, thereby incrementing the upper 16 bits and pointing to the next pixel. That whole sentence takes only a single addition! You won't find a simpler or faster way to draw lines on the screen or in RAM. (That key was missed in the Silicon Graphics patent.) All of these examples will only work if the line is moving to the right and stepping down. To make a functional routine, you will have to use 4 IF's for lines moving right, left, up or down. I think the slope will work if the steps are moving up or down. Try it and see what you think. Notes: These examples are written in Pascal because I do most of my programming in Delphi, which is visual/object Pascal. Plus, I don't know how to do a variant in C++, Basic or Java without using pointers and typecasts. These examples plot both end points of the line, but most programming environments do not plot the final point. Nearly all programming languages include a line drawing function, but this technique is still handy in case you need to draw in RAM. I originally wrote this for MS-DOS graphics, which did not include a line function in the API. |