Der Bresenham-Linienalgorithmus
Eine gerade Linie auf Papier ist trivial â auf einem Pixelraster nicht.
Pixel sitzen auf ganzzahligen Koordinaten, aber eine Linie von (0, 0)
nach (5, 3) hat eine Steigung von 0.6, und auĂer den Endpunkten liegt
kein Pixel exakt auf diesem Pfad. Irgendetwas muss fĂŒr jede Spalte
entscheiden, welche Zeile am nÀchsten liegt.
Der naive Ansatz wertet y = mx + b fĂŒr jedes x mit
Gleitkomma-Arithmetik aus. Das funktioniert, aber Gleitkomma-Multiplikation
und -Rundung sind langsamer, als es fĂŒr eine Operation nötig wĂ€re, die pro
Frame Millionen Mal laufen kann. Bresenhams Algorithmus, 1965 von Jack
Bresenham bei IBM veröffentlicht, liefert das identische Ergebnis nur mit
Ganzzahl-Addition, -Subtraktion und Bit-Shifts â keine Multiplikation,
keine Division, keine Rundung.
Die Idee
Betrachte eine Linie mit einer Steigung zwischen 0 und 1, von (x0, y0)
nach (x1, y1). Da die Steigung flach ist, rĂŒckt die Linie bei jedem
Schritt um genau ein Pixel in x vor, und y bleibt entweder gleich oder
erhöht sich um eins. Der Algorithmus muss den reellwertigen y-Wert nie
kennen â er muss bei jedem Schritt nur entscheiden, ob y erhöht wird
oder nicht.
Diese Entscheidung wird ĂŒber einen Fehlerterm getroffen: eine laufende
Summe, wie weit die ideale (reellwertige) Linie von der aktuellen
Pixelzeile abgedriftet ist. Jeder Schritt addiert die Steigung zum Fehler.
Ăbersteigt der Fehler ein halbes Pixel, wechselt der Algorithmus in die
nÀchste Zeile und zieht ein volles Pixel wieder ab. All das lÀsst sich mit
Ganzzahlen ausdrĂŒcken, indem man den Fehlerterm mit 2 * dx skaliert â
daher kommen die charakteristischen Konstanten 2*dy und 2*dy - 2*dx in
der Implementierung.
Implementierung
Die Version unten behandelt alle Steigungen und Richtungen, indem zuerst
der Oktant normalisiert wird: Sie vertauscht x/y bei steilen Linien
und die Endpunkte bei Linien, die von rechts nach links verlaufen, und
schreitet danach immer nach demselben einfachen Muster voran.
#include "plot.hpp"
#include <stdlib.h>
void bresenham_line(int x0, int y0, int x1, int y1) {
int steep = abs(y1 - y0) > abs(x1 - x0);
if (steep) {
int tmp = x0; x0 = y0; y0 = tmp;
tmp = x1; x1 = y1; y1 = tmp;
}
if (x0 > x1) {
int tmp = x0; x0 = x1; x1 = tmp;
tmp = y0; y0 = y1; y1 = tmp;
}
int dx = x1 - x0;
int dy = abs(y1 - y0);
int error = dx / 2;
int ystep = (y0 < y1) ? 1 : -1;
int y = y0;
for (int x = x0; x <= x1; x++) {
if (steep) {
PlotPixel(y, x);
} else {
PlotPixel(x, y);
}
error -= dy;
if (error < 0) {
y += ystep;
error += dx;
}
}
}
void frame() {
PlotBackground(250, 250, 250);
PlotColor(30, 100, 200);
bresenham_line(50, 400, 600, 80);
}
frame() ruft den Algorithmus einfach mit PlotPixel als Zeichenprimitive
auf, statt mit printf. Bearbeite den Code oben und drĂŒcke "Compile &
Run", um selbst andere Endpunkte auszuprobieren.
error ist hier der klassische Bresenham-Fehlerterm, um den Faktor zwei
kleiner skaliert als in manchen Lehrbuchversionen (dx/2 statt
2*dy - dx) â beide sind Ă€quivalent, diese Form vermeidet nur eine
Multiplikation zu Beginn.
Schritt fĂŒr Schritt durchgerechnet
FĂŒr die Linie von (0, 0) nach (5, 3): dx = 5, dy = 3, ystep = 1,
error startet bei 2.
| x | error davor | y | error danach (âdy) | Zeilenwechsel? |
|---|---|---|---|---|
| 0 | 2 | 0 | â1 | ja â y=1, error += dx â 4 |
| 1 | 4 | 1 | 1 | nein |
| 2 | 1 | 1 | â2 | ja â y=2, error += dx â 3 |
| 3 | 3 | 2 | 0 | nein |
| 4 | 0 | 2 | â3 | ja â y=3, error += dx â 2 |
| 5 | 2 | 3 | â1 | ja â y=4 (Schleife endet vor Verwendung) |
Die resultierenden Pixel sind (0,0) (1,1) (2,1) (3,2) (4,2) (5,3) â eine
vernĂŒnftige treppenförmige AnnĂ€herung an die ideale Linie, nie mehr als
ein halbes Pixel vom wahren Pfad entfernt.
Warum das immer noch wichtig ist
Moderne GPUs rasterisieren Dreiecke, nicht einzelne Linien, und verwenden ihre eigenen Festkomma-Rasterisierungsregeln. Aber Bresenhams Algorithmus â und seine Erweiterungen fĂŒr Kreise und allgemeine Kegelschnitte â taucht ĂŒberall dort auf, wo Linien ohne GPU gezeichnet werden mĂŒssen: Terminal- UIs, eingebettete Displays, Plotting-Bibliotheken, Retro-Spiele und jeder Software-Rasterizer, der zu Lernzwecken oder fĂŒr eingeschrĂ€nkte Hardware geschrieben wird. Es ist auĂerdem ein gutes Beispiel fĂŒr einen wiederkehrenden Trick in Low-Level-Grafikcode: eine reellwertige Berechnung durch eine Ă€quivalente Ganzzahl-Berechnung zu ersetzen, indem man den akkumulierten Fehler statt des Werts selbst verfolgt.
Fazit
Bresenhams Algorithmus verwandelt die Frage "welches Pixel liegt am nĂ€chsten an dieser Linie" in einen laufenden Ganzzahl-Vergleich, der pro Schritt nur eine Addition und gelegentlich eine Subtraktion aktualisiert. Es ist ein kleines StĂŒck Code, aber ein gutes Beispiel dafĂŒr, wie viel man gewinnen kann, wenn man vor dem Schreiben der Schleife die richtige Darstellung fĂŒr ein Problem wĂ€hlt.