>binaervarianz >projects >CocoaRay
Kapitel 4: Eine Aufrufungshierarchie anlegen

Bisher wird unser Bild vollständig in der -(void)drawImage Methode gezeichnet. Unser ganzer Raytracingcode könnte also hier rein. Natürlich würde das der Übersicht nicht gerade beitragen, sobald unser Code etwas komlexer wird. Also überlegen wir uns eine logische Anordnung von Objekten und Funktionen.

Welche Informationen wären in externen Objekten besser aufgehoben? Unsere darzustellende Szene ist natürlich unabhängig vom Renderalgorithmus und gehört in eine eigene Klasse. Zu den Szeneninformationen gehören zum Beispiel Kamera- und Lichtposition. Wiederum unabhängig davon möchten wir unser Modell, also die darzustellenden Objekte verwalten. Also auch dazu eine eigene Klasse.

Zudem werden wir alle reinen C-Funktionen, also zumeist die mathematischen Berechnungen, in einer gesonderten Datei definiert. Auch den Algorithmus selbst wird aufgespalten. In der -(void)drawImage Methode gehen wir lediglich die einzelnen Pixel des Bildes durch. Wir rufen von hier aus eine weitere Funktion auf, die uns für eine bestimmte Koordinate den Farbwert liefert, in den der Pixel eigefärbt werden muss. Diese Funktion kümmert sich  um das 'Shading',  also die Beleuchtung, den Schattenwurf und die Überdeckung. Dazu wird eine weitere Funktion genutzt, die für einen Lichtstrahl berechnet, ob und welches Objekt in der Szene getroffen wird. Dazu muss in einer letzten Funktion der Schnittpunkt eines Lichtstrahls mit dem Objekt berechnet werden.

Durch welche Datentypen soll die Szene nun repräsentiert werden? Ein Punkt im Raum kann mit 3 Koordinaten definiert werden. Für Transformationen eigenen sich später dann bis zu 4 Koordinaten, aber wir benötigen vorerst nur 3. Zur Berecnung benötigen wir zwingend Kommawerte, der Genauigkeit wegen in Fließkommakodierung. Zur ersten, vorgegriffenen Optimierung beschränken wir uns auf 32bit floats statt 64bit doubles. Das Grundobjekt unseres Modells soll das gebräuchliche Dreieck werden. Dazu benötigen wir jeweils 3 der eben beschriebenen Vektoren.

Farben werden genau wie im späteren Bildpuffer durch 3 Werte je 8bit für Rot, Grün und Blau angegeben. Als Datentyp hierfür eignet sich unsigned char.

Wir legen also 6 neue Dateien an. Jeweils Header und Implementation für scene, model und math. Die Headerdateien müssen natürlich in BIRenderer eingebunden werden, wenn Funktionen aus den neuen klassen benutzt werden sollen. In der math.h definieren wir die Datentypen:

typedef struct _BIFloatVector {
float x;
float y;
float z;
}BIFloatVector;

typedef struct _BIFloatTriangle {
BIFloatVector v1;
BIFloatVector v2;
BIFloatVector v3;
unsigned char color[3]; }BIFloatTriangle;

Im BIRenderer implementieren wir in der drawImage Funktion einen Aufruf zur Bestimmung der Farbe eines Pixels. Dessen Anwort erhalten wir in Form eines Arrays, dessen Zeiger wir der Funktion vorher übergeben haben. 


- (void)drawImage{
unsigned char colors[3];
int columnIndex=0, rowIndex=0;
for(rowIndex=0;rowIndex<360;rowIndex++){
for(columnIndex=0;columnIndex<240;columnIndex++){
[self getColorForX:columnIndex andY:rowIndex toArray:colors];
baseAddr[rowIndex*3*360 + columnIndex*3+0]=colors[0];
baseAddr[rowIndex*3*360 + columnIndex*3+1]=colors[1];
baseAddr[rowIndex*3*360 + columnIndex*3+2]=colors[2];
}
}
}

Diese Funktion müssen wir natürlich erst implementieren. Dies tun wir vereinfacht wie folgt. (Die Deklaration im Header wird nun nicht mehr explizit erwähnt und bleibt dem Leser selbst überlassen.)

- (void)getColorForX:(float)x andY:(float)y inArray:(unsigned char[3]) color{ 
if ([self getHitForX:x andY:y]){
color[0]='\x00';
color[1]='\x00';
color[2]='\x00';
}
else{
color[0]='\xFF';
color[1]='\xFF';
color[2]='\xFF';
}
}

Womit wir unser Bild relativ einfach Schwarz/Weiß einfärben, je nach dem ob an dieser Stelle ein Dreieck getroffen wird oder nicht. Die hierzu benutzte Funktion muss dazu den Lichtstrahl für den aktuellen Pixel berechnen und ihn auf Überschneidung mit jedem Dreieck der Szene prüfen. Dazu braucht die Funktion Zugriff auf die Model- und Scene-Objekte. Je ein Pointer zu den zwei Objekten muss also im BIRenderer Header deklariert (Model* myModel;) und in der init Funktion mit [[myModel alloc] init]; initialisiert werden.

- (bool) getHitForX:(float)x andY:(float)y{ 

BIFloatTriangle triangle;;
BIFloatVector camera= [myScene cameraPosition]; //camera point
BIFloatVector pixelPoint; //point of the pixel
pixelPoint.x=x;
pixelPoint.y=y;
pixelPoint.z=0; //display pane lies at z=0;
BIFloatVector direction= SUB(pixelPoint,camera); //vector camera->pixel
for(int i =0; i<[myModel triangleCount]; i++){ //for all triangles in the scene
triangle=[myModel getTriangleAtIndex:i];
if (intersectVector(camera,direction,triangle)){
return true;
}
}
return false;
}

Wir haben hier bereits 2 mathematische Hilfsfunktionen genutzt. SUB(a,b) subtrahiert 2 Vektoren und hinter intersectVector steckt der Code zur Berechnung des Schnittpunktes von Lichtstrahl und Dreieck. Um jetzt erstmal wieder zu einem kompilierbaren Ergebnis zu kommen, müssen wir die benutzten Funktionen von Model, Scene und Math aber wenigstens im Header deklarieren und im Implementationsfile mit einer Dummy-Funktion versehen. Benutzt haben wir:

  1. BIFloatVector Scene::cameraPosition
  2. int Model::triangleCount
  3. BIFloatTriangle Model::triangleAtIndex:(int)i
  4. BIFloatVector Math::SUB(BIFloatVector a, BIFloatVector b)
  5. bool Math::intersectVector(BIFloatVector cam, BIFloatVector dir, BIFloatTriangle tri)

1. und 2. sind einfache Getter, geben also nur den Wert einer internen Variable zurück. Funktion und Variable müssen also im Header deklariert, die Variablen in der init Funktion gesetzt und die Funktionen implementiert werden. 3. ist nur wenig komplizierter. Bei der Variable handelt es sich um ein Array, die Funktion muss also nicht die ganze Variable, sondern das durch den Parameter ausgewählte Element zurückgeben. Math ist kein Objekt, sondern eine reine C-Datei. Da wir einfache Hilfsfunktionen erstellen, können wir auf den zusätzlichen Aufwand der ObjC-Objektverwaltung verzichten. Die Syntax der Headerdatei vereinfacht sich dadurch leicht, Funktionen werden zum Beispiel einfach mittels BIFloatVector SUB(BIFloatVector a, BIFloatVector b); deklariert. Implementiert wird die SUB Funktion wie folgt:

BIFloatVector SUB(BIFloatVector a, BIFloatVector b){
BIFloatVector temp;
temp.x = a.x - b.x;
temp.y = a.y - b.y;
temp.z = a.z - b.z;
return temp;
}

Die 5. Funktion, intersectVector, enthält einen mathematischen Algorithmus, den wir im nächsten Kapitel erarbeiten wollen. Für's erste implementieren wir als Dummyfunktion:

bool intersectVector(BIFloatVector cam, BIFloatVector dir, BIFloatTriangle tri){
if(dir.x<10 && dir.x>-10 && dir.y<10 && dir.y>-10){
return true;}
else {
return temp;}
}

Fehlen für einen neuen Prototypen nur noch Werte für die Variablen zu den Funktionen 1. bis 3. Diese setzen wir in den init Funktionen der Model und Scene Objekte. Als Beispiel hier einige Werte:

//Model
//array for triangles initialized with size 2
triangleCount=2;
triangles[0].v1.x= 240;
triangles[0].v1.y= 120;
triangles[0].v1.z= 70;
triangles[0].v2.x= 80:
triangles[0].v2.y= 100:
triangles[0].v2.z= 50:
triangles[0].v3.x= 230:
triangles[0].v3.y= 210:
triangles[0].v3.z= 90:

triangles[1].v1.x= 360;
triangles[1].v1.y= 242;
triangles[1].v1.z= 70;
triangles[1].v2.x= 307:
triangles[1].v2.y= 80:
triangles[1].v2.z= 70:
triangles[1].v3.x= 20:
triangles[1].v3.y= 38:
triangles[1].v3.z= 70:

//Scene:
cameraPosition.x= 180;
cameraPosition.y= 120;
cameraPosition.z= 0;

Was folgendes Testbild erzeugen sollte:

Prototyp_C4


>binaervarianz >projects >CocoaRay