Raycasting front-to-back



  • Hallo,

    ich beschäftige mich grade mit Volumenrendering. Ich habe zunächst mal einen GLSL-Shader programmiert, der back-to-front-Raycasting ausführt, was geklappt hat. Mit der front-to-back-Variante hab ich allerdings das Problem, dass nur das erste Slice des Volumens dargestellt wird; Bereiche die eigentlich transparent sind, sind schwarz, anstatt dass tiefer im Volumen liegende Bereiche dargestellt sind.

    Hier meine Funktion:

    vec4 trace_front_to_back(sampler3D volume
                             , vec2 coord
                             , int len
                             , sampler1D opac_lut
                             , sampler1D color_lut
                             , int step_size)
    {
        vec4 accum = vec4(0, 0, 0, 0);
        vec4 accum_transparency = vec4(1, 1, 1, 1);
        int steps = len-1;
    
        const int max_traversal = 200;
        for (int i=0; i<max_traversal; ++i) {
    
            // get the current voxel value
            vec4 voxel_curr
                    = texture3D(textures[0]
                        , vec3(coord.x, coord.y, steps/(len*1.0)));
            float curr_voxel_val = voxel_curr.r;
    
            // composition
            voxel_curr *= texture1D(color_tf, curr_voxel_val);
            vec4 current_transparency = texture1D(opac_tf, curr_voxel_val);
            accum += (accum_transparency) * voxel_curr;
    
            accum_transparency -= (1.0- accum_transparency*current_transparency);
    
            //traverse on the ray
            steps -= step_size;
            if (steps <= 0) {
                break;
            }
        }
        return clamp(accum, 0, 1);
    }
    

    Anmerkung: Für die Transparenzwerte gilt dass 1 vollständig transparent ist, 0 ist vollständig opak.



  • Hi!

    Ich habe den Verdacht, dass du eventuell die Transparenz nicht ganz korrekt aufsummierst
    (ich muss zugeben, dass ich das etwas verwirrend fand, ich selbst hätte intuitiv mit Opazität gearbeitet,
    also 0=komplett durchsichtig, dann werden auch einige Berechnungen etwas simpler aufzuschreiben).

    Ich beziehe mich auf diese Zeile hier:

    accum_transparency -= (1.0- accum_transparency*current_transparency);
    

    Angenommen, du durchläufst in der Schleife nacheinander 2 halb-transparente Voxel (zweimal current_transparency = 0.5 hintereinander).
    Ich würde dann erwarten, dass danach die akkumulierte Transparenz 0.75 ist (habe ich das richtig verstanden?). Wenn ich das aber jetzt manuell ausrechne:

    // accum_transparency = accum_transparency - (1.0-accum_transparency*current_transparency); 
    
    // Schleife initialisieren
    accum_transparency = 1.0
    ...
    // 1. Durchlauf, accum_transparency = 1.0, current_transparency = 0.5
    ...
    accum_transparency = 1.0 - (1.0 - 1.0 * 0.5) = 1.0 - 0.5 = 0.5
    ...
    // 2. Durchlauf, accum_transparency = 0.5, current_transparency = 0.5
    ...
    accum_transparency = 0.5 - (1.0 - 0.5 * 0.5) = 0.5 - 0.75 = -0.25
    

    Ich vermute dass das dein Problem ist. accum_transparency wird negativ, obwohl es eigentlich (von Rundungsfehlern abgesehen) immer zwischen 0 und 1 liegen sollte.
    Daher wird wahrscheinlich dann irgendwann auch dein accum negativ, was nach dem clamp() dann zu 0 wird.

    Finnegan



  • Hi,

    danke schonmal für den Hinweis. Wenn ich zwei halbtransparente Voxel hintereinander habe müsste die akkumulierte Transparenz ja 0,25 betragen, da das erste Voxel die Hälfte der "Intesität" absorbiert und von dieser Hälfte das zweite Voxel ebenfalls die Hälfte der Intensität absorbiert. Kann aber auch sein, dass ich da einen Denkfehler habe.

    Dein Hinweis ist richtig, habe die Zeile auf

    accum_transparency = accum_transparency * current_transparency;
    

    aktualisiert, was komischerweise genau das gleiche Bild liefert.



  • mortified_penguin schrieb:

    Wenn ich zwei halbtransparente Voxel hintereinander habe müsste die akkumulierte Transparenz ja 0,25 betragen,

    Ja, 0.25 sollte richtig sein. Mein Fehler. Muss die ganze Zeit gegen meine Intuition kämpfen, die von Opazität statt Transparenz ausgeht.
    Negativ ist natürlich trotzdem falsch 😃

    In dem Sinne ist "Transparenz" eigentlich auch die richtige Wahl, da es sich nicht um die Transparenz des aktuellen Voxel handelt,
    sondern um den Intensitätsanteil des aktuellen Voxel der von den Voxeln die auf dem Strahl "davor" liegen durchgelassen wird.

    Eine andere Sache die mir noch aufgefallen ist: Bist du sicher, dass du das Voxel-Volumen in der richtigen Richtung durchläufst?
    So wie ich das sehe hast du da noch keine View-Projektion oder sowas drin, du durchläufst das Volumen einfach in Richtung
    der Z-Achse und zwar von +Z (Länge des Strahls) nach 0 ... ist das richtig so?
    Sollte es nicht eher entweder von 0 bis (Länge des Strahls) oder, wenn du aus Richtung +Z auf das Volumen schaust,
    von (Tiefe des Volumens) bis (Tiefe des Volumens) - (Länge des Strahls) sein?:

    int steps = len-1;
    ...
            vec4 voxel_curr
                    = texture3D(textures[0]
                        , vec3(coord.x, coord.y, steps/(len*1.0)));
    ...
    steps -= step_size;
    

    Keine Ahnung ob das die Ursache ist, aber es sieht mir irgendwie falsch aus.

    Finnegan



  • Die Länge des Strahls ist ja gleich der Tiefe des Volumens, also sollte der Durchlauf von len bis 0 richtig sein. Wenn ich aber testweise von 0 bis len laufe, bekomme ich den gleichen Fehler den ich im OP beschrieben hab.



  • Mir gehen grad ein wenig die Ideen aus, daher ein paar allgemeine Fragen:

    Habe ich das richtig verstanden? Das ist exakt genau dasselbe Setup, das in der back-to-front-Variante korrekt funktioniert?
    Es gibt da noch ein paar Dinge, die mich etwas wundern:

    vec4 trace_front_to_back(sampler3D volume
    ...
                             , sampler1D opac_lut
                             , sampler1D color_lut
    ...
          vec4 voxel_curr
                    = texture3D(textures[0]
                        , vec3(coord.x, coord.y, steps/(len*1.0)));
    ...
            voxel_curr *= texture1D(color_tf, curr_voxel_val);
            vec4 current_transparency = texture1D(opac_tf, curr_voxel_val);
    }
    

    Müssten die ersten Argumente von texture* nicht die Sampler sein, die du in den Parametern von trace_front_to_back
    übergeben bekommst? Also:

    ...
          vec4 voxel_curr
                    = texture3D(volume
                        , vec3(coord.x, coord.y, steps/(len*1.0)));
    ...
            voxel_curr *= texture1D(color_lut, curr_voxel_val);
            vec4 current_transparency = texture1D(opac_lut, curr_voxel_val);
    ...
    

    Sind die _tf Variablen global? Sind die korrekt aufgesetzt und verweisen die auf die richtigen Texturen?

    Eine weitere Sache hier:

    voxel_curr *= texture1D(color_tf, curr_voxel_val);
    

    Du modulierst die Intensität des Voxels mit der (via Sampler interpolierten) Farbe für diesen Wert aus dem LUT.
    Ist das so gewollt, dass die LUT-Farbe mit der Voxel-Intensität skaliert wird?
    Ich hätte jetzt gedacht, dass dass im LUT lediglich ein "Mapping" Intensität->Farbe stehen würde.
    So wie es da steht haben z.B. niedrige Intensitäten immer eine "dunkle" Farbe, egal wie "hell" die Farbe im LUT
    für die Instensität definiert wurde.

    Ausserdem hast du im OP von "Slices" gesprochen. Wie machst du das Rendering selbst?
    Ich bin bisher davon ausgegangen dass du lediglich ein Quad renderst, bei dem dieser Shader aktiviert ist.
    Ist das korrekt? Falls du aus irgendeinem Grund das Volumen stückweise rendern solltest also ein Quad pro "Slice",
    die jeweils hintereinander liegen, könnte natürlich auch der Z-Buffer dein Problem sein, da dann nur das erste Quad
    gerendert wird und die dahinter liegenden nicht, da für diese dann der Z-Test fehlschlagen würde (wegen dem ersten Quad).

    Finnegan



  • Ja deine Punkte sind korrekt, das hab ich beim refactorn übersehen. Das ist das selbe Setup, mit dem back-to-front raycasting funktioniert.

    Ich render nur einen Quad auf den dann das Volumen projiziert wird.



  • So langsam gehen mir wirklich die Ideen aus 😃

    Renderst du das Quad eventuell mit aktiviertem Alpha-Blending? In diesem Fall würdest du z.B. bei voller Deckkraft (transparency=0) den Alphakanal des Voxel-Anteils des Ausgabe-Fragments ebenfalls mit 0 multiplizieren, was dann beim Rendering genau den umgekehrten Effekt wie bei deinem Raycasting hätte: Voxel-Anteile die du als voll transparent und somit schwarz bestimmt hättest würden dann mit maximaler Deckkraft einfließen während die opaken Anteile transparent wären..., das könnte evtl. den beschiebenen Effekt auslösen. Nur so eine Idee. Ich weiss nicht genau wie du das back-to-front implementiert hast, eventuell könnte sich da der Effekt eines zusätzlichen Alpha-Blendings nicht zeigen. Hast du vielleicht mal einen Screenshot zur Hand? Vielleicht wird daran deutlicher wo das Problem liegen könnte.

    Finnegan



  • Ich hab kein Alphablending aktiviert. Hier mal ein Bild, zur besseren Sichtbarkeit hab ich beim zweitletzten Slice angefangen. Lila ist die Haut, weiß der Knochen.
    http://fs2.directupload.net/images/150809/yqy79z7s.png



  • Eine kurze Info, was in dem Volumen abgebildet ist wäre noch fein gewesen :D. Ich vermute das ist ein Schädel?

    Sieht tatsächlich ein wenig so aus als wäre bereits die erste current_transparency die du dir für beliebige voxel_curr aus dem opac_lut -Sampler ziehst Null. D.h. auch die leere "Luft" scheint eine Transparenz von 0 zu haben, da ich davon ausgehe dass der Schädel in Z-Richtung breiter wird und dann der nicht-schwarze Bereich eigentlich größer sein müsste.

    Ich würde das mal schrittweise Debuggen und mir de Werte ansehen, die du da aus den verschiedenen Texturen rausziehst. Optimalerweise mit einem geeigneten GLSL-Debugger (kann leider keinen empfehlen) oder die klassische "print"-Methode, wobei "print" im Fall von GLSL natürlich das zurückgeben einer Farbe via return ist 😉

    Du könntest dir z.B. mal ganz simpel statt accum die erste current_transparency ausgeben, die du aus dem LUT ziehst. Die sollte ja zumindest in den Luft -Bereichen nicht 0 sein sondern eher Weiß (1).

    Die anderen Werte kannst du ähnlich debuggen und so zumindest grob feststellen ob die Werte stimmen. Da dein Shader-Modell ja offenbar Branching unterstützt kannst du natürlich auch noch präziser debuggen: wenn Bedingung zutrifft, dann gib "Grün" zurück, ansonsten "Rot" ... so solltest du dem Problem stückweise auf die Schliche kommen. Ich sehe nämlich jetzt so ad hoc nur durch Code-Starren nichts auffälliges mehr, sofern die ganzen Rahmenbedingungen okay sind (Texturen korrekt, Sampler richtig konfiguriert, Sättigungsarithmetik, Berücksichtigt dass gesampelte Werte gemäss Sampler-Konfiguration interpoliert sind, sinnvolle Behandlung von Rand-Pixeln der Texturen [CLAMP, REPEAT, feste Farbe, etc.] - beachten dass Pixel außerhalb der eigentlichen Textur [ausserhalb von [0,1] \times [0,1\]] via Interpolation in einen gesampelten Randpixel "hineinbluten" können, len und step_size haben sinnvolle Werte, etc. - vieles davon sollte aber schon beim back-to-front probleme gemacht haben).

    Finnegan



  • Was mich wundert ist, wenn ich die Schleife folgendermaßen verkleinere:

    for (int i=0; i<16; ++i) {
                vec4 voxel_curr
                        = texture3D(volume
                            , vec3(coord.x, coord.y, (1.0*33)/(steps))); // random slice
                accum = voxel_curr;
       }
    

    Also eine beliebige Schicht wähle und deren Wert an (x,y,33) in einer Schleife in die Variable accum schreibe, bekomme ich ein schwarzes Bild, wenn die Schleife öfter als 22 mal durchläuft. Eventuell ein Treiberbug?



  • mortified_penguin schrieb:

    Also eine beliebige Schicht wähle und deren Wert an (x,y,33) in einer Schleife in die Variable accum schreibe, bekomme ich ein schwarzes Bild, wenn die Schleife öfter als 22 mal durchläuft. Eventuell ein Treiberbug?

    Ach? Und vorher ist das Bild okay wie du es erwartest? Und steps ist auch 33 (beachten: Texturkoordinaten liegen dann ausserhalb der Textur, dann greift die Sampler-Konfiguration für die Ränder)?

    Wenns nur an der Anzahl der durchläufe liegt könnte es auch kein Treiber-Bug sondern ein Treiber-Feature sein, das einen Shader-Thread abbricht, wenn er zu lange braucht - schonmal ne Endlosschleife in nem Shader programmiert? Bei meiner letzten Erfahrung damit ging gar nix mehr - schon übel wenn das System im Grafiktreiber hängt, der grad auf die GPU wartet :D.

    Allerdings klingt 22 für mich noch ziemlich klein. Es gibt da Raycasting-Shader mit aufwändigeren Schleifen die ganz schicke Resultate erzielen und ordentlich laufen.

    Finnegan

    P.S.: Ich würde bei obiger Schleife ausserdem erwarten, dass der GLSL-Compiler die Schleife komplett wegoptimiert. Ich weiss die sind wahrscheinlich nicht ganz so pfiffig wie ein akueller C++-Compiler, aber das sollte eigentlich simpel genug sein.



  • Scheint in der Tat ein Treiberbug zu sein. Ich arbeite unter Linux mit nouveau; habs jetzt auf meinem Laptop mit proprietärem Treiber probiert, da scheint alles ok zu sein. Warum jetzt allerdings die Version back-to-front geht deren Schleife genauso oft durchläuft und sogar noch geschachtelt ist, versteh ich jetzt auch nicht wirklich.

    Danke übrigens für die Hilfe 👍



  • Manchmal sind es relativ simple Dinge die ein Treiber toleriert und ein anderer nicht. Hatte z.B. mal ein fehlendes Padding bei einer uniform-Datenstruktur in Direct3D11... länger nicht bemerkt, da der NVidia-Treiber das kommentarlos geschluckt hat. ATI-Treiber hat einfach nix gemacht und hat nen schwarzen Bildschirm angezeigt. GL_ARB_debug_output könnte bei OpenGL eventuell hilfreich sein Sachen zu finden, wo man sich z.B. nicht genau an die Spezifikation hält. Ansonsten ist der proprietäre Treiber die richtige Wahl wenn man GPU-Programmierung macht. Man mag von dem Laden halten was man will, deren OpenGL-Implementation ist einfach die beste die man für aktuelle GPUs finden kann 😉 ... auch wenn ein hinreichend komplexes Programm dann nicht selten auf anderen Treibern schlecht oder gar nicht läuft (liegt aber leider daran dass doch so manche OGL-Implementierungen nicht das gelbe vom Ei sind).

    Finnegan


Anmelden zum Antworten