Code-Organisation bei Software-Rendererern (Ray Tracer/Rasterizer)



  • Wie würdet ihr generell den Code organisieren beim

    • Ray Tracing (Bilderzeugung durch Strahlenverfolgung)

    • Rasterung (Bilderzeugung durch perspektivische oder zentrale Projektion)

    Was ich am meisten schwer finde, ist nicht Mathematik oder so, sondern "Vereinfachung" und "Organisation" der Codekomponenten.
    Also wie finde ich am besten ein gutes schönes Design, sodass alles wie "geschmiert" läuft und sauber strukturiert ist.

    Mir sind Prinzipien wie SOLID bekannt.
    Jedoch habe ich noch Probleme beim umschreiben/refactoren der Komponenten, da ich mir noch ziemlich unschlüssig bin, wie ich das alles Organisieren soll.

    Beim Ray Tracer bin ich mir zum Beispiel nicht sicher, wie ich den "Vertexprozessor" ("vertex shader") und "Pixelprozessor" ("pixel shader") nennen soll.

    Die Sachen bei der Rasterung werden nach der Transformation der Vertexpositionen (also 3D Punkte im Raum) ja "Fragmente" genannt (also Sachen, die bereit sind vom Rasterizer sich rendern zu lassen.).

    Wie heißen diese Beiden bei Ray Tracing?
    Hitprozessor und Hitpixelprozessor?
    Wie könnte ich das sauber strukturieren?
    Wie könnte ich das mit dem Rasterizer sauber vereinen?

    Als Grundlage dient mir bei der Rasterung und der Strahlenverfolgung, die Ebenengleichung in 3D:

    Bei Rasterng muss man einfach nur gucken, dass das gilt:

    p = a + β·(b - a) + γ·(c - a)
    Ebenengleichung aufstellen.

    p = (1 - β - γ)·a + β·b + γ·c

    Umstellen der Gleichung und auflösen nach Beta und Gamma.
    Dann gucken, dass

    β, γ >= 0 and β + γ <= 1

    sind.

    Bein der Strahlenverfolgung muss man einfach nur das P mit der Liniengleichung in 3D vereinen:

    p = a + β · (b - a) + γ · (c - a)

    p = t · d + o

    Dann p in die Ebenengleichung einsetzen:

    t· d + o = a + β · (b - a) + γ · (c - a)

    Umstellen.

    o - a = β · (b - a) + γ · (c - a) - t · d

    Und nach Beta, Gamma, und t auflösen.

    Und dann schauen, dass

    β, γ >= 0 und β + γ = 1.

    gilt.

    Bei t kann man frei wählen bis wohin in die Tiefe reingegangen werden soll:

    t im Intervall von [1, 100] zum Beispiel.

    So viel zur Mathematik.

    Bei meinem C-Projekt ein PS1-Software-Rasterizer habe ich so etwas:

    typedef void DrawPixelFunction(int x, int y);
    typedef void ComputeColorFunction(long alpha, long beta, long gamma,
                                      signed int shift_amount,
                                      unsigned char (*colors)[3], void* data);
    typedef void DrawPixelWithColorFunction(int x, int y, unsigned char red,
    
    
    /*
       Assume counter-clockwise orientation.
       The function does not draw triangles with clockwise orientation.
    */
    void RasterizeTriangle(int ax, int ay, int bx, int by, int cx, int cy,
                           ComputeColorFunction* compute_color,
                           DrawPixelWithColorFunction* draw_pixel, void* optional);
    

    Also zwei Funktionen bzw. Funktionszeiger, wo mir die Farbe berechnen (ist das schon ein "Pixelshader"?) und halt eine "zeichne mir einen Pixel-Funktion".

    Tief im inneren des Rasterizers sieht das dann so aus:

    
            /* Draw pixel. */
            {
              unsigned char colors[3] = {0, 0, 0};
              unsigned char* color = colors;
              compute_color(kAlphaScaled, kBetaScaled, kGammaScaled,
                            kFractionalBits, &colors, data);
              draw_pixel(x, y, color[0], color[1], color[2]);
            }
    

    Der Rasterizer arbeitet mit Festkommazahlen, also muss ich bei der Rückumwandlung auf Float zurueckskalieren ...

    ComputeColor berechnet mir die Texturpositionen und holt mir die Pixel aus der Textur raus.
    Ausserdem werden auch hier Beleuchtungsrechnungen wie "flat shading" (nach Lamberts Beleuchtungsmodell berechnet).

    Wie würde so eine Pipeline für Ray-Tracing aussehen?
    Pseudocode bzw. ein Ansatz reicht mir. 🙂

    PS: Werde mir auch nochmal XMAMan's Tutorial zu RayTracing lesen zur Rekapitulation. 🙂



  • Hallo Dozgon,

    bei der Rasterung ist die Grundidee: Lade eine Menge von Dreiecken und Texturen in den Speicher, lege fest welche Shader verwendet werden sollen und parametrisiere die Shader und anderen Bauteile der Grafikpipeline. Dann rufe nur noch den Zeichenbefehl auf um das Dreiecksarray zu zeichnen. Dann wird das Array mit dem Painter-Algorithmus gemalt (Schleife geht über alle Dreiecke).

    Beim Raytracing iteriert man nicht über eine gegebene Menge von Objekten/Fragmenten/Dreiecken/Linien sondern man iteriert über alle Pixel und bestimmt dessen Farbwert, indem man schaut, welches Objekt durch diesen Pixel betrachtet wird. Es gibt zwei Grundsätze beim Raytracing: Strahlen von der Kamera aus erzeugen und dann durch die Scene verfolgen (Pathtracing) und Strahlen von der Lichtquelle erzeugen und durch die Scene verfolgen (Lighttracing).

    Vom Design her würde ich mich beim Rasterizer an der Grafikpipeline(https://de.wikipedia.org/wiki/Grafikpipeline#/media/Datei:3D-Pipeline.png / http://www.songho.ca/opengl/gl_transform.html) orientieren.

    D.h. du hast erstmal Klassen zum Speichern von Texturen und Dreiecken. Mit 4*4-Matrizen (Model * Kamera * Projektion) transformierst du alle Dreiecke vom Objektspace in Clippingkoordinaten. Das macht der Vertexshader, welcher eine Callbackfunktion ist, um ihn frei austauschbar zu machen. Für das Clipping hast du dann eine eigene Klasse, um aus ein Dreieck dann eine Liste von Dreiecken zu machen. Dann die Division durch W für jeden Vertex. Für die Viewporttransformation hast du auch eine eigene Klasse, um die Dreiecke in 2D-Dreiecke zu bringen, welche in Pixelkoordinaten vorliegen.

    Dann hast du eine Triangle-Raster-Klasse, welche als Input ein 2D-Dreieck bekommt und was dann für jedes Pixel, was innerhalb vom Dreieck liegt ein Callback-Handler aufruft. Innerhalb von diesen Callbackhandler wird dann erst der PixelShader um die Farbe zu bestimmen. Dann noch das Zeichnen in den Farb- und Tiefenpuffer.

    Du brauchst also mindestens die Klassen: ColorTexture, DepthTexture, Buffer (Enthält eine Farb- und Tiefentextur), TriangleRasterizer, VertexShader, PixelShader, Matrix4x4, Vertex, Vector3D, Clipping.

    Beim Raytracer konvertiert man alle Dreiecke/Objekte auch erst vom Objektspace in den World- oder Kamera-Space. Man könnte das dort auch Vertexshader nennen, wenn man das will. Aber ab da kannst du es nicht mehr mit einer Grafikpipeline vergleichen. Du schickst ja keine Dreiecke durch eine Pipeline, wo am Ende dann ein Farbpuffer wartet sondern du ermittelst für jeden Pixel den Schnittpunkt mit dem ersten Dreieck.

    Die Gemeinsamkeit zwischen Rasterizer und Raytracer ist also, dass beide eine Menge von Dreiecken, Lichtquellen und eine Kamera als Input bekommen und beide ein Bild erzeugen. Der Teil dazwischen muss dann unterschiedlich implementiert werden.


Anmelden zum Antworten