(4) Linien
Vorlesung
„Computergraphik I“
S. Müller
Linien
Wiederholung I
Gerade
Parameterdarstellung
Lineare Interpolation
Ebene
Parameterdarstellung
Normalform
Rasterisierung
v t u s A
X = + ⋅ + ⋅
= 0 +
⋅ +
⋅ +
⋅ x b y c z d
a
v t A
X = + ⋅
( t ) A
B t
X = ⋅ + 1 − ⋅
a
n
Pixel
Wir haben Ausgabegeräte mit endlicher, fester Auflösung und damit ein festes Raster
Bildpunkte werden „Pixel“ genannt für „picture element“
In Wirklichkeit ist ein Pixel kein fester Punkt, sondern eine Fläche
Rasterisierung:
Wir nähern z.B. eine Linie durch
„Flächen“ an
Problem der Unterabtastung (englisch: „aliasing“) führt zu
gezackten Strukturen (speziell bei schrägen Linien)
Implementierung
Bildspeicher ist ein Array von Werten mit einer definierten Startadresse im Speicher
Den Kern der
Computergraphik bildet eine putpixel-Funktion z.B. mit den Werten:
x, y (Position)
r, g, b (Farbe)
Je nachdem, wie dieses
Speicherarray organisiert ist, lässt sich die Speicheradresse bestimmen und die Werte für r, g und b setzen
$Start
Beispiel: Bildschirm löschen
Schleife über alle
Bildschirmpixel und setzen der Hintergrundfarbe
Beispiel: put_pixel in OpenGL
#include <GL/glut.h>
#define BREITE 500
#define HOEHE 500
void put_pixel(GLint x, GLint y) {
glBegin(GL_POINTS);
glVertex2i(x, y);
glEnd();
}
void display(void) {
glClear (GL_COLOR_BUFFER_BIT);
glColor3f (1.0, 0.0, 0.0);
put_pixel(50, 50);
glFlush ();
}
void init (void) {
glClearColor (1.0, 1.0, 1.0, 0.0);
glOrtho(0, BREITE, 0, HOEHE, -1, 1);
}
int main(int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode (GLUT_SINGLE|GLUT_RGB);
glutInitWindowSize( BREITE, HOEHE);
glutInitWindowPosition (100, 100);
glutCreateWindow ("Points");
init ();
glutDisplayFunc(display);
glutMainLoop();
return 0;}
Beispiel: Bildschirm löschen
for (x = 0; x < BREITE; x ++)
for (y = 0; y < HOEHE; y++) {
put_pixel(x, y);
}
glFlush ();
pixel_clear.vcproj pixel_rechteck.vcproj
Beispiel Funktionen
void display(void) {
GLint x;
glClear (GL_COLOR_BUFFER_BIT);
for (x = 0; x < BREITE; x ++) {
glColor3f (0.0, 1.0, 0.0);
put_pixel(x, x);
}
glFlush ();
}
void display(void) {
GLint x;
glClear (GL_COLOR_BUFFER_BIT);
for (x = 0; x < BREITE; x ++) {
glColor3f (1.0, 0.0, 0.0);
put_pixel(x, sin(PI*2*x/BREITE)*
HOEHE/2+HOEHE/2);
}
glFlush ();
}
Linien I
void draw_line(GLint ax, GLint ay, GLint bx, GLint by)
{
GLint x, y;
float t;
for (t = 0.0; t < 1.0; t += 0.01) {
x = ax + t*(bx - ax);
y = ay + t*(by - ay);
put_pixel(x, y);
} }
Problem: wie ist Schrittweite entlang von t zu wählen?
Besser: x als Parameter pixelweise erhöhen
) ( B A t
A
X = + ⋅ −
) (
x xx
t b a
a
x = + ⋅ −
)
(
y yy
t b a
a
y = + ⋅ −
line_supernaiv.vcproj
Linien II
) ( B A t
A
X = + ⋅ −
A
B
x y
a
xb
xb
ya
y) (
x xx
t b a
a
x = + ⋅ −
)
(
y yy
t b a
a
y = + ⋅ −
x x
x
a b
a t x
−
= −
)
(
xx x
y y
y
x a
a b
a a b
y ⋅ −
− + −
=
x
x a
b x= −
∆
y
y a
b y = −
∆
α
= tan
∆
= ∆
−
= −
x y a
b
a m b
y yα
)
(
xy
m x a
a
y = + ⋅ −
x m a
m a
y =
y− ⋅
x+ ⋅ c
αx m c
y = + ⋅
Linie zeichnen
void draw_line(GLint ax, GLint ay, GLint bx, GLint by) {
GLint x;
float m = (float)(by - ay)/(bx - ax);
float c = ay - m*ax;
for (x = ax; x < bx; x ++) put_pixel(x, m*x+c);
}
line_naiv.vcproj
Bemerkung: in C liefert 1 / 2 den Wert 0, da es sich um eine Division von Integerwerten handelt. Dagegen liefern 1.0 / 2 den Wert 0.5; ebenso liefert (float) 1 / 2 den Wert 0.5.
Bresenham-Algorithmus I
Das Zeichnen einer Linie ist ein sehr essentieller Befehl in Graphiksystemen. Er muss also äußerst effizient
implementiert werden
Wie kann man den letzten Algorithmus beschleunigen?
Teuer ist die Float-
Berechnung m*x+c, sowie die float-Division zur
Berechnung von m
Trick 1: inkrementell arbeiten
:
x y
0= m ⋅ x + c
c x
m
y
1= ⋅ ( + 1 ) + m c
x m
y
1= ⋅ + + m y
y
1=
0+ :
+ 1
x
Implementierung
void draw_line(GLint ax, GLint ay, GLint bx, GLint by) {
GLint x;
float y = ay;
float m = (float)(by - ay)/(bx - ax);
for (x = ax; x < bx; x ++) {
put_pixel(x, y);
y = y + m;
}
} Float-Operationen immer noch teuer.
Bresenham-Algorithmus
Wir beschränken uns auf Linien im 1. Oktant
Alle anderen lassen sich durch Vertauschen der x- und y-Koordinaten von A und B, bzw. durch negieren
darauf zurückführen.
Für Linien im ersten Oktant gelten:
Erhöht man x um Eins, dann kann sich y auch maximal um Eins erhöhen
1
Beispiele
Für jedes x wird also nur ein Pixel gezeichnet
„Gehe entlang der Linie von links nach rechts und gehe dabei ev. manchmal ein Pixel höher…“
Pseudocode
float y = ay;
for (x = ax; x < bx; x ++) {
put_pixel(x, y);
if („Bedingung“) y = y + 1;
}
Wie sieht „Bedingung“ aus?
Der Kandidat für das nächste Pixel ist
oder
Mit dem Mittelpunkt
Die Idee ist einfach: dieser Punkt wird (inkrementell) in die Funktion eingesetzt. Liegt der Punkt über der Geraden, wird das untere Pixel
gewählt, sonst das obere
) , 1 ( x + y
) 1 ,
1 ( x + y +
) 5 . 0 ,
1
( x + y +
„Midpoint-Algorithmus“
Dies ist nicht der originale
Bresenham-Algorithmus,
zeichnet aber die gleiche
Linie und ist etwas intuitiver
Implizite Darstellung
Die ursprüngliche Gleichung
lässt sich umformen in
Diese Darstellung f(x,y)=0 nennt sich „implizite Form“
)
(
xx x
y y
y
x a
a b
a a b
y ⋅ −
− + −
=
) (
) (
) (
)
( b
xa
xa
yb
xa
xb
ya
yx a
xy ⋅ − = ⋅ − + − ⋅ −
0 )
( )
( − + ⋅ − + ⋅ − ⋅ =
⋅ a
yb
yy b
xa
xa
xb
ya
yb
xx
Midpoint-Algorithmus
Für einen Punkt (x, y) gilt in diesem Fall:
f(x,y) = 0: er liegt auf der Geraden
f(x,y) > 0: er liegt oberhalb der Geraden
f(x,y) < 0: er liegt unterhalb der Geraden
Pseudocode für „Bedingung“
if (f(x+1, y+0.5) < 0) y = y + 1;
Inkrementelles Vorgehen
Einsetzen der zwei möglichen Pixel liefert:
x y y
x x
x y
y
b y b a a b a b
a x y
x
f ( , ) = ⋅ ( − ) + ⋅ ( − ) + ⋅ − ⋅
) (
) , ( )
, 1
( x y f x y a
yb
yf + = + −
) (
) (
) , ( )
1 ,
1
( x y f x y a
yb
yb
xa
xf + + = + − + −
Pseudocode
…
d = f(ax + 1, ay + 0.5);
for (x = ax; x <= bx; x ++) {
put_pixel(x, y);
if (d < 0) {
y++;
d = d + (ay-by) + (bx-ax);
} else
d = d + (ay-by);
}
Schon besser. Inkrementell und fast an allen Stellen Integeroperationen. Das Problem ist d durch die Addition mit 0.5
Bestimmung des initialen d-Wertes
Was noch offen ist: wie bestimmt sich das initiale d?
d = f(ax + 1, ay + 0.5);
Antwort: Einsetzen in die implizite Geradengleichung liefert:
x y y
x x
x y
y y
x
y x
b a
b a a
b a
b a
a
a a
f
⋅
−
⋅ +
−
⋅ +
+
−
⋅ +
=
= +
+
) (
) 5 . 0 (
) (
) 1 (
) 5 . 0 ,
1
(
Letzter Schritt
Wie werde ich die float-Multiplikation mit 0.5 los?
Antwort: gesucht ist nur das Vorzeichen, also ob der eingesetzte Punkt oberhalb der Linie (positiv) oder unterhalb der Linie (negativ) ist.
Statt setzt man
x y y
x x
x y
y y
x
y x
b a
b a a
b a
b a
a
a a
f
⋅
−
⋅ +
−
⋅ +
+
−
⋅ +
=
= +
+
) (
) 5 . 0 (
) (
) 1 (
) 5 . 0 ,
1 (
0 )
,
( x y =
f 2 ⋅ f ( x , y ) = 0
x y y
x x
x y
y y
x
y x
b a
b a a
b a
b a
a
a a
f
⋅
−
⋅ +
−
⋅ + +
−
⋅ +
⋅
=
= +
+
⋅
2 2
) (
) 1 2
( ) (
) 1 (
2
) 5 . 0 ,
1 (
2
Finaler Code
void draw_line(GLint ax, GLint ay, GLint bx, GLint by) {
GLint x = ax;
GLint y = ay;
int d = 2*(x+1)*(ay-by)+(2*y+1)*(bx-ax)+2*ax*by-2*ay*bx;
for (x = ax; x <= bx; x ++) {
put_pixel(x, y);
if (d < 0) {
y++;
d = d+2*(ay-by)+2*(bx-ax);
} else
d = d+2*(ay-by);
} }
Nur Integeroperationen !
line_midpoint.vcproj
C++ kompakt - Teil 3
(Dank an Markus Geimer, Matthias Biedermann)
Wiederholung
Vom Compiler automatisch erzeugte Methoden:
Default-Konstruktor
Kopier-Konstruktor
Destruktor
Zuweisungsoperator
Referenz
Alternativer Name für ein Objekt
Muss zwingend initialisiert werden
Notation: Datentyp&
Anwendung:
Call-by-reference
Überladen von Funktionen
Funktionen, die sich nur in Anzahl und Typen der
Parameter unterscheiden
Auswahl der “richtigen”
Variante anhand der realen Parameter beim Aufruf
Überladen von Operatoren
Definition der Standard- Operatoren für eigene Datentypen
Nicht möglich:
• Neue Operatoren definieren
• Funktionsweise für elem.
Typen ändern
Objektorientierte Programmierung
C++ basiert auf C, ermöglicht aber zusätzlich OO-Programierung
alle Konzepte wie Klassen, Kapselung, Vererbung usw.
jedoch optional (im Gegensatz zu Java!)
Syntax (Überblick):
Weiterführende Konzepte:
Mehrfachvererbung, private Vererbung
class Object {
public:
...
protected:
string m_name;
};
class Drawable : public Object {
public:
...
private:
Vector2D m_position;
};
Felder (Arrays)
Die Deklaration
int a[10];
definiert ein Feld a mit 10 Elementen vom Typ int . Auf die
einzelnen Elemente kann mit a[0] bis a[9] zugegriffen werden.
Auf diese Art lassen sich nur Arrays definieren, deren Größe zur Compilezeit bekannt ist (variable Größe später).
Mehrdimensionale Felder definiert man analog:
int a[10][20];
a[5][2] = 19;
Es gibt zur Laufzeit keine Möglichkeit, die Größe eines Arrays
festzustellen…
Exkurs: STL
Die Standard Template Library enthält viele effiziente Container, Algorithmen u.v.m., die für eigene Zwecke verwendet werden können
Beispiel: einfachere Felder
#include <vector>
…
vector<float> a; // default-Größe (meist 0) vector<float> b(10); // 10 Elemente
Verwendung wie bei herkömmlichen Arrays, zusätzlich z.B.:
Hinzufügen weiterer Elemente:
a.push_back(2.87f); // hinten
Abfragen der Größe:
Exkurs: STL (2)
Eigene Datentypen können ebenso verwendet werden, z.B.:
#include “Vector2D.h”
#include <vector>
#include <iostream>
using namespace std;
…vector<Vector2D> points;
points.push_back(Vector2D(1, 5)); // anonyme Instanz points.push_back(Vector2D(-3, 0));
cout << points.size() << “ Punkte eingefügt.” << endl;
Weitere nützliche STL-Komponenten
Container wie map, list, stack
Algorithmen wie find(), sort(), min()
Zeichenketten string
Zeiger
Für einen beliebigen Datentyp type bezeichnet type* den Typ
“Zeiger auf type ”, d.h. eine Variable vom Typ type* kann die Adresse eines “Objektes” vom Typ type aufnehmen.
Wichtige Operatoren:
Adressoperator &
Dereferenzierungsoperator * (Inhaltsoperator)
Zeiger Beispiel
Beispiel:
int x = 1;
int y = 2;
int* ip; // “Zeiger auf int”
ip = &x; // ip enthält Adresse von x y = *ip; // y ist jetzt 1
*ip = 0; // x ist jetzt 0
ip = &y; // ip zeigt jetzt auf y
4711 1 (x)
4712 2 (y)
4713 - (*ip)
4711 1 (x)
4712 2 (y)
4713 4711 (*ip)
4711 1 (x)
4712 1 (y)
4713 4711 (*ip)
4711 0 (x)
4712 1 (y)
4713 4711 (*ip)
4711 0 (x)
4712 1 (y)
4713 4712 (*ip)
Zeiger II
Zeigern kann der Wert 0 zugewiesen werden (Null-Pointer). So ist prüfbar, ob ein Zeiger belegt ist oder nicht:
int x = 1;
int y = 2;
int* ip = 0;
if (ip) y = *ip; // Keine Zuweisung
ip = &x; // ip enthält Adresse von x if (ip) y = *ip; // y = 1
4711 1 (x)
4712 2 (y)
4713 0 (*ip)
4711 1 (x)
4712 2 (y)
4713 4711 (*ip)
4711 1 (x)
4712 1 (y)
4713 4711 (*ip)
Referenz vs. Zeiger
Referenz:
int a = 2;
doMagic( a);
void doMagic( int &x) { x = 5;
}
Referenz ist quasi ein konstanter Zeiger, der nicht verändert werden kann.
Zeiger
int a = 2;
doMagic( &a);
void doMagic( int *x) { *x = 5;
}
Dynamische Speicherverwaltung
Mit Hilfe von Zeigern kann man auch dynamische (herkömmliche) Arrays erzeugen (n sei eine Variable, welche die Größe enthält):
int* a = new int[n];
Es gibt in C++ keinen Garbage Collector, d.h. man muss sich selbst um die Freigabe des Speichers kümmern!
delete[] a;
Es gibt auch keinen Referenzzähler:
int* a = new int[10];
int* b = &a[0];
delete[] a;
int x = b[5]; // Fehler!!!
delete[] b; // Fehler!!!
Dynamische Speicherverwaltung (2)
Um ein Array von Objekten anzulegen, muss die entsprechende Klasse einen Default-Konstruktor (d.h. einen Konstruktor ohne Parameter)
bereitstellen.
Man kann auch einzelne Objekte anlegen und freigeben:
int* a = new int;
delete a;
Die Operatoren delete und delete[] sind auch für Null-Pointer definiert.
In diesem Fall wird der Aufruf ignoriert.
Wichtig:
new / delete und new[] / delete[] sind unterschiedliche Operatoren und dürfen nicht vermischt werden!
Dynamische Speicherverwaltung (3)
Anlegen eines zweidimensionalen Feldes (x * y):
int** feld;
feld = new (int*)[x];
for (int i = 0; i < x; ++i) feld[i] = new int[y];
Freigeben:
for (int i = 0; i < x; ++i) delete[] feld[i];
delete[] feld;
Dynamische Speicherverwaltung (4)
Beispiel mit STL-vector (dynamisch angelegt):
vector<int>* feld1D = new vector<int>(n); // 1D-Feld vector<vector<int> >* feld2D =
new vector<vector<int> >(x); // 2D-Feld for (int i = 0; i < x; ++i)
feld2D[i] = new vector<int>(y);
Freigeben:
for (int i = 0; i < x; ++i) delete[] feld[i];
delete[] feld;
Bei fast allen Compilern muss Leerzeichen dazwischen sein!
Wo ist der Fehler?
int* create() {
int feld[25];
for (int i = 0; i < 25; ++i) feld[i] = (2 * i) % 7;
return &feld[0];
}
An dieser Stelle wird die Adresse einer lokalen Variablen zurückgeliefert, die nach dem Verlassen der Funktion nicht mehr existiert!