Raytracing - Ich brauche neue Ideen was ich an den Bild noch verbessern kann
-
Der Link ist ne gute Idee, um Geschwindigkeit rauszuholen.
Ich habe die Funktion für die Schnittpunktabfrage zwischen Strahl und Dreieck mal testweise ersetzt.
Renderzeit meine Funktion: 52 Sekunden
Renderzeit Jacco Bikkers Funktion: Schwankt zwischen 48 und 51 SekundenEine kleine Verbesserung ist es ja zumindest. Mal sehen was sein KD-Baum bringt.
@Update:
Es gibt mindestens zwei Varianten wie man ein KD-Baum traversieren kann. Entweder rekursiv oder man hat eine eigene Stack-Variable, wo man dann ohne Rekursion auskommt. Diese Information habe ich von hier http://dcgi.felk.cvut.cz/home/havran/ARTICLES/cgf2011.pdf
Der Link, den du mir geschickt hast, implementiert die Stack-Variante aber er war nicht ganz so leicht zu lesen für mich. Ich habe dafür aber eine Rekursive Variante hier gefunden:
http://www.socher.org/uploads/Main/RenderingCompetitionSocherGranados/src/Kdtree.h
http://www.socher.org/uploads/Main/RenderingCompetitionSocherGranados/src/SAHKdtree.hDiesen Algorithmus habe ich nach C# übersetzt und lediglich die Funktion zum Clippen des Querry-Rays mit der Szenen-Bounding-Box habe ich von den Jacco Bikker genommen.
D.h. ich habe jetzt ein Rekursiven KD-Baum mit SAH-Splitplane-Berechnung, welchen ich vielleicht noch auf Iterativ oder weiteres erweitern kann. Ich werde dazu diesen Link hier: http://www.sci.utah.edu/~wald/PhD/wald_phd.pdf weiter durcharbeiten, da dass anscheinend eine gute Quelle ist, wo viele drauf verweisen.
Mein Bounding Intervall Hierachie mit Median-Splitplanes hat 18 Sekunden zum Rendern (+ Erstellung der BIH)gebraucht
Der Rekursive KD-Baum mit SAH-Splitplane hat 10 Sekunden zum Rendern(+ Erstellung des Baumes) gebraucht.
Es war etwas mühsam, da lauter so kleine fiese Fehler mir beim Übersetzen passiert sind aber es hat sich gelohnt. Der Renderzeit ist jetzt schon mit der 'einfachen' Variante besser.
-
auf 50% bei einfachen szenen ist schonmal nicht schlecht. wobei es gerade bei komplexen szenen ist wo die guten algorithmen glaenzen.
rekursive und iterativ ist logisch recht gleich.
beim rekursiven springst du fuer den "nahen" knoten in die in die funktion, dabei legt der compiler fuer dich den anderen knoten auf den stack.
beim iterativen legst du den anderen knoten auf deinen stack und verwendest den "nahen" knoten als hauptknoten weiter.wenn du nun einen schnittpunkt findest, musst du bei der rekursiven variante zig hiearchystufen den stack hoch und jedesmal pruefst du "schon fertig? ach, na dann return".
beim iterativen springst du einfach einmal raus (return) und bist fertig. das kann sehr viel zeit sparen.zudem ist der stack ein wenig reddundant, denn das program weiss nicht 100% was du brauchst, sichert also eventuell sehr viel mehr daten und uebergibt gleichzeitig deine schon z.b. in registern existierenden daten an die aufzurufende funktion. das ist uU viel hin und her ohne wirklich nuetzliches zu machen.
iterativ koennte dir weitere 10%-20% bringen, denke ich. und an sich ist was sehr einfach.
stattbool Trace(Ray,Node) { RayLeft,RayRight; if(Node->IntersectTriangles(Ray)) return true; if(Node->NoChildren()) return false; Node->split(Ray,RayLeft,RayRight); NodeLeft = Node->Left(); NodeRight= Node->Right(); if(RayLeft.Distance(Ray)>RayRight.Distance(Ray)) { swap(RayLeft,RayRight); swap(NodeLeft,NodeRight); } if(Trace(RayLeft,NodeLeft)) return true; return Trace(RayRight,NodeRight); }
machst du
bool Trace(Ray,Node) { //das kann man anders machen, aber erstmal den ray auf den stack zu schieben ist verstaendlicher NodeStack; RayStack; NodeStack.push(Node); RaySTack.push(Ray); while(NodeStack.size()>0) { Node = NodeStack.pop();//hier holen wir immer was noch abzuarbeiten ist Ray = RayStack.pop(); while(true) { if(Node->IntersectTriangles(Ray)) return true; //beim treffer ist uns der stack egal, wir springen sofort ganz raus if(Node->NoChildren()) break; // statt return false;, weil wir uns jetzt was vom stack holen muessen //bleibt gleich RayLeft,RayRight; Node->split(Ray,RayLeft,RayRight); NodeLeft = Node->Left(); NodeRight= Node->Right(); if(RayLeft.Distance(Ray)>RayRight.Distance(Ray)) { swap(RayLeft,RayRight); swap(NodeLeft,NodeRight); } //der unterschied NodeStack.push(NodeRight); // <- die seite die wir spaeter rekursiv abarbeiten wuerden RaySTack.push(RayRight); // schieben wir auf unseren stack Ray = RayLeft; // die die wir zuerst abarbeiten wuerden Node = NodeLeft; // bestimmen wir als die aktuelle }//endlos while }//stack loop return false; }
-
Ich versuche gerade den iterativen Ansatz umzusetzen und ich komme mit einer Abbruchbedingung nicht klar.
http://www.sci.utah.edu/~wald/PhD/wald_phd.pdf Seite 121 (107 Oben Rechts)
Rekursiver Ansatz:
// case three: traverse both sides in turn t_hit = RecTraverse(FrontSideSon(node),t_near,d); if (t_hit <= d) return t_hit; // early ray termination return RecTraverse(BackSideSon(node),d,t_far);
Gleiche Seite Iterativer Ansatz:
// have a leaf now IntersectAllTrianglesInLeaf(node); if (t_far <= ray.t_closesthit) return; // early ray termination
Beim rekursiven Ansatz steht das hier
if (t_hit <= d) return t_hit;
was ich noch verstehe. Wenn ich im vorderen Blattknoten was finde, dann brauche ich den hinteren Blattknoten nicht auch noch kontrollieren, da dort der Schnittpunkt ja weiter weg sein muss.
Beim iterativen Ansatz steht aber das hier:
if (t_far <= ray.t_closesthit)
Wenn ich mich in ein Blattknoten befinde und t_far zeigt auf die hintere Ebene von diesen Knoten, warum gilt es dann Abbruch, wenn mein Schnittpunkt hinter der t_far-Linie liegt?
Wenn man
if (t_far <= ray.t_closesthit)
durch
if (ray.t_closesthit < t_far)
ersetzten würde, dann macht es für mich mehr Sinn. t_far ist die Splitplane vom Elternknoten.
Ich habe diese Ersetzung auch gemacht und komme mit dem iterativen Ansatz jetzt auf 12 Sekunden. Wenn ich diese Ersetzung nicht mache, dann brauche ich nur 2 Sekunden und das Bild ist fehlerhaft^^
Ich kann mir so schwer vorstellen, dass Ingo Wald da ein Fehler gemacht haben kann. Der Fehler muss bei mir liegen. Kann mir jemand sagen, warum iterativ langsamer ist, wenn ich es mit meiner Modifikation mache und warum fast alle Dreiecke im Bild fehlen, wenn ich es mit Ingo Walds Weg mache?
Hier ist meine Funktion nochmal im Ganzen:
ISchnittpunkt GetSchnittpunktStrahlKDBaumIterativ(Strahl ray, IIntersecableObject excludedObjekt, float time) { IntersectionPoint point = new IntersectionPoint(); float t_min, t_max; if (ClipRayWithBoundingBox(ray, this.topBox, out t_min, out t_max) == false) return null; Node node = this.root; KdStack stack = new KdStack(); while (true) { // traverse ’til next leaf while (node is InnerNode) { SplitPlane p = (node as InnerNode).p; float t_split = (p.pe - ray.Start[p.pk]) * (ray.Richtung[p.pk] == 0 ? float.MaxValue : 1.0f / ray.Richtung[p.pk]); // near is the side containing the origin of the ray Node near, far; if (ray.Start[p.pk] < p.pe) { near = (node as InnerNode).leftChild; far = (node as InnerNode).rightChild; } else { near = (node as InnerNode).rightChild; far = (node as InnerNode).leftChild; } if (t_split > t_max || t_split < 0) { node = near; } else if (t_split < t_min) { node = far; } else { stack.Push(far, t_split, t_max); node = near; t_max = t_split; } } // have a leaf now (node as LeafNode).traverse(ray, time, excludedObjekt, t_min, t_max, point); //if (t_max <= point.Distance) return point.Point; //early ray termination (Ingo Wald) if (point.Distance < t_max) return point.Point; //early ray termination (XMAMan) if (stack.IsEmpty()) return point.Point; // noting else to traverse any more... node = stack.Pop(out t_min, out t_max); } }
-
ja, das
if (t_far <= ray.t_closesthit)
schaut wie ein bug aus. zudem fehlt
t_far=d;
am ende vom inner loop. (kannst ja mal profilen wieviel boost das bringt
an sich sollte dein code erstmal laufen.
-
Momentan bringt der iterativen Ansatz überhaupt kein Boost. Bei mein Hauptarbeitlaptop ist iterativ 1-2 Sekunden langsamer. Bei mein Ersatzlaptop sind beide Verfahren gleichschnell.
Ich muss zugeben, dass ich noch garnicht so recht verstanden habe, warum eine eigene Stackvariable schneller sein soll, als der Callstack vom .Net-Framework. Gibts beim Callstack mehr Overhead, da man neben den Funktionsargumenten auch noch die Rücksprungadresse mit auf den Stack legen muss?
Laut Profiler verbringe ich 60 Prozent der Rechenzeit im Blattknoten und 40% im Traversierungsknoten (Inner Node), wenn man jetzt nur den KD-Baum als 100% betrachtet.
Wenn ich im Hauptbildschirm vom PRofiler schaue, dann ist die Schnittpunktabfrage zwischen Strahl und Dreieck, das KD-Baum-Traversieren und die Get-Methode zum Zugriff auf X/Y/Z per Indize von der Vektorklasse das, was das Meiste an CPU-Zeit ausmacht.
@Edit:
Bei mein Rechner auf Arbeit sehen die Renderzeiten wie folgt aus:
Bounding-Intervall-Hierachie: 71 Sekunden
KD-Baum-Rekursiv: 47 Sekunden
KD-Baum-Interativ: 49 SekundenIch werde mich jetzt als nächstes mit der Surface Area-Heuristic näher beschäftigen. Vielleicht kann man da noch was rausholen.
-
XMAMan schrieb:
Ich muss zugeben, dass ich noch garnicht so recht verstanden habe, warum eine eigene Stackvariable schneller sein soll, als der Callstack
weil du beim erfolgreichen "hit" den callstack nicht hoch traversieren musst, sondern einfach rausgehst aus der funktion.
vom .Net-Framework. Gibts beim Callstack mehr Overhead, da man neben den Funktionsargumenten auch noch die Rücksprungadresse mit auf den Stack legen muss?
nicht nur funktionsargumente, auch alles was in registern ist, wird temporaer auf dem stack "gesichert".
Laut Profiler verbringe ich 60 Prozent der Rechenzeit im Blattknoten und 40% im Traversierungsknoten (Inner Node), wenn man jetzt nur den KD-Baum als 100% betrachtet.
klingt normal. wenn der kd-tree schneller ist als dein BIH, kannst du eventuell feiner tesselieren und somit ein wenig arbeit vom leaf zu inner nodes verlagern.
Wenn ich im Hauptbildschirm vom PRofiler schaue, dann ist die Schnittpunktabfrage zwischen Strahl und Dreieck, das KD-Baum-Traversieren und die Get-Methode zum Zugriff auf X/Y/Z per Indize von der Vektorklasse das, was das Meiste an CPU-Zeit ausmacht.
ist das array mit den vektor objekten eine einzige allokation? oder ist jeder vector mit "new" allokiert?
Bei mein Rechner auf Arbeit sehen die Renderzeiten wie folgt aus:
Bounding-Intervall-Hierachie: 71 Sekunden
KD-Baum-Rekursiv: 47 Sekunden
KD-Baum-Interativ: 49 SekundenIch werde mich jetzt als nächstes mit der Surface Area-Heuristic näher beschäftigen. Vielleicht kann man da noch was rausholen.
versuch die allokation von deinem stack einmal vorher zu machen, statt dauernt neu zu allokieren. es kann sein, dass das die meiste zeit zieht, weil die stack groesser immer zu klein ist am anfang und dann der stack "resized" wird, was mehr allokieren und memcopy bedeutet.
(natuerlich ein stack pro thread )
-
Wenn ich den Stack mit einer Anfangsgröße von 40 Anlege, dann hat es keinen Einfluß auf die Renderzeit.
Mit Array und Vekor meine ich folgende Funktion:
[Serializable()] [StructLayout(LayoutKind.Explicit)] public class Vektor { [FieldOffset(0)] public float X; [FieldOffset(4)] public float Y; [FieldOffset(8)] public float Z; public float this[int key] { get //Diese Funktion hier macht fast 10% meiner CPU-Zeit beim Rendern aus { unsafe { fixed (float* f = &this.X) { return f[key]; } } } set { unsafe { fixed (float* f = &this.X) { f[key] = value; } } } }
D.h. wenn jemand bei ein
Vektor v = new Vektor(1,2,3);
auf
v[2]
zugreift, dann dauert das recht lange. Jeder Eckpunkt von ein Dreieck hat sein eigenen mit new Vektor(..) angelegten Vektor. D.h. nicht die Vektor-Objekte bilden ein Array sondern seine X/Y/Z-Werte. In C würde man ein Union-Struct nehmen. Das hier ist mein Nachbau davon, da es sowas direkt in C# nicht gibt.
-
Bist du sicher dass du Vektor als
class
umsetzen willst? Ich würde meinen sowas wie ein 3D-Vektor schreit geradezu nachstruct
...Und was den Indexer angeht würde ich mal versuchen nicht zu versuchen schlauer als der Compiler zu sein...
public float this[int key] { get { switch (((uint)key) & 3u) { case 0: return X; case 1: return Y; case 2: return Z; case 3: return 0; } } set ... }
Würde mich nicht wundern wenn das viel schneller wäre. Bzw. alternativ könnte man noch probieren
public float this[int key] { get { switch (key) { case 0: return X; case 1: return Y; case 2: return Z; default: throw new BlubException(); } } set ... }
-
Wenn ich das mit der Switch-Anweisung mache, dann braucht er auch 47 Sekunden zum rendern. D.h. genau so lange, wie mit der Pointerarritmethik. Das verwundert mich doch sehr, da 3 If-Anweisungen doch mehr Rechenzeit brauchen müssten, als eine Addition und Multiplikation.
Ich benutze die Vektor-Klasse auch um darin meinen Farbwert zu speichern. Wenn ein Strahl aber kein Objekt trifft, dann zeige ich über den Null-Wert an, dass bei diesen Pixel dort nichts ist. Ich darf an der Stelle noch keine Farbe vom Hintergrundbild zurück geben, da ich beim Tonemapping die Hintergrundfarbwerte nicht mit verrechnen will/darf.
Deswegen ist ist das eine Klasse, um es auf Null setzen zu können.
-
das macht mich neugierig irgendwie, wie schnell es in c++ waere. Von jemand anderem die c++ version war ca 4mal schneller, aber das ist sicherlich sehr von fall zu fall abhaengig.
wenn unabhaengig vom code die laufzeit fast gleich bleibt, bist du vielleicht memory bound. ein if bzw switch kostet dich 1 bis 20 cycle, ein cache miss kann 100cycle+ sein.
-
XMAMan schrieb:
Wenn ich das mit der Switch-Anweisung mache, dann braucht er auch 47 Sekunden zum rendern. D.h. genau so lange, wie mit der Pointerarritmethik. Das verwundert mich doch sehr, da 3 If-Anweisungen doch mehr Rechenzeit brauchen müssten, als eine Addition und Multiplikation.
Mich verwundert dass es mit switch nicht deutlich schneller ist. Denn fixed riecht für mich nach Pinning, und Pinning hätte ich für teuer gehalten. Aber vielleicht ist Pinning hauptsächlich hinderlich für den GC, wenn zu viel gleichzeitig gepinnt ist während ne Collection läuft. Hmmm...
Müsste man sich den generierten Code angucken.Und... die
switch (((uint)key) & 3u)
Variante sollte er genau so optimieren können, mit dem einzigen kleinen Unterschied dass er dabei einor reg, imm
mehr braucht - was jetzt nicht die Welt kostet.XMAMan schrieb:
Ich benutze die Vektor-Klasse auch um darin meinen Farbwert zu speichern. Wenn ein Strahl aber kein Objekt trifft, dann zeige ich über den Null-Wert an, dass bei diesen Pixel dort nichts ist. Ich darf an der Stelle noch keine Farbe vom Hintergrundbild zurück geben, da ich beim Tonemapping die Hintergrundfarbwerte nicht mit verrechnen will/darf.
Deswegen ist ist das eine Klasse, um es auf Null setzen zu können.
Dann pack nen
bool
zusätzlich mit rein. Ganz im Ernst. Hier nen Reference-Type zu verwenden ist doch geradezu das Textbook Example für Performance Killer.
-
Wiso ist eine Struct schneller als eine class? Die eine Variable liegt auf dem Heap, die andere auf dem Stack. Der GC muss immer dann ran, wenn ich also eine Vektor-Klasse erzeuge oder damit rechne, was ja beim Raytracen sehr häufig passiert. Habe ich stattdessen eine Struct, geht das erzeugen(und somit rechnen) schneller. Ist das der Grund warum du mir Struct empfielst? Wenn das wirklich stimmen sollte, dann sollte ich die Klasse Vektor, Ray und all so anderes Zeug, was für jedes Pixelsamples mehrmals erzeugt wird, überdenken.
Ich bräuchte mal ein Programm, womit man feststellen kann, ob der Garbage Collecdtor der Flaschenhals ist.
@Update:
Ich habe mal Testweise Vektor-class durch Vektor-struct ersetzt. Das Problem mit den Farbwert, der anzeigt, ob ein Objekt getroffen wurde, habe ich dadurch gelößt, dass ich Vektor? bei der Methode zurück gebe. Die Renderzeit ist durch die Struct 1-2 Sekunden langsamer. Schade... War ne gute Idee aber so einfach läßt sich C# nicht austricksen.
-
XMAMan schrieb:
Mein nächstes Bild wird diesmal eine Nahaufnahme von kleinen Gegenständen sein. Sowas muss auch mal sein^^ Dort möchte ich mich um Subsurface Scattering kümmern.
Ohne Tiefenunschärfe:
https://picload.org/image/rodpowrg/test.pngMit Tiefenunschärfe
https://picload.org/image/rodwgpll/test.pngIch hab den Raytracer in den letzten 8 Wochen jetzt nochmal komplett neu geschrieben und nun sieht das Bild auch schon viel besser aus.
-
schaut immer besser aus
die pflanze im hintergrund schaut irgendwie seltsam zerschnitten aus im bereich vor dem licht. wobei das durch bokeh kommen kann, schaut dennoch ungewohnt aus fuer mein auge.
vielleicht stimmt was nicht mit dem sample selektieren, fuers DOF.
-
Wenn du damit das ganz rechte Blatt meinst, was vor dem Fenster ist, dann denke ich, dass es einfach vom Fenster überstrahl wird. Selbst vor der Wand ist es ja kaum 1 Pixel breit, da man dort seitlich draufschaut.
Ich habe jetzt noch ein Topf hinzugefügt:
-
Ich meinte alle Blätter, die sehen aus als ob sie aliasing bei der ueberstrahlung haben. Das schaut aus als wuerde etwas nicht stimmen.
-
Der Grund warum es genau vor der Lichtquelle Aliasingeffekte gibt und vor der Wand nicht ist folgender.
Die Wand und das Blatt ist ungefähr gleichhell. Bilde ich den Durchschnittswert zwischen (0.5; 0.5; 0.5) = Wand und (0; 0.5; 0) Blatt, dann erhalte ich (0.25; 0.5; 0.25).
Bild ich aber den Durchschnitt zwischen Fenster (100; 100; 100) und Blatt (0.5; 0.5; 0.5) dann erhalte ich ~ (50; 50; 50). Das Grün wird quasi weggewichtet. Damit ich aber ein schönes Farbverlauf zwischen Grün und Weiß erhalte, müssen beide Farben ungfähr gleichhell sein.
Beim umrechnen eines Farbwertes, wie es der Raytracer erzeugt, gehe ich so vor, dass ich alles, was über 1 ist wegschneide. D.h. (100; 100; 100) wird zu (1;1;1) Nun multipliziere ich noch mit 255 und erhalte so die RGB-Werte. Auf die Weise verschwindet dann das grüne Blatt vor dem Fenster.
-
XMAMan schrieb:
Bild ich aber den Durchschnitt zwischen Fenster (100; 100; 100) und Blatt (0.5; 0.5; 0.5) dann erhalte ich ~ (50; 50; 50). Das Grün wird quasi weggewichtet. Damit ich aber ein schönes Farbverlauf zwischen Grün und Weiß erhalte, müssen beide Farben ungfähr gleichhell sein.
selbst dann sollte es durch die abnehmender/zunehmende abdeckung faelle geben, wo du z.b. 90% oder 99% vom blatt hast.
Beim umrechnen eines Farbwertes, wie es der Raytracer erzeugt, gehe ich so vor, dass ich alles, was über 1 ist wegschneide. D.h. (100; 100; 100) wird zu (1;1;1) Nun multipliziere ich noch mit 255 und erhalte so die RGB-Werte. Auf die Weise verschwindet dann das grüne Blatt vor dem Fenster.
Kann man machen, aber das ist wirklich nicht so gut. Du brauchst tonemapping
http://filmicworlds.com/blog/filmic-tonemapping-operators/
https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
https://de.slideshare.net/ozlael/hable-john-uncharted2-hdr-lighting
-
rapso schrieb:
Kann man machen, aber das ist wirklich nicht so gut. Du brauchst tonemapping
http://filmicworlds.com/blog/filmic-tonemapping-operators/
https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
https://de.slideshare.net/ozlael/hable-john-uncharted2-hdr-lightingIch habe jetzt sämtliche Tonemapping-Operatoren die du mir gegeben hast eingebaut und so sieht das aus:
https://image.ibb.co/kYec1a/Tonemapping_Result.jpg
Ich finde bei vielen von diesen Operatoren sehen die Farben einfach nicht mehr so kräftig aus. Schön wäre es ja, wenn es da was gäbe, was schön satte Farben erzeugt und trotzdem mit verschiedenen Helligkeitsstufen klar kommt.
-
ich finde das schaut schon nicht schlecht aus, gerade ACES!
An sich gibt es zwei (bzw 3) vorgehensweisen das zu verbessern
1. du speicherst die bilder in einem HDR format ab, z.B. rgbe und dann kannst du in photoshop/gimp as tonemapping machen.
2. du gibst die regler in deinem tool, damit artist das interaktiv aendern
3. bei Filmen kommt es auf den eigentlich Film in der kamera an. Die sind deswegen auch so teuer, weil es sehr viele verisonen gibt von denen alle ganz genau spezifiziert sind und je nach situation (helligkeit, bewegungsschnelligkeit, kamera, framerate, gewuenschter effekt wie z.B. grain) ein spezieller film ausgesucht wird. Alles noetige zu wissen ist schon viel arbeit, darin profi zu sein macht dann den ueberbezahlten seltenen fachmann aus: https://photo.stackexchange.com/questions/60707/how-to-read-a-film-color-response-chartIch wuerde dir vorschlagen immer 2 bilder auszugeben, ein in HDR, ein schon nach tonemapping als platzsparendes jpg. (So wie es DSLRs machen).