Tutorial Raytracing - Grundlagen II: Unterschied zwischen den Versionen
I0n0s (Diskussion | Beiträge) K |
DGLBot (Diskussion | Beiträge) K (Der Ausdruck ''<cpp>(.*?)</cpp>'' wurde ersetzt mit ''<source lang="cpp">$1</source>''.) |
||
Zeile 7: | Zeile 7: | ||
Wir wollen die Objekte in unsem Raytracer möglichst gleich behandeln können, so daß der eigentliche Raytracing Algorithmus ohne jegliches wissen über die Struktur der Objekte arbeiten kann. Sprich: Wir definieren ein Basisobjekt, von dem anschließend alle weiteren Objekte abgeleitet werden. Was also soll dieses Objekt an Methoden und Eigenschaften zur Verfügung stellen? Jedes Objekt hat sicherlich eine Position, sowie gewissen Materialeigenschaften, die dem Objekt zugewiesen werden. Auch soll das Objekt sauber mit Transformationen umgehen können. Und was natürlich ganz wichtig ist: Wir brauchen eine Methode die prüft, ob sich das Objekt mit dem Strahl schneidet. Über einige dieser Dinge haben wir uns noch keine Gedanken gemacht, entsprechend sollen hierfür nur leere Dummyklassen und Methoden zum Einsatz kommen, die wir erst später realisieren. Damit es nicht vergessen wird: Es ist auch sinnvoll Vater-Kindbeziehungen zu haben um später zusammengesetzte Objekte definieren zu können. Das klingt alles etwas wild, artet in einer großen Menge Code aus, ist aber eigentlich gar nicht so schlimm, wei es sich anhört: | Wir wollen die Objekte in unsem Raytracer möglichst gleich behandeln können, so daß der eigentliche Raytracing Algorithmus ohne jegliches wissen über die Struktur der Objekte arbeiten kann. Sprich: Wir definieren ein Basisobjekt, von dem anschließend alle weiteren Objekte abgeleitet werden. Was also soll dieses Objekt an Methoden und Eigenschaften zur Verfügung stellen? Jedes Objekt hat sicherlich eine Position, sowie gewissen Materialeigenschaften, die dem Objekt zugewiesen werden. Auch soll das Objekt sauber mit Transformationen umgehen können. Und was natürlich ganz wichtig ist: Wir brauchen eine Methode die prüft, ob sich das Objekt mit dem Strahl schneidet. Über einige dieser Dinge haben wir uns noch keine Gedanken gemacht, entsprechend sollen hierfür nur leere Dummyklassen und Methoden zum Einsatz kommen, die wir erst später realisieren. Damit es nicht vergessen wird: Es ist auch sinnvoll Vater-Kindbeziehungen zu haben um später zusammengesetzte Objekte definieren zu können. Das klingt alles etwas wild, artet in einer großen Menge Code aus, ist aber eigentlich gar nicht so schlimm, wei es sich anhört: | ||
− | <cpp> /// General Raytracing object in a tracer. All traceable objects must be derivates | + | <source lang="cpp"> /// General Raytracing object in a tracer. All traceable objects must be derivates |
public abstract class ObjectBase | public abstract class ObjectBase | ||
{ | { | ||
Zeile 90: | Zeile 90: | ||
#endregion | #endregion | ||
} | } | ||
− | </ | + | </source> |
Das schaut jetzt tatsächlich etwas wild aus, aber keine Angst. Wir werden das alles der Reihe nach mit Leben füllen. Die Trans-Space-Funktionen werden wir anschließend mit Leben füllen - nur TransSpaceParentToObject kann gleich bestückt werden (wir werden das aber später erweitern müssen). Sie soll Strahlen aus dem Eltern-Raum in den lokalen Raum überführen - für die meisten Objekte bedeutet dies: Die globalen Koordinaten werden in lokale übersetzt, so daß sich unser Objekt genau im Zentrum des Geschehens, d.h. bei (0,0,0), befindet und wir uns dann nie wieder über die Position des Objektes Gedanken machen müssen. | Das schaut jetzt tatsächlich etwas wild aus, aber keine Angst. Wir werden das alles der Reihe nach mit Leben füllen. Die Trans-Space-Funktionen werden wir anschließend mit Leben füllen - nur TransSpaceParentToObject kann gleich bestückt werden (wir werden das aber später erweitern müssen). Sie soll Strahlen aus dem Eltern-Raum in den lokalen Raum überführen - für die meisten Objekte bedeutet dies: Die globalen Koordinaten werden in lokale übersetzt, so daß sich unser Objekt genau im Zentrum des Geschehens, d.h. bei (0,0,0), befindet und wir uns dann nie wieder über die Position des Objektes Gedanken machen müssen. | ||
Zeile 98: | Zeile 98: | ||
Noch schnell ein kleiner Blick in eine mögliche Definition der IntersectionInfo. Darin befindet sich nichts anderes, als wie das, was ich gerade beschrieben habe. Also keine Überraschungen ;-) | Noch schnell ein kleiner Blick in eine mögliche Definition der IntersectionInfo. Darin befindet sich nichts anderes, als wie das, was ich gerade beschrieben habe. Also keine Überraschungen ;-) | ||
− | <cpp> public struct IntersectionInfo | + | <source lang="cpp"> public struct IntersectionInfo |
{ | { | ||
/// Position on ray where the cut occured | /// Position on ray where the cut occured | ||
Zeile 112: | Zeile 112: | ||
public Color localColor; | public Color localColor; | ||
} | } | ||
− | </ | + | </source> |
== Transformationen == | == Transformationen == | ||
Zeile 119: | Zeile 119: | ||
Wer hoch in den Code des Basis-Objektes schaut, dem wird auffallen, daß Transformationen durch das Interface ILinearTransformation definiert sind: | Wer hoch in den Code des Basis-Objektes schaut, dem wird auffallen, daß Transformationen durch das Interface ILinearTransformation definiert sind: | ||
− | <cpp> | + | <source lang="cpp"> |
/// Defines a linear Trafo | /// Defines a linear Trafo | ||
public interface ILinearTransformation | public interface ILinearTransformation | ||
Zeile 138: | Zeile 138: | ||
ILinearTransformation MultiplyWithLinear(ILinearTransformation factor); | ILinearTransformation MultiplyWithLinear(ILinearTransformation factor); | ||
} | } | ||
− | </ | + | </source> |
So kann man ohne weiteres Quaternionen und Matrizen wild nebeneinander verwenden. Mit der Methode MultiplyWithLinear kann man dann auch verschiedene Multiplikationen miteinander verknüpfen. So kann man Quaternionen einfach mit Quaternionen multiplizieren und gemischtes, wenn etwa Quaternionen mit Matrizen multipliziert werden, in Matrizen umwandeln: | So kann man ohne weiteres Quaternionen und Matrizen wild nebeneinander verwenden. Mit der Methode MultiplyWithLinear kann man dann auch verschiedene Multiplikationen miteinander verknüpfen. So kann man Quaternionen einfach mit Quaternionen multiplizieren und gemischtes, wenn etwa Quaternionen mit Matrizen multipliziert werden, in Matrizen umwandeln: | ||
− | <cpp> | + | <source lang="cpp"> |
#region ILinearTransformation Members | #region ILinearTransformation Members | ||
Zeile 189: | Zeile 189: | ||
} | } | ||
#endregion | #endregion | ||
− | </ | + | </source> |
An diesem Beispiel aus meinem Quaternioncode kann man folgendes sehen: Die Funktionen ConvertTo3x3 und ConvertInverseTo3x3 berechnen jeweils die zum Quaternion gehörenden, äquivalenten Matrizen ( Man erinnere sich an den Satz, den ich bereits im Nachhilfe Tutorial zitiert habe: Die Spalten sp<sub>i</sub> der Matrix sind die Bilder der Einheitsvektoren. Dann dürfte sofort klar sein, was hier passiert ). In MultiplyWithLinear wird dann einfach geprüft, ob die Transformation mit der rechts multipliziert wird, ein Quaternion ist. Dann wird einfach Quaternion-Multiplikation durchgeführt, ansonsten werden sowohl der Faktor als auch das betrachtete Quaternion in 3x3 Matrizen umgewandelt und in einer LinearTransformationMatrix abgelegt - welches natürlich ILinearTransformation implementiert und immer die Transformationsmatrix und ihr Inverses bereithält. | An diesem Beispiel aus meinem Quaternioncode kann man folgendes sehen: Die Funktionen ConvertTo3x3 und ConvertInverseTo3x3 berechnen jeweils die zum Quaternion gehörenden, äquivalenten Matrizen ( Man erinnere sich an den Satz, den ich bereits im Nachhilfe Tutorial zitiert habe: Die Spalten sp<sub>i</sub> der Matrix sind die Bilder der Einheitsvektoren. Dann dürfte sofort klar sein, was hier passiert ). In MultiplyWithLinear wird dann einfach geprüft, ob die Transformation mit der rechts multipliziert wird, ein Quaternion ist. Dann wird einfach Quaternion-Multiplikation durchgeführt, ansonsten werden sowohl der Faktor als auch das betrachtete Quaternion in 3x3 Matrizen umgewandelt und in einer LinearTransformationMatrix abgelegt - welches natürlich ILinearTransformation implementiert und immer die Transformationsmatrix und ihr Inverses bereithält. | ||
Damit ihr nicht ewig rumrechnen müsst (es handelt sich nämlich um eine schrecklich langwierige und ekelige Rechnerei: Ich habe etwa 2 Stunden gebraucht, bis das Ergebnis endlich fehlerfrei und brauchbar auf 2 DIN-A4 Blättern verteilt da stand), wenn ihr Quaternionen implementiert noch ein paar kleine Methoden des Quaternions. Den Rest werdet ihr dann wohl aus dem [[Quaternion|Quaternionen-Artikel]] selbst in kurzer Zeit zusammenstöpseln können. | Damit ihr nicht ewig rumrechnen müsst (es handelt sich nämlich um eine schrecklich langwierige und ekelige Rechnerei: Ich habe etwa 2 Stunden gebraucht, bis das Ergebnis endlich fehlerfrei und brauchbar auf 2 DIN-A4 Blättern verteilt da stand), wenn ihr Quaternionen implementiert noch ein paar kleine Methoden des Quaternions. Den Rest werdet ihr dann wohl aus dem [[Quaternion|Quaternionen-Artikel]] selbst in kurzer Zeit zusammenstöpseln können. | ||
− | <cpp> // "Coordinates" of the Quaternion. | + | <source lang="cpp"> // "Coordinates" of the Quaternion. |
// coords[0] is the real component, coords[1] the i comp., ... | // coords[0] is the real component, coords[1] the i comp., ... | ||
private double[] coords = new double[4]; | private double[] coords = new double[4]; | ||
Zeile 265: | Zeile 265: | ||
return result; | return result; | ||
} | } | ||
− | </ | + | </source> |
Wer ein Quaternion mithilfe des angegebenen Konstruktors erstellt, erhält ein Rotationsquaternion der Norm 1. Genau das, was für die Funktionen Apply und ApplyInverse notwendig ist, damit sie das tun, was sie sollen. Damit soll ersteinmal der Faulheit beim Rechnen und dem Copy&Paste genüge getan sein. Und wehe ich entdecke irgendwann Raytracer, die Objekte nur rotieren können statt beliebige [[Matrix|Matrizen]] zu vertragen ;-) Am Ende bleibt es natürlich euch überlassen. Wenn ihr wollt, könnt ihr all das auch allein durch Matrizen implementieren und euch den Umweg über das Quaternion ersparen - allerdings sieht man einem Quaternion schneller an was es bewirkt, als einer Matrix. | Wer ein Quaternion mithilfe des angegebenen Konstruktors erstellt, erhält ein Rotationsquaternion der Norm 1. Genau das, was für die Funktionen Apply und ApplyInverse notwendig ist, damit sie das tun, was sie sollen. Damit soll ersteinmal der Faulheit beim Rechnen und dem Copy&Paste genüge getan sein. Und wehe ich entdecke irgendwann Raytracer, die Objekte nur rotieren können statt beliebige [[Matrix|Matrizen]] zu vertragen ;-) Am Ende bleibt es natürlich euch überlassen. Wenn ihr wollt, könnt ihr all das auch allein durch Matrizen implementieren und euch den Umweg über das Quaternion ersparen - allerdings sieht man einem Quaternion schneller an was es bewirkt, als einer Matrix. | ||
Nachdem wir einen kleinen Ausflug in Richtung Quaternionen und Transformationen hinter uns gebracht haben, könnte man hoffen, daß wir jetzt endlich mit dem abstrakten Kram aufhören und wir bald mal wieder ein paar neue Bilder zu Gesicht bekommen. Soweit ist es leider noch nicht. Ich möchte zwischendrin den Schlachtplan für die nächsten Minuten bekannt geben: Wir werden jetzt die Punkte in den Trans-Space-Funktionen der Basisklasse füllen, um uns dann dem Licht durch Phong-Lighting über den Umweg der Normalen zu widmen. Beginnen wir also mit der Funktion TransSpaceParentToObject, welche Strahlen des elterlichen Raumes in den lokalen Raum transformieren soll. Wenn man nun an die Kameraanalogie denkt, wie sie etwa im [[Tutorial_Kamera1|Kamera Tutorial]] beschrieben ist, dann wissen wir, daß wir die auf das Objekt angewendete Transformation einfach nur umkehren müssen. Denkt man dann noch daran, daß wir erreichen wollen, daß das Objekt im lokalen Raum am Ursprung befinden soll, ist die Implementation schnell naheliegend: Den Ursprung des Strahles müssen wir erst um die Position verschieben und dann invers transformieren. Bei der Richtung genügt es, diese invers zu transformieren. | Nachdem wir einen kleinen Ausflug in Richtung Quaternionen und Transformationen hinter uns gebracht haben, könnte man hoffen, daß wir jetzt endlich mit dem abstrakten Kram aufhören und wir bald mal wieder ein paar neue Bilder zu Gesicht bekommen. Soweit ist es leider noch nicht. Ich möchte zwischendrin den Schlachtplan für die nächsten Minuten bekannt geben: Wir werden jetzt die Punkte in den Trans-Space-Funktionen der Basisklasse füllen, um uns dann dem Licht durch Phong-Lighting über den Umweg der Normalen zu widmen. Beginnen wir also mit der Funktion TransSpaceParentToObject, welche Strahlen des elterlichen Raumes in den lokalen Raum transformieren soll. Wenn man nun an die Kameraanalogie denkt, wie sie etwa im [[Tutorial_Kamera1|Kamera Tutorial]] beschrieben ist, dann wissen wir, daß wir die auf das Objekt angewendete Transformation einfach nur umkehren müssen. Denkt man dann noch daran, daß wir erreichen wollen, daß das Objekt im lokalen Raum am Ursprung befinden soll, ist die Implementation schnell naheliegend: Den Ursprung des Strahles müssen wir erst um die Position verschieben und dann invers transformieren. Bei der Richtung genügt es, diese invers zu transformieren. | ||
− | <cpp> | + | <source lang="cpp"> |
public virtual Ray TransSpaceParentToObject(Ray r) | public virtual Ray TransSpaceParentToObject(Ray r) | ||
{ | { | ||
Zeile 282: | Zeile 282: | ||
return ray; | return ray; | ||
} | } | ||
− | </ | + | </source> |
Bei dieser Überlegung fallen auch gleich die Funktionen TransSpacePointParentToObject, welche einen Punkt in die lokalen Koordinaten überträgt und die Entsprechende Funktion TransSpaceParentToObject für Richtungen ab. Das ganze können wir natürlich auch in die andere Richtung machen: Bislang ist mir aber noch kein Grund eingefallen, dies zu tun. Wichtig dagegen ist es, Richtungen in Weltkoordinaten umzurechnen, da wir dies für die Normalen später benötigen werden. Dies erreichen wir leicht, wenn wir ausnutzen, daß Objekte immer ihre Eltern kennen: | Bei dieser Überlegung fallen auch gleich die Funktionen TransSpacePointParentToObject, welche einen Punkt in die lokalen Koordinaten überträgt und die Entsprechende Funktion TransSpaceParentToObject für Richtungen ab. Das ganze können wir natürlich auch in die andere Richtung machen: Bislang ist mir aber noch kein Grund eingefallen, dies zu tun. Wichtig dagegen ist es, Richtungen in Weltkoordinaten umzurechnen, da wir dies für die Normalen später benötigen werden. Dies erreichen wir leicht, wenn wir ausnutzen, daß Objekte immer ihre Eltern kennen: | ||
− | <cpp> public virtual Vertex3 TransSpaceObjectToWorld(Vertex3 v) | + | <source lang="cpp"> public virtual Vertex3 TransSpaceObjectToWorld(Vertex3 v) |
{ | { | ||
if (ptransformation != null) | if (ptransformation != null) | ||
Zeile 297: | Zeile 297: | ||
return v; | return v; | ||
} | } | ||
− | </ | + | </source> |
Die entsprechende Funktion für Punkte solltet ihr der Übung halber selbst schreiben. Mit den genannten Zweien kommen wir auf jeden Fall erst einmal aus - ich bin mir gar nicht sicher, ob in meinem eigenen Raytracer je die Anderen zum Einsatz gekommen sind. Naja, es schadet jedenfalls nicht, wenn sie da sind. | Die entsprechende Funktion für Punkte solltet ihr der Übung halber selbst schreiben. Mit den genannten Zweien kommen wir auf jeden Fall erst einmal aus - ich bin mir gar nicht sicher, ob in meinem eigenen Raytracer je die Anderen zum Einsatz gekommen sind. Naja, es schadet jedenfalls nicht, wenn sie da sind. | ||
Zeile 303: | Zeile 303: | ||
Um nun mit dem Licht weiter machen zu können, müssen wir die Intersect-Funktionen für Kugeln und Ebenen um Normalenerzeugung erweitern. Wir dürfen nicht vergessen, diese dann in den World-Space zu transformieren: | Um nun mit dem Licht weiter machen zu können, müssen wir die Intersect-Funktionen für Kugeln und Ebenen um Normalenerzeugung erweitern. Wir dürfen nicht vergessen, diese dann in den World-Space zu transformieren: | ||
− | <cpp> /// Calculates the World-Space normal for a surface point | + | <source lang="cpp"> /// Calculates the World-Space normal for a surface point |
protected Vertex3 CalcNormal(Vertex3 v) | protected Vertex3 CalcNormal(Vertex3 v) | ||
{ | { | ||
Zeile 350: | Zeile 350: | ||
return -1; | return -1; | ||
} | } | ||
− | </ | + | </source> |
== Phong Licht == | == Phong Licht == | ||
Zeile 360: | Zeile 360: | ||
jetzt den zugehörigen Farbwert über unser Material: | jetzt den zugehörigen Farbwert über unser Material: | ||
− | <cpp>public RaytraceImage ( int width , int height ) | + | <source lang="cpp">public RaytraceImage ( int width , int height ) |
{ | { | ||
for ( int x = 0; x < width; x++) { | for ( int x = 0; x < width; x++) { | ||
Zeile 383: | Zeile 383: | ||
} | } | ||
} | } | ||
− | </ | + | </source> |
== Abschluss == | == Abschluss == |
Version vom 10. März 2009, 19:48 Uhr
Inhaltsverzeichnis
Raytracing Grundlagen II
Einführung
Und weiter geht es mit unsem Raytracing Tutorial. Wir wollen unseren Raytracer nun langsam aber sicher soweit bekommen, daß sich damit etwas ansehnlichere Bilder erzeugen lassen. Wer das vorangehende Tutorial nicht gelesen hat, sollte dies am besten gleich tun - ich will ja nicht alles neu erklären. Womit werden wir uns denn heute beschäftigen? Zuerst werden wir ein erweiterbares Objektmodell für alle Objekte planen und überlegen. Dann wollen wir uns ein wenig mit Phong-Lighting beschäftigen und einige Gedanken in Richtung Transformationen verschwenden. Mit diesem Plan sind wir, so denke ich, erstmal gut beschäftigt.
Ein schönes Basisobjekt
Wir wollen die Objekte in unsem Raytracer möglichst gleich behandeln können, so daß der eigentliche Raytracing Algorithmus ohne jegliches wissen über die Struktur der Objekte arbeiten kann. Sprich: Wir definieren ein Basisobjekt, von dem anschließend alle weiteren Objekte abgeleitet werden. Was also soll dieses Objekt an Methoden und Eigenschaften zur Verfügung stellen? Jedes Objekt hat sicherlich eine Position, sowie gewissen Materialeigenschaften, die dem Objekt zugewiesen werden. Auch soll das Objekt sauber mit Transformationen umgehen können. Und was natürlich ganz wichtig ist: Wir brauchen eine Methode die prüft, ob sich das Objekt mit dem Strahl schneidet. Über einige dieser Dinge haben wir uns noch keine Gedanken gemacht, entsprechend sollen hierfür nur leere Dummyklassen und Methoden zum Einsatz kommen, die wir erst später realisieren. Damit es nicht vergessen wird: Es ist auch sinnvoll Vater-Kindbeziehungen zu haben um später zusammengesetzte Objekte definieren zu können. Das klingt alles etwas wild, artet in einer großen Menge Code aus, ist aber eigentlich gar nicht so schlimm, wei es sich anhört:
/// General Raytracing object in a tracer. All traceable objects must be derivates
public abstract class ObjectBase
{
#region Positioning Variables
private Vertex3 pos = new Vertex3();
/// The Position of the Object in parental Space
public Vertex3 Position
{
get { return pos; }
set { pos = value; /*InvalidateBoundings(true);*/ }
// Anm.: InvalidateBoundings brauchen wir jetzt noch
// nicht. Wenn man allerdings mit Bounding-Volumes
// zu arbeiten beginnt, wird man entsprechende
// Funktionalität benötigen. Vorerst gilt: Forget it.
}
private ILinearTransformation ptransformation;
/// The Transformation applied to the object
public ILinearTransformation Transformation
{
get
{
return ptransformation;
}
set
{
ptransformation = value;
//InvalidateBoundigs(true);
}
}
#endregion
#region Material
private Material objMaterial = new StandardMaterial();
/// Material assigned to object
public Material ObjectMaterial
{
...
}
#endregion
#region Parenting
// The parental object
private ObjectBase fParent;
/// The Parent of the object
public ObjectBase Parent
{
...
}
#endregion
#region Trans Space Functions
public virtual Vertex3 TransSpaceWorldToObject(Vertex3 v) {...}
public virtual Ray TransSpaceWorldToObject(Ray r) {...}
public virtual Vertex3 TransSpacePointWorldToObject(Vertex3 v) {...}
public virtual Vertex3 TransSpacePointParentToObject(Vertex3 v) {...}
public virtual Ray TransSpaceParentToObject(Ray r)
{
Ray ray = r;
ray.o -= pos;
return ray;
}
public virtual Vertex3 TransSpaceParentToObject(Vertex3 v) {...}
public virtual Vertex3 TransSpaceObjectToWorld(Vertex3 v) {...}
public virtual Vertex3 TransSpacePointObjectToWorld(Vertex3 v) {...}
#endregion
#region Intersection Tests
/// Calculates the first visible intersection. Returns values smaller
/// equal zero if there is no intersection.
/// param name="rs": Global rendersettings
/// param name="ray": the ray to test for intersection
/// param name="raymindist":
/// param name="raymaxdist":
/// param name="calcNormals":
/// param name="calcTextureCoordinates":
/// param name="info":
public abstract int Intersect(RenderSettings rs, Ray ray, double raymindist, double raymaxdist,
bool calcNormals, bool calcTextureCoordinates, ref IntersectionInfo info);
#endregion
}
Das schaut jetzt tatsächlich etwas wild aus, aber keine Angst. Wir werden das alles der Reihe nach mit Leben füllen. Die Trans-Space-Funktionen werden wir anschließend mit Leben füllen - nur TransSpaceParentToObject kann gleich bestückt werden (wir werden das aber später erweitern müssen). Sie soll Strahlen aus dem Eltern-Raum in den lokalen Raum überführen - für die meisten Objekte bedeutet dies: Die globalen Koordinaten werden in lokale übersetzt, so daß sich unser Objekt genau im Zentrum des Geschehens, d.h. bei (0,0,0), befindet und wir uns dann nie wieder über die Position des Objektes Gedanken machen müssen.
Einen Blick sind auch die Parameter von Intersect wert. rs beschreibt globale Einstellungen des Renderers. Darin könnte man z.B. festlegen, ob Schatten berechnet werden sollen oder nicht. Ihr könnt hier im wesentlichen spazieren führen was ihr wollt, aber diese Option zu haben ist jedenfalls nicht verkehrt. ray dürfte klar sein. Dies wird ein Strahl sein, der jeweils in elterlichen Koordinaten gegeben ist, also mithilfe von TransSpaceParentToObject in lokale Koordinaten umgerechnet werden kann. raymindist und raymaxdist beschreiben jeweils Bereich des Strahles, in dem wir nach Überschneidungen suchen sollen - haben wir es mit einem numerisch zu berechnenden Objekt zu tun, kann diese Information bereits sehr viel Geschwindigkeit ausmachen. Beim Schnitt mit Kugeln ist die Sache dagegen weniger wichtig. Essenziell ist, daß beim Aufruf von Intersect sichergestellt sein muss, daß nur dann positive Rückmeldung gemacht wird, wenn die Überschneidung an einer Stelle des Strahles zu finden ist, die größer gleich raymindist ist: Darauf sollte man sich verlassen können. calcNormals und calcTextureCoordinates beschreibt, ob beim Überschneidungstest Informationen über Flächennormalen bzw. Texturkoordinaten erzeugt werden sollen: Bei manchen Objekten ist auch diese Information nur kostspielig zu bekommen und wir wollen es deshalb optional halten. Es bleibt noch der Call by Reference Parameter info. Darin sollen genauere Informationen bezüglich des Schnittes gespeichert werden - etwa lokale Farben, Material, Texturkoordinaten, Schnittposition, etc. (Die Schnittposition könnte man auch direkt zurückgeben, also den Rückgabewert durch einen Float-Typ wie double ersetzen. Seis drum)
Noch schnell ein kleiner Blick in eine mögliche Definition der IntersectionInfo. Darin befindet sich nichts anderes, als wie das, was ich gerade beschrieben habe. Also keine Überraschungen ;-)
public struct IntersectionInfo
{
/// Position on ray where the cut occured
public double rayPosition;
/// Normal of surface at cut position
public Vertex3 normal;
/// Texture coordinates
public Vertex3 texture;
/// Material of surface at intersection position
public Material material;
/// The local color of the intersection. This must be optional
/// for all materials!!! Some Objects may always return black here.
public Color localColor;
}
Transformationen
Wir wollen uns nun um Transformationen kümmern. Was also sind Transformationen? Wer mit OpenGl vertraut ist, kennt sie sie ganz gut: Es handelt sich um genau das, was man mit den Funktionen glTranslate, glRotate, glScale, usw. bewirken kann. Verschiebungen haben wir bereits durch die Position des Objektes abgehakt, wir können uns hier also auf Dinge wie Scheerungen, Rotationen usw. beschränken. Wer Nachholbedarf in diesen Themen hat, der sollte sich jetzt hier darum kümmern, dieses Defizit etwas zu mildern. Da DelphiGl aber schon eine Weile existiert, finden sich fast alle nötigen Informationen in Tutorias oder Wiki-Artikeln: Matrix, Nachsitzen, Matrix 2, Quaternion. Auch die Wikipedia ist hier in der Kategorie Lineare Algebra gut ausgestattet, um weitere Informationen einzuholen. Wer hoch in den Code des Basis-Objektes schaut, dem wird auffallen, daß Transformationen durch das Interface ILinearTransformation definiert sind:
/// Defines a linear Trafo
public interface ILinearTransformation
{
/// Applys the Trafo to v
Vertex3 Apply(Vertex3 v);
/// Applys the inverse Trafo to v
Vertex3 ApplyInverse(Vertex3 v);
/// Conversion of the Trafo to Matrix3x3
Matrix3x3 ConvertTo3x3();
/// Conversion of the Inverse Trafo to Matrix3x3
Matrix3x3 ConvertInverseTo3x3();
/// Right Mulltiplication of factor with this to a new, returned ILinearTransformation.
/// Probably the trafos are converted to a LinearTransformationMatrix, but if both
/// trafos are of the same type, its a good idea to multiply them together.
ILinearTransformation MultiplyWithLinear(ILinearTransformation factor);
}
So kann man ohne weiteres Quaternionen und Matrizen wild nebeneinander verwenden. Mit der Methode MultiplyWithLinear kann man dann auch verschiedene Multiplikationen miteinander verknüpfen. So kann man Quaternionen einfach mit Quaternionen multiplizieren und gemischtes, wenn etwa Quaternionen mit Matrizen multipliziert werden, in Matrizen umwandeln:
#region ILinearTransformation Members
/// Rotates a Vertex3 through the Quaternion - this should be normalized!
public Vertex3 Apply(Vertex3 v) {...}
/// Rotates a Vertex3 against the Quaternion. A normalized Quaternion is assumed!
public Vertex3 ApplyInverse(Vertex3 v) {...}
/// Converts the Quaternion to a 3x3 Matrix
public Matrix3x3 ConvertTo3x3()
{
Matrix3x3 result = new Matrix3x3();
result.sp0 = Apply(new Vertex3(1.0, 0.0, 0.0));
result.sp1 = Apply(new Vertex3(0.0, 1.0, 0.0));
result.sp2 = Apply(new Vertex3(0.0, 0.0, 1.0));
return result;
}
/// Converts the Inverse Quaternion to a 3x3 Matrix
public Matrix3x3 ConvertInverseTo3x3()
{
Matrix3x3 result = new Matrix3x3();
result.sp0 = ApplyInverse(new Vertex3(1.0, 0.0, 0.0));
result.sp1 = ApplyInverse(new Vertex3(0.0, 1.0, 0.0));
result.sp2 = ApplyInverse(new Vertex3(0.0, 0.0, 1.0));
return result;
}
/// Multiply the Quaternion with another trafo. If its a Quaternion, this and factor
/// will be multiplied together, otherwise a LinearTransformationMatrix is generated.
public ILinearTransformation MultiplyWithLinear(ILinearTransformation factor)
{
if (factor == null) return this;
Quaternion fac = factor as Quaternion;
if (fac != null)
{
return Multiply(fac);
}
else
{
// factor is not wenn known to us, we need a new LinearTransformationMatrix
Matrix3x3 lin = ConvertTo3x3() * factor.ConvertTo3x3();
Matrix3x3 inv = factor.ConvertInverseTo3x3() * ConvertInverseTo3x3();
return new LinearTransformationMatrix(lin, inv, factor.Operator2Norm() * Operator2Norm());
}
}
#endregion
An diesem Beispiel aus meinem Quaternioncode kann man folgendes sehen: Die Funktionen ConvertTo3x3 und ConvertInverseTo3x3 berechnen jeweils die zum Quaternion gehörenden, äquivalenten Matrizen ( Man erinnere sich an den Satz, den ich bereits im Nachhilfe Tutorial zitiert habe: Die Spalten spi der Matrix sind die Bilder der Einheitsvektoren. Dann dürfte sofort klar sein, was hier passiert ). In MultiplyWithLinear wird dann einfach geprüft, ob die Transformation mit der rechts multipliziert wird, ein Quaternion ist. Dann wird einfach Quaternion-Multiplikation durchgeführt, ansonsten werden sowohl der Faktor als auch das betrachtete Quaternion in 3x3 Matrizen umgewandelt und in einer LinearTransformationMatrix abgelegt - welches natürlich ILinearTransformation implementiert und immer die Transformationsmatrix und ihr Inverses bereithält.
Damit ihr nicht ewig rumrechnen müsst (es handelt sich nämlich um eine schrecklich langwierige und ekelige Rechnerei: Ich habe etwa 2 Stunden gebraucht, bis das Ergebnis endlich fehlerfrei und brauchbar auf 2 DIN-A4 Blättern verteilt da stand), wenn ihr Quaternionen implementiert noch ein paar kleine Methoden des Quaternions. Den Rest werdet ihr dann wohl aus dem Quaternionen-Artikel selbst in kurzer Zeit zusammenstöpseln können.
// "Coordinates" of the Quaternion.
// coords[0] is the real component, coords[1] the i comp., ...
private double[] coords = new double[4];
/// Creates a rotation Quaternion
public Quaternion(double alpha, Vertex3 rotAxis)
{
alpha = (2.0*Math.PI/(360.0*2.0))* alpha;
coords[0] = 1.0;
if (rotAxis.Magnitude < double.Epsilon)
{
return;
}
rotAxis.Normalize();
double c, s;
c = Math.Cos(alpha); s = Math.Sin(alpha);
coords[0] = c; coords[1] = rotAxis[0] * s;
coords[2] = rotAxis[1] * s; coords[3] = rotAxis[2] * s;
}
/// Rotates a Vertex3 through the Quaternion - this should be normalized!
public Vertex3 Apply(Vertex3 v)
{
Vertex3 result = new Vertex3();
double v0 = v[0]; double v1 = v[1]; double v2 = v[2];
double a00 = coords[0] * coords[0];
double a01 = coords[0] * coords[1];
double a02 = coords[0] * coords[2];
double a03 = coords[0] * coords[3];
double a11 = coords[1] * coords[1];
double a12 = coords[1] * coords[2];
double a13 = coords[1] * coords[3];
double a22 = coords[2] * coords[2];
double a23 = coords[2] * coords[3];
double a33 = coords[3] * coords[3];
result[0] = v0 * (+a00 + a11 - a22 - a33)
+ 2 * (a12 * v1 + a13 * v2 + a02 * v2 - a03 * v1);
result[1] = v1 * (+a00 - a11 + a22 - a33)
+ 2 * (a12 * v0 + a23 * v2 + a03 * v0 - a01 * v2);
result[2] = v2 * (+a00 - a11 - a22 + a33)
+ 2 * (a13 * v0 + a23 * v1 - a02 * v0 + a01 * v1);
return result;
}
/// Rotates a Vertex3 against the Quaternion. A normalized Quaternion is assumed!
public Vertex3 ApplyInverse(Vertex3 v)
{
Vertex3 result = new Vertex3();
double v0 = v[0];
double v1 = v[1];
double v2 = v[2];
// Like Apply. We only konjugate the im. factors
double a00 = coords[0] * coords[0];
double a01 = -coords[0] * coords[1];
double a02 = -coords[0] * coords[2];
double a03 = -coords[0] * coords[3];
double a11 = +coords[1] * coords[1];
double a12 = +coords[1] * coords[2];
double a13 = +coords[1] * coords[3];
double a22 = +coords[2] * coords[2];
double a23 = +coords[2] * coords[3];
double a33 = +coords[3] * coords[3];
result[0] = v0 * (+a00 + a11 - a22 - a33)
+ 2 * (a12 * v1 + a13 * v2 + a02 * v2 - a03 * v1);
result[1] = v1 * (+a00 - a11 + a22 - a33)
+ 2 * (a12 * v0 + a23 * v2 + a03 * v0 - a01 * v2);
result[2] = v2 * (+a00 - a11 - a22 + a33)
+ 2 * (a13 * v0 + a23 * v1 - a02 * v0 + a01 * v1);
return result;
}
Wer ein Quaternion mithilfe des angegebenen Konstruktors erstellt, erhält ein Rotationsquaternion der Norm 1. Genau das, was für die Funktionen Apply und ApplyInverse notwendig ist, damit sie das tun, was sie sollen. Damit soll ersteinmal der Faulheit beim Rechnen und dem Copy&Paste genüge getan sein. Und wehe ich entdecke irgendwann Raytracer, die Objekte nur rotieren können statt beliebige Matrizen zu vertragen ;-) Am Ende bleibt es natürlich euch überlassen. Wenn ihr wollt, könnt ihr all das auch allein durch Matrizen implementieren und euch den Umweg über das Quaternion ersparen - allerdings sieht man einem Quaternion schneller an was es bewirkt, als einer Matrix.
Nachdem wir einen kleinen Ausflug in Richtung Quaternionen und Transformationen hinter uns gebracht haben, könnte man hoffen, daß wir jetzt endlich mit dem abstrakten Kram aufhören und wir bald mal wieder ein paar neue Bilder zu Gesicht bekommen. Soweit ist es leider noch nicht. Ich möchte zwischendrin den Schlachtplan für die nächsten Minuten bekannt geben: Wir werden jetzt die Punkte in den Trans-Space-Funktionen der Basisklasse füllen, um uns dann dem Licht durch Phong-Lighting über den Umweg der Normalen zu widmen. Beginnen wir also mit der Funktion TransSpaceParentToObject, welche Strahlen des elterlichen Raumes in den lokalen Raum transformieren soll. Wenn man nun an die Kameraanalogie denkt, wie sie etwa im Kamera Tutorial beschrieben ist, dann wissen wir, daß wir die auf das Objekt angewendete Transformation einfach nur umkehren müssen. Denkt man dann noch daran, daß wir erreichen wollen, daß das Objekt im lokalen Raum am Ursprung befinden soll, ist die Implementation schnell naheliegend: Den Ursprung des Strahles müssen wir erst um die Position verschieben und dann invers transformieren. Bei der Richtung genügt es, diese invers zu transformieren.
public virtual Ray TransSpaceParentToObject(Ray r)
{
Ray ray = r;
ray.Origin -= pos;
if (ptransformation != null)
{
ray.Origin = ptransformation.ApplyInverse(ray.Origin);
ray.Direction = ptransformation.ApplyInverse(ray.Direction);
}
return ray;
}
Bei dieser Überlegung fallen auch gleich die Funktionen TransSpacePointParentToObject, welche einen Punkt in die lokalen Koordinaten überträgt und die Entsprechende Funktion TransSpaceParentToObject für Richtungen ab. Das ganze können wir natürlich auch in die andere Richtung machen: Bislang ist mir aber noch kein Grund eingefallen, dies zu tun. Wichtig dagegen ist es, Richtungen in Weltkoordinaten umzurechnen, da wir dies für die Normalen später benötigen werden. Dies erreichen wir leicht, wenn wir ausnutzen, daß Objekte immer ihre Eltern kennen:
public virtual Vertex3 TransSpaceObjectToWorld(Vertex3 v)
{
if (ptransformation != null)
{
v = ptransformation.Apply(v);
}
if (fParent != null)
{
v = fParent.TransSpaceObjectToWorld(v);
}
return v;
}
Die entsprechende Funktion für Punkte solltet ihr der Übung halber selbst schreiben. Mit den genannten Zweien kommen wir auf jeden Fall erst einmal aus - ich bin mir gar nicht sicher, ob in meinem eigenen Raytracer je die Anderen zum Einsatz gekommen sind. Naja, es schadet jedenfalls nicht, wenn sie da sind.
Um nun mit dem Licht weiter machen zu können, müssen wir die Intersect-Funktionen für Kugeln und Ebenen um Normalenerzeugung erweitern. Wir dürfen nicht vergessen, diese dann in den World-Space zu transformieren:
/// Calculates the World-Space normal for a surface point
protected Vertex3 CalcNormal(Vertex3 v)
{
Vertex3 normal = TransSpaceObjectToWorld(v);
normal.Normalize();
return normal;
}
...
public override int Intersect(RenderSettings rs, Ray ray,
double raymindist, double raymaxdist, bool calcNormals,
bool calcTextureCoordinates, ref IntersectionInfo info)
{
Ray osray = TransSpaceParentToObject(ray);
double a, b, c;
a = osray.d.DotDot; b = 2 * (osray.o * osray.d);
c = osray.o.DotDot - sphereRadius * sphereRadius;
double t1, t2;
int roots = GeoMath.CalcQuadricRoots(a, b, c, out t1, out t2);
// roots are sorted so check only for tr2
if (roots > 0 && t2 >= raymindist && t1 <= raymaxdist)
{
info = new IntersectionInfo();
Vertex3 v; info.material = ObjectMaterial;
info.rayPosition = t1;
if (t1 >= raymindist)
{
v = osray.Evaluate(t1);
if (calcNormals) info.normal = CalcNormal(v);
if (calcTextureCoordinates)
info.texture = GeoMath.SphereCoordinates(v);
return 1;
}
v = osray.Evaluate(t2);
if (calcNormals) info.normal = CalcNormal(v);
if (calcTextureCoordinates)
info.texture = GeoMath.SphereCoordinates(v);
info.rayPosition = t2;
return 1;
}
else
return -1;
}
Phong Licht
Wir sind soweit. Die ersten hübscheren Bilder aus unserem Raytracer sind greifbar nahe. Wir müssen uns im wesentlichen nur noch mit Phong Licht beschäftigen, um aus unserem Raytracer mehr herauszuquetschen. Das Phong-Beleuchtungsmodell ist ein sehr einfaches Modell, um Licht zu simulieren. Es ist physikalisch ziemlich falsch: Durch Hinzufügen von Objekten in einen physikalischen Raum, die sich lichttechnisch nach dem Phong Modell verhalten, wird i.A. die vorhandene Lichtenergie erhöht. Das ist sehr tragisch, wenn man globale Beleuchtungsmethoden verwendet, aber fürs erste ist es ein sehr gut aussehendes und leicht zu berechnendes Modell. Wir wollen uns also von dieser Anomalität nicht beunruhigen lassen und uns nur daran erinnern, wenn wir einmal globale Beleuchtungsmethoden einführen wollen. Jedem sollte das Phong Modell wohl vertraut sein, da es das Standard-Modell in OpenGl ist. Wer hier noch Nachholbedarf hat, schaue doch einmal im Beleuchtungs-Artikel oder in Tom Nuydens': Phong For Dummies.
Jedenfalls, wenn wir jedem Objekt ein Material mit den Koeffizienten aus dem Phong-Modell zugeordnet haben, dann können wir mithilfe einer IntersectionInfo für Strahl/Objekt-Schnittpunkte und aller Lichtquellen leicht die zugehörige Farbe bestimmen. An der Stelle, wo wir also im Tiefentracer den Grauwert bestimmten, bestimmen wir jetzt den zugehörigen Farbwert über unser Material:
public RaytraceImage ( int width , int height )
{
for ( int x = 0; x < width; x++) {
for ( int y = 0 ; y < height; y++) {
Ray shoot = ShootRay (x , y);
double maxdist = double.Infinity;
IntersectionInfo cinfo = new IntersectionInfo();;
IntersectionInfo iinfo = new IntersectionInfo();
foreach (ObjectBase obj in SichtbareObjekte) {
if (obj.Intersect(rs, shoot, 0, maxdist, true, true, ref cinfo) > 0
&& cinfo.rayPosition < maxdist) iinfo = cinfo;
}
if (maxdist < double.Infinity && iinfo.material != null) {
SetColor(x, y, iinfo.Material.GetColor(iinfo));
}
else
SetColor(x, y, BackgroundColor);
}
}
}
Abschluss
Da habt ihr euch ja durch ein dickes Ding gequält ( besonders wenn ihr im Bereich Transformationen oder Phong noch Nachholbedarf hattet, hats sicher eine Weile gedauert ). Gratulation. Langsam wird das Raytracing ja interessant und die Szenen sind nicht mehr ganz so langweilig. Es bleibt aber immer noch genügend Stoff für ein weiteres Grundlagen Tutorial: In Planung sind hier ein paar Worte bezüglich Reflexionen, Schatten und ein paar weiteren Objekten wie Dreiecken (das impliziert eine Lehrstunde in Sachen Phong Shading), Boxen und Zylinder. Dazu sollten wir ausserdem unseren Raytracer umbauen: Er wird in eine eigene Tracer-Klasse ausgelagert werden, um so schnell die Wahl zwischen verschiedenen Raytracern zu haben. Stay tuned.