Sitzung: Jeden Freitag ab 14:30 s.t. online. Falls ihr den Link haben wollt, schreibt uns.

C-Kurs/DreiD

In dieser Aufgabe wollen wir eine stark vereinfachte 3D-Anwendung schreiben, welche einen 3D-Körper rotieren lässt. Du kannst selbst entscheiden, bis zu welchem Zwischenergebnis du die Aufgabe lösen willst.

Vorgabe

Lade dir zunächst die Vorgaben herunter. Die "screen"-Dateien brauchen nicht verändert werden, sie sorgen für die grafische Ausgabe. Deine Aufgabe ist es, die Rechenoperationen der main.c zu ergänzen.

Kompiliert wird das Programm mit:

gcc main.c screen.c -lX11 -lm -o 3D.out

Denke daran, in der Konsole die Pfeiltaste Hoch zu verwenden, statt es jedesmal neu einzugeben.

Werden die Vorgaben direkt kompiliert und ausgeführt, ist ein Quadrat mit Linien zu sehen. Dies ist ein frontal betrachteter Drahtgitter-Würfel. Für die Rotationen fehlen die zu ergänzenen Transformationsoperationen. Beende das Programm mit Ctrl+C in der Konsole.

Rechenoperationen

Für die Aufgabe könnte es hilfreich sein, einen Schmierzettel bereitzulegen.

LinA-Grundkenntnisse sind von Vorteil.

Transformation

Wir befinden uns in der Funktion "refresh_transformation_matrix" und wollen den Würfel nun erst um die X-Achse und dann um die Y-Achse drehen. Dazu verwenden wir Rotationsmatrizen, welche wir als 3x3-Array umsetzen, wobei wir uns einigen, dass der erste Array-Index die Zeile und der zweite die Spalte der Matrix ist. Die rot_x ist bereits vorgegeben. Initialisiere auf gleiche Weise darunter die rot_y, unter Verwendung des Winkels "alpha". Schreibe direkt darunter einen Algorithmus, der die Matrizen rot_x und rot_y miteinander multipliziert und das Ergebnis auf der globalen Matrix "transform" speichert. Versuche dabei Schleifen, statt konstanter Indizes zu verwenden. Beachte: Matrix-Multiplikation ist nicht kommutativ.

Die resultierende Matrix auf einen Vektor angewandt, rotiert diesen nun entsprechend um die X- und Y-Achse. Dies soll in der Funktion "apply_transform" geschehen. apply_transform nimmt einen Punkt der Figur im Raum und soll diesen entsprechend der aktuellen Rotation verändern. Die Parameter sind Call-by-Reference. Bisher sind in den Funktionen nur Zuweisungen ohne Effekt. Ändere mit Hilfe der globalen "transform"-Matrix die Inhalte der Parameter so, dass der x-y-z-Vektor am Ende rotiert ist (Matrix-Vektor-Multiplikation).

Teste deine Implementierung. Zu diesem Zeitpunkt sollte schon ein rotierender Würfel zu sehen sein. Falls der Würfel beim Rotieren verzerrt wird, sind möglicherweise Indizes bei der Matrix Multiplikation verdreht.

Nebenbei: echte 3D-Anwendungen verwenden 4x4-Matrizen, damit auch Translationen möglich sind. Da wir aber nur rotieren wollen, reichen 3x3-Matrizen.

Culling

Schalte den "WIREFRAMES_MODE" auf 0, wodurch der Würfel ausgefüllte Flächen bekommt und sieh dir zunächst das Ergebnis an. Zu sehen ist das Problem, dass wir strikt jede Fläche zeichnen, egal ob sie räumlich vor oder hinter den bisher gezeichneten Flächen ist. Die allgemeine und standardmäßige Lösung wäre ein Z-Buffer, wodurch Pixel, die verdeckt werden würden, einfach nicht gezeichnet werden. Dies ist für unsere Zwecke zu aufwändig, daher nehmen wir eine speziellere Lösung: Culling.

Die Überlegung ist, dass es bei einem soliden Würfel nicht passieren kann, dass wir irgendeine fläche von Innen sehen, da diese immer von einer Außenfläche verdeckt wird. Außerdem verdeckt nie eine Außenfläche eine andere (sichtbare) Außenfläche, da der Würfel konvex ist. Wir brauchen also nur die Außenflächen zu zeichnen.

Wir befinden uns in der Funktion "render_primitive", welche ein einzelnes Dreieck zeichnet. Nachdem die Punkte (x1,y1, ... ,z3) transformiert wurden, muss geprüft werden, ob wir von innen auf die Fläche schauen. Bilde dazu aus den Koordinaten die beiden Richtungsvektoren des Dreiecks (der Ortsvektor ist egal) und bilde aus ihnen das Kreuzprodukt. Nun kann man sich überlegen, dass das Dreieck genau dann von außen zu sehen ist, wenn die Z-Koordinate des Kreuzprodukts kleiner als 0 ist (somit brauch man streng genommen die X- und Y-Koordinaten des Kreuzprodukts gar nicht). Ist die Z-Koordinate also größer-gleich 0, verlasse die Funktion vorzeitig (vor dem Zeichnen des Dreiecks).


Nun sollte ein rotierender, ausgefüllter, fehlerfreier Würfel zu sehen sein.

Licht

Wir wollen nun die einfachste Art von Licht implementieren: Direktionales Licht. Wir haben lediglich einen Licht-Vektor (lgt_x,lgt_y,lgt_z), welcher von der Vorgabe bereits gesetzt wird und normalisiert ist. Zeigt dieser nun in die gleiche Richtung, wie unser Kreuzprodukt von eben, gehen wir davon aus, dass das Dreieck frontal beleuchtet wird. Wie könnte man prüfen, inwiefern die Richtungen der Vektoren übereinstimmen? Als wichtiger Hinweis: Das Kreuzprodukt muss noch normalisiert werden, da die Länge des Vektors das Ergebnis verfälscht.

Das Ergebnis deiner Berechnung soll ein Float zwischen 0 (nicht angeleuchtet) und 1 (voll angeleuchtet) sein. Multipliziere dein Ergebnis jeweils mit den Farb-Parametern im "set_color"-Aufruf. Wenn alles stimmt, sollte schon ein (wenn auch etwas zu kontrastreiches) Licht zu sehen sein. Experimentiere mit dem Helligkeitswert um die Lichtausbreitung etc. anzupassen. Setze beispielsweise eine Untergrenze größer 0, bilde Wurzeln oder/und gleiche den Wert mit Addition und Multiplikation an.

Andere Figuren

Wenn du Lust hast, kannst du mit Hilfe von render_primitive und render_face eigene 3D-Figuren erstellen. Ersetze dazu den Würfel in der main.c zwischen begin_scene und end_scene einfach durch deinen Algorithmus. Denke aber daran, dass, sofern Wireframes aus sind, die Figur konvex sein sollte und die Koordinaten der Dreiecke/Vierecke jeweils im Uhrzeigersinn angegeben werden müssen. Eine mögliche Figur ist der Kegel:

   /* Kegel */
   refresh_color(1,1,1);
   int acc = 32; //Genauigkeit
   float rad = 1.2; //Radius
   int i;
   float ang = 0, lst_ang;
   for(i=1; i<acc+1; i++)
   {
       refresh_color(0.8,0.8,1);
       lst_ang = ang;
       ang = (float)i/acc*2*M_PI;
       render_primitive(0,-1.2,0, sin(ang)*rad,1.2,cos(ang)*rad, sin(lst_ang)*rad,1.2,cos(lst_ang)*rad);
       if(i%2 == 0)
           refresh_color(0.1,0.1,0.5);
       else
           refresh_color(0.2,0.2,0.6);
       render_primitive(0,1.2,0, sin(lst_ang)*rad,1.2,cos(lst_ang)*rad, sin(ang)*rad,1.2,cos(ang)*rad);
   }

Theoretisch können auch animierte Figuren gezeichnet werden. Benutze dazu den Zähler "c_time".