(3) Rasterisierung
Vorlesung
„Computergraphik I“
S. Müller
Rasterisierung
Wiederholung I
Trigonometrie
sin, cos, tan
Fläche im Dreieck
Lineare Algebra
Punkte und Vektoren
Betrag, Normierung
Skalarprodukt
• Ergebnis ist Skalar
• Winkel zw. 2 Vektoren
• Projektion auf Einheitsvektor
Kreuzprodukt
• Ergebnis ist Vektor
• Winkel zwischen 2 Vektoren
• Normale zu 2 Vektoren
Rechts-, Linkssystem
Beispiel I
= 0 0 1 x
= 0 1 0 y
= 1 0 0 z
x z
y
=
× y x
z
=
=
×
=
1 0 0 0
1 0 0
0 1
=
× x y
z
−
=
−
=
×
=
1 0 0 0
0 1 0
1 0
=
× x x
=
×
=
0 0 0 0
0 1 0
0 1
z ?
Beispiel II
Fläche in einem Dreieck
α 2 sin
1 ⋅ ⋅ ⋅
= a b
A α
a b C
A B
α
⋅ sin
⋅
=
×
= a b a b
c
c
b a A = ⋅ ×
2
1 c ?
3D-Koordinatensysteme
x
z y
x
y z
Linke-Hand-System (Linkssystem)
Rechte-Hand-System
(Rechtssystem)
Kreuzprodukt
b a c
×
=
a b
α b
a c
×
=
a b
α
Linke-Hand-System Rechte-Hand-System
Beispiel 1
Ein Polygon ist (oft) gegeben durch eine Liste seiner 3D-Eckpunkte, die gegen den Uhrzeigersinn
eingetragen werden, wenn man von oben/draußen darauf blickt
Berechne die normierte Normale auf das Polygon in einem Rechtssystem
A
E D
C
B
A B
AB
b
−
=
= a
A E
AE
a = = −
b n b a
×
= n
n
Beispiel 1: Probleme
Diese Normalenberechnung ist nur korrekt wenn
Flächen „planar“
Flächen konvex
Punkte nicht kolinear
a
n A
E D
C
B
A
E D
C
b B
Punkte nicht in einer Ebene Konkave Fläche
Konvex/Konkav
Konvex heißt, daß die
Verbindung zweier Punkte vollständig innerhalb der Fläche liegt
Analog: Innenwinkel nicht größer als 180°
konkav konvex
Eselsbrücke konkav: man
kann Kaffee drauf schütten
Beispiel 2: Polygone mit Löchern
Wie mache ich ein Loch in ein Polygon?
Doppelte Hilfskante
… auch nur ein konkaves Polygon
A
D C
B
H G
E F
P = {A, B, C, D, A, E, H, G, F, E}
Grundlegende Datenstruktur in den meisten Graphiksystemen sind daher Dreiecke
3 Punkte liegen immer in einer Ebene
Normale ist eindeutig
Problem: Triangulierung
Beispiele: Dreiecksmodelle
Gerade
Durch 2 (nicht-identische) Punkte A und B wird eine Gerade aufgespannt
Jeder Punkt X auf der Geraden ist gegeben durch:
Jeder Punkt auf der Kante (edge) zwischen A und B liegt im Parameterbereich t ∈ [0, 1]
Ein Strahl von A in Richtung B liegt im Bereich t ∈ [0, ∞ [ A
B AB
v = = −
v t A
X = + ⋅
A v B
Lineare Interpolation
Jeder Punkt auf der Gerade lässt sich also auch darstellen durch
Dies entspricht einer „linearen Interpolation“:
t = 0: Punkt A
t = 1; Punkt B
t∈[0, 1]; „t mal B und den Rest von A“
Eignet sich z.B. auch zur Interpolation von Farben an den Eckpunkten
A B
AB
v = = − v
t A
X = + ⋅
( t ) A
B t
X = ⋅ + 1 − ⋅
A v B
( B A )
t A
X = + ⋅ −
A t B t A
X = + ⋅ − ⋅
( t ) A
B t
X = ⋅ + 1 − ⋅
Zusammenfassung
Gerade
Parameterdarstellung
Lineare Interpolation
A B
AB
v = = −
A v B
v t A
X = + ⋅
( t ) A
B t
X = ⋅ + 1 − ⋅
A
B C
Ebenen/Dreiecke
Durch 3 Punkte wird eine Ebene aufgespannt
Parameterdarstellung:
Normalform
AB
u = v = AC X A
B C
u v
v t u s A
X = + ⋅ + ⋅
v n u
×
= ×
°
= 0
° n AX
° n
X 1
);
1 , 0 (
, t ∈ s + t ≤
s
(für Dreieck)Normalform I
Interpretation:
Gerade durch den Ursprung in Richtung n
Jeder Punkt X ist dann ein Punkt der Ebene, wenn er auf diese Gerade projiziert den gleichen Abstand vom Ursprung hat, wie die
= 0
° n AX
( X − A ) n ° = 0
( X n ° ) − ( A n ° ) = 0
A
B
° C n
X
O
A X
Normalform II
A
B
° C n
X
( ° ) = 0
−
⋅ +
⋅ +
⋅ n y n z n A n
x
x y z
= 0 +
⋅ +
⋅ +
⋅ n y n z n d
x
x y z( ° )
−
= A n
d
O
A
d
=
z y x X
=
°
z y x
n n n n
( X n ° ) − ( A n ° ) = 0
d beschreibt den (kürzesten) Abstand der Ebene vom Ursprung
Das Vorzeichen gibt Auskunft über die Lage des Urpungs zur Ebene
d=0: Ursprung Teil der Ebene
d>0: Ursprung im vorderen Halbraum
d<0: Ursprung im hinteren Halbraum
Normalform III
Typische Schreibweise für Ebenengleichung
Wobei a, b, c die
Komponenten der (normierten) Normalen sind und d den
Abstand der Ebene vom Ursprung bezeichnet
Oft sinnvoll: die 4 Parameter a,b,c,d in die Datenstruktur für das Dreieck aufnehmen, da sie dann nicht immer neu
berechnet werden müssen
Einsetzen eines beliebigen Punktes
P ist Teil der Ebene
P liegt im hinteren Halbraum
P liegt im vorderen Halbraum
= 0 +
⋅ +
⋅ +
⋅ x b y c z d
a
= 0 +
⋅ +
⋅ +
⋅ p b p c p d
a
x y z< 0 +
⋅ +
⋅ +
⋅ p b p c p d
a
x y z> 0 +
⋅ +
⋅ +
⋅ p b p c p d
a
=
z y x
p
p
p
P
Beispiel
Berechnen sie die
Ebenengleichung der oberen Würfelfläche
x z
y
) 0 , 0 , 0 (
) 2 , 2 , 2 (
=
=
0 1 0 y
n A = ( 2 , 2 , 2 )
= 0
° n AX
( X − A ) n ° = 0
0 0
1 0 2
2 2
=
−
z
y x
0 0
) 2 (
1 ) 2 (
0 ) 2
( x − ⋅ + y − ⋅ + z − ⋅ = 0
2 =
− y
=
Rastergraphik
Auf der Graphikkarte haben wir einen eigenen
Bildspeicher
Hier „schreiben“ wir unsere Bilder rein
Ein Digital/Analog-Wandler liest diesen periodisch aus und wandelt den Inhalt in das Bildsignal für den Monitor
um.
Typisches Format: 3 Byte pro Pixel (r,g,b)
Bildspeicher
AD/Wandler
Beispiele
Wir haben einen Bildschirm von 1024x768 Pixeln und je 1 Byte für r, g und b
Wieviele Farbe können wir darstellen?
Wie groß muss der Bildspeicher sein?
Bei einer Bildwiederholrate von 60 Hz, wie lange ist jedes Bild sichtbar?
216 .
777 .
16 256
256
256 ⋅ ⋅ =
Byte Byte 2 . 359 . 296
3 768
1024 ⋅ ⋅ =
msek sek
sek 0 , 016 16 60
1 = ≈
CRT (Kathodenstrahlröhre)
Flachbildschirme
TFT (Thin Film Transistor)/LCD
Liegt Spannung an, also unter Einwirkung eines
elektrischen Feldes, sind die Flüssigkristalle gerade ausgerichtet. Das polarisierte Licht wird am zweiten Polarisationsfilter absorbiert. Damit kann das Licht an dieser Stelle des TFT Bildschirms nicht austreten.
Für jedes Pixel 3mal (rgb)
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)
C++ kompakt - Teil 2
Matthias Biedermann
(Dank an Markus Geimer)
Wiederholung
Organisation von Projekten in
Definitionsdateien (.h)
Implementierungsdateien (.cpp, .cxx)
Startpunkt der Ausführung:
main-Funktion
Vor der Benutzung einer Klasse/Funktion, einbinden der Definitionsdatei mit
#include “name”
#include <name>
Vermeiden von Mehrfach- Includes durch
#ifndef SYMBOL #define SYMBOL // Inhalt der
// Definitionsdatei #endif
oder
#pragma once
Wiederholung (2)
Klassendefinition:
class Name { public:
[...]
protected:
[...]
private:
[...]
};
Methoden-Implementierung:
Typ Name::Methode(Params) { [...]
}
Konstruktoren:
Kein Rückgabewert
Name identisch mit Klassenname
Ein-/Ausgabe mit Streams
Vordefiniert:
cin, cout, cerr, clog
Verwendet die Operatoren
<< und >>
Sind definiert im
Systemheader iostream, im Namensraum std
Destruktoren
Ein Destruktor ist das Gegenstück zu einem Konstruktor. Er wird automatisch für jedes Objekt am Ende seiner Lebensdauer auf- gerufen.
Ein Destruktor ist immer parameterlos und besitzt ebenfalls keinen Rückgabewert. Sein Name setzt sich aus dem Klassen- namen und einer vorangestellten Tilde zusammen.
Beispiel:
Vector2D::~Vector2D()
{ // Bei Vector2D ist nichts freizugeben!
}
Destruktoren sollten immer dann verwendet werden, wenn in einer Klasse Member-Variablen dynamisch angelegt werden.
Anderenfalls wird der reservierte Speicher nie freigegeben!
Referenzen
“Eine Referenz ist ein alternativer Name für ein Objekt.”
Vorstellung: Eine Referenz ist ein (nicht veränderbarer) Zeiger auf ein Objekt, der bei jeder Benutzung dereferenziert wird.
Damit eine Referenz immer an ein Objekt gebunden ist, muss man sie zwingend initialisieren.
Beispiel:
int x = 17;
int& xr = x;
int y = x; // y = 17
int z = xr; // z = 17, da xr Synonym für x
Anwendung: Call-by-reference
Beispiel
Aufrufsequenz:
int a = 2;
doMagic(a);
Call-by-value:
void doMagic(int x) {
x = 5; // x ist lokale Kopie, a unverändert }
Call-by-reference:
void doMagic(int& x) {
x = 5; // a = 5, da x hier Synonym für a }
Bei großen Objekten kann das Anlegen einer lokalen Kopie für Call-by-value sehr teuer werden! Referenzen einsetzen
Automatisch erzeugte Methoden
Definiert der Programmierer keinen eigenen Konstruktor, dann erzeugt der Compiler implizit einen leeren Default-Konstruktor (ohne Parameter).
Ein leerer Destruktor wird ebenfalls automatisch erzeugt.
Der sog. Kopier-Konstruktor (hier am Beispiel der Vector2D- Klasse)
Vector2D(Vector2D& value);
initialisiert die Instanz mit einer 1:1-Kopie des Objektes value (Vorsicht bei dynamisch erzeugten Member-Variablen!).
Dazu kommt der Zuweisungsoperator =, der die Daten der
Objektes auf der rechten Seite 1:1 in das Objekt auf der linken Seite kopiert (Nochmal Vorsicht!).
Überladen von Funktionen
In C++ ist es möglich Funktionen zu definieren, die sich nur in der Anzahl bzw. den Typen der Parameter unterscheiden (overloading).
Unterscheidung nur durch den Rückgabetyp reicht jedoch nicht!
Beispiel:
float sqrt(float value);
double sqrt(double value);
Bei einem Aufruf wird anhand der realen Parameter entschieden, welche Variante auszuführen ist:
float f = 3.14159f;
double d = 2.71828;
float x = sqrt(f); // Ruft float-Variante auf double y = sqrt(d); // Ruft double-Variante auf
Überladen von Operatoren
Es ist auch möglich, die Operatoren für eigene Datentypen zu überladen. Man kann jedoch weder neue Operatoren definieren, noch die Funktionsweise für elementare Typen abändern.
Beispiel:
Vector2D operator +(Vector2D& a, Vector2D& b) { Vector2D result;
result.mElement[X] = a.x() + b.x();
result.mElement[Y] = a.y() + b.y();
return result;
}
Wichtig: “+=“ ist ein eigener Operator. Durch Überladen von “+“ wird er nicht automatisch definiert.
Eine gute Lösung... (1)
Vector2D& Vector2D::operator +=(const Vector2D& rhs) {
mElement[X] += rhs.mElement[X];
mElement[Y] += rhs.mElement[Y];
return *this;
}
Zuweisungen haben in C++ auch ein
Ergebnis!
Nur die Referenz übergeben und keine Kopie erzeugen.
rhs wird nicht verändert, ist also konstant.
Innerhalb der Klasse hat man direkten Zugriff auf alle Attribute. Auch auf die als Parameter übergebener Objekte.
Eine gute Lösung... (2)
const Vector2D operator +(const Vector2D& lhs, const Vector2D& rhs) {
return Vector2D(lhs) += rhs;
}
Frage: Warum ist der Ergebnistyp ein const Vector2D ? Sonst wäre auch a+b = c; möglich (grober Unfug)!
Nur die Referenzen übergeben und keine Kopien erzeugen.
rhs und lhs werden nicht verändert, sind also konstant.
Erzeuge eine temporäre Instanz, initialisiere sie mit dem Inhalt von lhs und addiere darauf rhs.
Beispiel
#include <iostream>
#include “Vector2D.h”
using namespace std;
int main() {
Vector2D a(1, 2);
Vector2D b(7, 5);
Vector2D c = a + b;
cout << “Ergebnis: (“ << c.x() << “,”
<< c.y() << “)” << endl;
return 0;
}
Anmerkungen
Durch die Implementierung von operator + auf der Basis von
operator += muss nur noch eine Operator-Implementierung gepflegt werden.
Operatoren sollten ‘erwartungskonform’ sein.
Beispiel:
Die Multiplikation eines Vektors mit einem Skalar ist kommutativ.
Daher sollte man hierfür zwei Operatoren überladen.
const Vector2D operator *(float s, const Vector2D&
v);
const Vector2D operator *(const Vector2D& v, float s);