RENDERING
Einleitung
Nachdem ich hier neulich erst beschrieben habe, wie ich seit etlichen Jahren halbwegs brauchbare Bilder erzeuge, habe ich das ganze über den Haufen geworfen und was besseres gebaut. Den neuen Algorithmus werde ich hier beschreiben - für mich selbst und für alle die es interessiert...
Auch mein neuer Algorithmus ist nicht gerade das Non-Plus-Ultra auf dem Gebiet, da ich aber schon einige Male danach gefragt wurde, wie, bzw. mit welchem Programm denn die Fraktal- oder auch Bezier-Bilder auf meinen Seiten entstanden sind und ich eine "Doku" in dieser Form auch für mich selbst immer hilfreich finde, habe ich mich mal an die Beschreibung gemacht. Dienen soll es (neben mir selbst, wenn ich mich mal wieder frage, wie ich dieses oder jenes eigentlich gemacht habe) Leuten, die wie ich, "mal eben" 3D-Objekte selbst visualisieren wollen. Hier sind neben einem relativ einfachen Ansatz auch einige Fallen erwähnt, an denen ich lange herumgeknobelt habe. Wer bestimmte Nachteile vermeiden will, die "mein" Ansatz prinzipiell mit sich herumschleppt, wird diese hier auch beschrieben finden. Man kann sich dann überlegen, ob man diese nicht besser von Anfang an vermeiden möchte...
Generelle Merkmale
Der im Folgenden erläuterte Algorithmus zeichnet sich durch folgende Features und Nicht-Features aus:
  • Der Algorithmus ist inzwischen ein "echter" Ray-Tracer. Beim Ray-Tracing werden, wie der Name schon andeutet, der Weg virtueller Lichtstrahlen, zwischen den Lichtquellen und dem virtuellen Auge verfolgt, womit prinzipiell beliebige Spiegelungen, Schattenwürfe, etc. berechenbar sind (aus Effizienzgründen werden allerdings normalerweise einige Einschränkungen gemacht)1).

  • Im Vergleich zu dem was Echtzeitbibliotheken (z.B. das von mir auch eingesetzte Java3D) so zu leisten im Stande sind, ist mein Renderingalgorithmus furchtbar laaaaangsaaaaam.

  • Dafür berechnet er den Schattenwurf, was sehr zum räumlichen Eindruck der Darstellung beiträgt (das kann Java3D z.B. nicht :-).

  • Für das Rendern von Fraktalen hat der Ansatz den Vorteil, dass er direkt auf (Raum-)Pixeln (manchmal auch "Voxel" genannt) arbeitet (ein Raumpixel ist durch seine X- und Y- und zusätzlich durch seine Z-Koordinate bestimmt). Eine Umrechnung in irgendeine Art von Flächen (z.B. Dreiecksflächen) wird nicht benötigt. Umgekehrt müssen Objekte die sich z.B. aus Bezier-Flächen zusammensetzen auch erst in eine Menge von Raumpixeln überführt werden. Ein Nachteil, insbesondere auch für perspektivische Darstellungen ist, dass so eine Repräsentation durch Raumpixel nicht mehr so ohne weiteres (größer) skalierbar ist (man kennt den Effekt von normalen 2D pixelbasierten Grafikformaten, wie Jpeg, PNG, Gif, Tiff, BMP usw. - im Gegensatz zu vektororientierten Formaten, wie z.B. SVG, die ohne Einbußen beliebig  verkleinerbar und vergrößerbar sind).

  • Grundsätzlich wird eine "Fluchtpunktperspektive" benutzt. Diese ergibt sich einfach daraus, das virtuelle Lichtstrahlen in einem Punkt (dem "Beobachter") gebündelt werden. Eine Quasi-Parallelperspektive, kann dadurch simuliert werden, dass der Beobachtungspunkt sehr weit von der Szene mit den abzubildenden Objekten entfernt gewählt wird. Die folgende Abbildung zeigt Beispiele für Parallel- und Fluchtpunktperspektiven:
    Parllelperspektive
    Fluchtpunktperpektive
    Fluchtpunktperspektive mit Java3D
    Parallelperspektive
     
    Fluchtpunktperpektive
     
    Zum Vergleich: ähnliche Perspektive mit Java3D
    Nicht unwichtig ist vielleicht darauf hinzuweisen, dass in der Fluchtpunktperspektive Objekte / Objekt-Teile sichtbar sein können, die in der Parallelperspektive unsichtbar sind (und umgekehrt), wie die folgenden Beispiele verdeutlichen:
    Parllelperspektive
    Fluchtpunktperpektive
    Fluchtpunktperpektive mit Java3D
    Parallelperspektive
    (nur die Vorderseiten der Säulen sind sichtbar)
     
    Fluchtpunktperpektive
    (die Innenseiten der Säulen sind sichtbar)
     
    Nochmal eine ähnliche Perspektive mit Java3D

  • Wie aus der Fotografie bekannt, trägt auch die Tiefenschärfe, bzw. oft eher der Mangel an Tiefenschärfe, sehr zum räumlichen Eindruck bei. Die Schärfe eines Punktes ist dabei abhängig davon, wie weit der Punkt von einer zu definierenden "Schärfeebene" entfernt ist. Bei mir wird zu diesem Zweck auf jeden Punkt ein Gausscher Weichzeichner mit entsprechend angepasstem Radius angewendet. Allerdings ist die Berechnung dafür sehr langsam, da ja jeder (nicht scharfe) Punkt über einen umliegenden Bereich von Nachbarpunkten "verschmiert" werden muss. Meine derzeitige Implementierung verbrät darüber hinaus viel Speicher, aber da gäbe es noch Einsparpotenziale. Das Beispiel unten zeigt Bilder mit unterschiedlicher Tiefenschärfe, wobei die Objekte zusätzlich mit einer Textur überzogen wurden, um den Effekt noch deutlicher zu machen:
    Bild mit geringer Tiefenschärfe
    Bild mittlerer Tiefenschärfe
    Bild ohne Unschärfen
    Bild mit geringer Tiefenschärfe
     
    Bild mit mittlerer Tiefenschärfe  
    Zum Vergleich dasselbe Bild ohne Unschärfen

  • Eine Farbverschiebung und Dunst können hinzugefügt werden. Der Dunst ist dabei ein tiefenabhängiges Aufhellen (Weißverschiebung). Bei der Farbverschiebung werden die Farben hin zu einer gewählten Farbe verschoben - je weiter ein Punkt weg ist desto stärker. In der Natur kann man z.B. eine Blauverschiebung beobachten.
    Blauverschiebung und Dunst
    Blauverschiebung und Dunst
    Zusätzlich zur Tiefenunschärfe ist hier eine tiefenabhängige Weiß- und Blauverschiebung benutzt worden  
    ...und das Ganze nochmal mit Hintergrund (aber weniger Unschärfe, Blau- und Weißverschiebung)
  • Texturen sind übrigens nicht Teil des Renderings und werden daher hier nicht weiter erläutert. Da aber für jedes Pixel eine eigene Farbe bestimmt werden kann, können natürlich prinzipiell auch Texturen über ein Objekt gelegt werden. Wie das passiert liegt aber vollständig in der Hand der die Szenenbeschreibung erstellenden Funktionen und hat mit dem eigentlichen Rendering daher nichts zu tun.

  • Spiegelungen können ebenfalls berechnet werden. Dafür ist es auch Möglich eine virtuelle Box zu definieren, die die Szene umgibt und die auf allen sechs Seiten eine Textur trägt, die dann vom Objekt reflektiert wird (andere Formen, wie Kugeln, Zylinder, etc. sind auch denkbar). Auf der Oberfläche eines Objekts kann sich aber auch ein anderes Objekt spiegeln, wobei hier eine max. Anzahl gegenseitiger Spiegelungen angegeben werden kann (und muss).
    Spiegelungen einer umgebenden Box
    Spiegelung an einer umgebenden Box
    Drei Seiten einer ein Objekt umgebenden "Spiegelungs-Box"
     
    Gerendertes Objekt mit Spiegelungen
  • Transparenzen werden derzeit nicht unterstützt. Das liegt hauptsächlich an der Form, wie ich die Szene beschreibe. Es ist zwar effizient möglich von einem Punkt außerhalb der Objekte (also von irgendwo im leeren Raum) den nächsten festen, also zur Oberfläche eines Objekts gehörenden, Punkt zu finden, jedoch nicht so ohne Weiteres den nächsten Oberflächen-Punkt von einem Punkt innerhalb eines Objekts.
Szenen- / Objektbeschreibung
Generell arbeitet der Algorithmus auf einer Szenen-Beschreibung, die die Lichtquellen, eine Repräsentation der abzubildenden Objekte, den Beobachtungspunkt, den zu fokussierenden Punkt (zentraler Punkt der Szene) sowie die Begrenzungskoordinaten der Szene enthält.
  • Lichtquellen haben prinzipiell eine Licht-Farbe und eine Position. Lichtquellen können zudem gerichtet sein, d.h. sie strahlen Licht nur in einem bestimmten Winkel ab. Prinzipiell muss eine Lichtquelle in der Lage sein für einen bestimmten Punkt die Lichtintensität angeben zu können.

  • Alle abzubildenden Objekte sind in einem (bei mir) sogenannten "Space" abgelegt. Dieser kann intern ganz unterschiedlich aufgebaut sein. Gemeinsam ist allen Spaces aber, dass sie in der Lage sind für einen "Strahl" der bei einem Punkt p=(x,y,z) beginnt und in Richtung (dx,dy,dz) verläuft, den nächsten "festen" Punkt angeben zu können auf den dieser Strahl trifft. Ein Punkt ist dabei mindestens durch seine Farbe und einen Normalenvektor (also die Ausrichtung der Oberfläche an diesem Punkt) gekennzeichnet. Optional kann er auch noch weitere Material-Eigenschaften enthalten, wie insbesondere die Spiegelungseigenschaften. Sollten diese Material-Eigenschaften fehlen werden für das Rendering stattdessen Default-Werte herangezogen. Folgende grundsätzlich unterschiedlichen Formen von Spaces gibt es derzeit:
    • Binär-Spaces: Es gibt z.B. einen Space, der alle Oberflächen-Punkte der Objekte als "Voxels" (3D-Pixel) repräsentiert. Da es, wegen der großen Anzahl der Punkte, unmöglich ist ein 3D-Array der Form Voxel[width][height][depth] aufzubauen, unterteilt dieser Space-Typ den Raum rekursiv in jeweils 2x2x2=8 Sub-Spaces, die sich bis in eine bestimmte Tiefe entsprechend weiter unterteilen. In der max. Sub-Space-Tiefe ist dann ein spezieller Sub-Space abgelegt, der atomar ist, sich also nicht weiter unterteilt und das Voxel repräsentiert. Da komplett leere Sub-Spaces nicht angelegt werden, wird hier viel Speicher gespart. Zudem enthält, um Speicher zu sparen, ein "atomarer" Sub-Space nicht seine eigenen Koordinaten, denn diese gehen ja bereits aus seiner Position im Space hervor.
    • Virtuelle Spaces: Spezielle Objekte machen es möglich, dass der Space die Voxel nur virtuell enthält. Zu diesen Objekten gehören z.B. Fraktale. Diese können für jeden Punkt im Raum angeben, wie weit der nächste Punkt noch entfernt ist. Damit ist es leicht möglich den jeweils nächsten Punkt, der von einem Strahl getroffen wird effizient zu berechnen, womit die Speicheraufwändige Repräsentation wie im zuvor beschriebenen Fall entfallen kann.
    • Hybride Spaces: Trotzdem lassen sich auch virtuelle Spaces teilweise beschleunigen, indem z.B. für bestimmte Punkte (insbesondere Beobachter und Lichtquellen) Vorabberechnungen gemacht werden. Daher gibt es auch Mischformen, die schneller sind, aber auch mehr Speicher benötigen als rein virtuelle Spaces.
Weitere Parameter sind "global" für die ganze Szene bzw. das ganze Bild festgelegt und werden im nächsten Abschnitt erläutert.
Globale Parameter
Die folgenden "globalen" Parameter gelten für alle Bildpunkte (sofern diese nicht z.B. durch spezielle Material-Eigenschaften, die in der Material-Matrix für eine Punkt definiert sind überschrieben werden):
  • Position der direkten Lichtquelle:
    Beschreibt die Koordinaten einer Lichtquelle. Angegeben wird über die GUIs dabei normalerweise nur die Richtung - die Entfernung wird entsprechend der Entfernung des Beobachters gewählt. Dies ist aber keine Limitierung des Algorithmus'.

  • Farbe des direkten Lichts:
    Beschreibt die Farbe des direkten Lichts

  • Anteil des indirekten Lichts:
    Beschreibt den Anteil des indirekten Lichts am gesamten Licht. Bei einem Wert von 1 ist das indirekte Licht genauso Stark wie das direkte, bei 0 gibt es nur direktes und bei 2 nur indirektes Licht. Indirektes Licht wird derzeit durch eine Lichtquelle realisiert, die sich immer an der Position des Beobachters befindet.

  • Farbe des indirekten Lichts:
    Die Farbe des indirekten Lichts wird derzeit nur in Abhängigkeit des direkten Lichts beschrieben, als Hue-Verschiebung. Dies ist aber nur aus Platzgründe in den GUIs so (intern wird das in eine "eigene" Farbe für das indirekte Licht umgerechnet)...

  • Hintergrundbild:
    Bild, das für alle bis dahin transparenten Pixel verwendet wird.

  • Hintergrundfarbe:
    Die Farbe, die für Bildpunkte gewählt wird, die "leer" sind. Sofern das Ausgabeformat Transparenzen unterstützt, kann hier auch mit einer durchsichtigen oder teildurchsichtigen Farbe gearbeitet werden.

  • Schärfeebene:
    Eine Z-Koordinate für die Ebene in der Punkte scharf abgebildet werden.

  • Maximaler Blur-Radius:
    Maximaler Radius für den Gausschen Weichzeichner, der auf Punkte angewendet wird, die einen maximalen Abstand zur Schärfeebene haben. Ist dieser Abstand 0 wird keine Tiefen-(Un-)Schärfe berechnet.

  • Farbverschiebung:
    Ein Faktor der angibt wie stark die weitest entfernten Punkte in Richtung einer definierten Farbe (z.B. Blau) verschoben werden

  • Dunst (Weißverschiebung)
    Ein Faktor der angibt wie stark die weitest entfernten Punkte aufgehellt werden sollen.
Der Algorithmus im Detail
Der Algorithmus im Detail Während dieser Teil bei meinem alten Verfahren recht umfänglich war, wird die Sache - von der Theorie her - jetzt erfreulich einfach. Bis ich die Zeit habe das doch noch etwas ausführlicher zu beschreiben, mach ich's mal ganz kurz...

Ausgehend vom Beobachter (also einer Koordinate im Raum) werden virtuelle "Lichtstrahlen" (bzw. "Sichtstrahlen") verfolgt.
Dabei wird eine virtuelle "Projektionsebene" angenommen. Die Projektionsebene ist eine flache Ebene im Raum, die auf einen rechteckigen Bereich begrenzt ist (dies kann z.B. durch vier Geraden geschehen) und in Höhe x Breite Pixel unterteilt ist, wobei diese Höhe und Breite der Größe des zu erzeugenden Bildes entspricht. Der Einfachheit halber wird die Projektionsebene mit ihrer Mitte auf dem dem zentralen Punkt der Szene plaziert. Sie wird dabei so breit und so hoch gewählt, dass sie (mindestens) bis an die Grenzen der Szene reicht. Die Projektionsebene ist dabei dem Beobachter direkt zugewandt.

Durch jedes Pixel (also jedes Segment der Projektionsebene) wird nun vom Beobachter ausgehend ein "Lichtstrahl" verfolgt und berechnet, ob und auf welchen festen Punkt eines Objekts dieser trifft. Wird ein solcher Punkt p gefunden, so wird ausgehend von allen Lichtquellen je ein Strahl in Richtung dieses Objektpunkts p verfolgt. Trifft ein solcher Strahl zuerst auf einen anderen Punkt q, dann ist der fragliche Punkt p durch q abgeschattet. Falls der Lichtstrahl aber den Punkt p trifft, wird berechnet mit welcher Intesität Licht in Richtung des Betrachters reflektiert wird (also welche Helligkeit dieses Pixel durch diese Lichtquelle hat). Dies ist, neben der Farbe des Lichts und der Farbe des Punktes selbst, insbesondere auch abhängig von der Orientierung der Oberfläche zum Licht, sowie der Orientierung der Oberfläche zum Licht und zum Betrachter. Für ein sehr mattes Objekt wird nur die Orientierung der Oberfläche zum Licht, für ein sehr glänzendes Objekt nur die direkte Reflektion in Richtung des Betrachters verwendet. In der Regel werden beide Arten in einem bestimmten Verhältnis, welches durch die Materialeigenschaften an diesem Punkt (bzw. einen Defaultwert) gegeben ist verwendet. Wenn die Oberfläche an diesem Punkt p spiegelnd ist wird (rekursiv) ein neuer Lichtstrahl beginnend an Punkt p in Richtung der durch "Einfallswinkel = Ausfallswinkel" bestimmten Reflektionsrichtung berechnet, um zu bestimmen welcher andere Punkt an Punkt p gespiegelt wird (d.h. welche Farbe mit welcher Helligkeit). Tja das war's dann eigentlich auch schon... :-)

Linsentransformation
Der Übersichtlichkeit halber wird die Projektionsebene hier hinter dem Beobachter gezeigt, wodurch die Scene dort aber auf dem Kopf steht. Der Abstand vom Betrachter zur Szene entspricht einer Art "Brennweite"eines virtuellen Foto-Objektivs
Nachbearbeitung
Zusätzlich sind einige Nachbearbeitungsschritte (bei mir intern als "Filter" bezeichnet) möglich. Diese benötigen zumeist nur das gerenderte Bild - können aber auch den Z-Buffer verwenden, wenn zusätzlich die Z-Koordinate benötigt wird. Die Filter werden normalerweise in der hier angegebenen Reihenfolge auf das Bild angewendet (mögliche Abweichungen von dieser Reihenfolge sind jeweils angegeben):
  • Hue-, Saturation- und Brightness-Anpassung:
    Durch eine entsprechende Farbraumkonvertierung werden alle Pixel-Farben angepasst. Dies erfolgt vor den folgenden Schritten, damit nur die eigentlichen Objektpixel verändert werden, der Hintergrund aber unverändert bleibt (hätte man aber auch anders machen können).

  • Farbverschiebung und Dunst:
    Abhängig von der Tiefe eines Punktes und den Einstellungen werden die Farben aufgehellt und in Richtung einer Farbe verschoben (z.B. Blau).

  • Setzen eines Hintergrundbildes:
    Optional werden alle (semi-)transparenten Pixel mit der Farbe des entsprechenden Pixels eines (ggf. skalierten und verschobenen) Hintergrundbildes verrechnet (dieses kann selbst wieder transparente Pixel enthalten, so dass der nächste Schritt auch dann noch Sinn macht).

  • Setzen einer Hintergrundfarbe:
    Alle (immernoch) (semi-)transparenten Pixel können mit eine Hintergrundfarbe verrechnet werden (unter dem Punkt "Tiefenschärfe berechnen" ist ein Beispiel zu sehen, in dem ein Schachbrettmuster als Hintergrundbild verwendet wurde). Optional können sowohl Hintergrundfarbe als auch Hintergrundbild bereits als erste Filter angewendet werden, so dass sich alle Filter und nicht nur die Tiefenschärfeberechnung auch auf den Hintergrund auswirken.

  • Tiefenschärfe berechnen:
    Die Berechnung der Tiefenschärfe benötigt nur das zuvor angelegte Bild, sowie den Z-Buffer (für jeden Punkt wird neben der X- und Y-Koordinate auch die Z-Koordinate zur Ermittlung des Abstands von der Schärfeebene benötigt). Daher ist dieser Schritt intern als eine Nachbearbeitung implementiert. Prinzipiell wird für jeden Punkt ein Weichzeichnungs-Radius bestimmt, der umso größer ist je weiter der Punkt von der Schärfeebene entfernt ist. Statt eines linearen Zusammenhangs zwischen diesem Radius und dem Abstand zur Schärfeebene wird eine (re-normierte) Arcustangens-Funktion verwendet. Der Radius für den Punkt an Position (x,y) ergibt sich damit durch:

    Radius(x,y) = atan(abs(z-Schärfeebene)/maxDist * 4) / atan(4) * MaxBlurRadius)

    Atan
    Verwendeter Ausschnitt der Arcustangens-Funktion (mit max. Distanz = 1 und max. Blur-Radius = 1)

    Die Normierung auf den Wert von atan(4) ist dabei eine reine Konvention, damit der maximale Blur-Radius auch tatsächlich, für die Punkte erreicht wird, die am weitesten von der Schärfeebene entfernt sind. Um Probleme, die durch den Wert 0 für den Radius auftreten können zu vermeiden, ist es sinnvoll diesen Wert noch +1 zu nehmen. Dadurch ergibt sich insbesondere auch ein schönerer Übergang zwischen ganz scharfen und leicht unscharfen Bereichen (diese Optimierung ist in den unten gezeigten Beispielen übrigens noch nicht enthalten, was man jeweils an der linken vorderen Ecke des Rechtecks erkennen kann).
    Mit dem so berechneten Radius2) wird ein Gaußscher Weichzeichner auf den Punkt (x,y) angewendet. Dabei wird der Wert jedes Farbkanals (z.B. Rot, Grün, Blau und ggf. Alpha) auf die umliegenden Punkte, mittels einer Gewichtungsfunktion, verteilt (die Punkte in der Mitte bekommen den größten Anteil, die Punkte weiter außen bekommen abhängig vom Radius immer weniger ab). Als Gewichtungsfunktion wird eine zweidimensionale Normalverteilung verwendet:


    1
    -
    dx2+dy2

    2 var(x,y)
    Gewicht(x+dx,y+dy) = 

     e



    2 * PI * var(x,y)



    Zweidimensionale Normalverteilung
    Gewichtsfunktion (2D-Normalverteilung) bei Radius = 9 (im Intervall x = [-9...+9], y = [-9...+9]) und Varianz = (9/3)2 = 9

    wobei die Varianz var(x,y) = (Radius(x,y)/3)2 gewählt wird, so dass nur Bereiche der Funktion mit sehr kleinen Werten (Gewichten) außerhalb des Radius liegen, die wegen ihrer Kleinheit aber gefahrlos abgeschnitten werden können, ohne dass dies sichtbare Auswirkungen hätte. Die Variablen dx und dy iterieren jeweils über das Intervall [-Radius(x,y)...+Radius(x,y)]. Hier ist wichtig, dass der Radius nie Null ist, da die Funktion für var = 0 nicht definiert ist. Da gerade diese Funktion sehr oft ausgewertet werden muss und dabei recht langsam ist, verwende ich mittlerweile Look-Up-Tables, in denen die Werte für (ganzzahlig gerundete) Radien, sowie quadrierte Abstände vom Zentrum (dx2+dy2) abgelegt sind.

    Wie sich gezeigt hat, birgt dieses Verfahren noch ein paar Fallen. Es reicht leider nicht, einfach, wie beschrieben, einen Gaußschen Weichzeichner, der für jeden Punkt, abhängig von dessen Abstand zur Schärfeebene, einen anderen Radius verwendet zu nehmen3), weil dann an Übergängen zwischen scharfen Objektteilen zu sehr unscharfen Teilen Helligkeitsartefakte auftreten. Das kommt daher, dass Punkte die sehr unscharf sind viel ihrer Helligkeit an umliegende Punkte abgeben, während schärfere Punkte ihre Helligkeit in einem engeren Bereich konzentrieren. An Übergängen zwischen mehr und weniger scharfen Bereichen entsteht so ein Ungleichgewicht, welches sich in hellen und dunklen "Auren" äußert. Der Effekt ist unten im linken Bild zu erkennen. Hier hilft es für jeden Punkt die Summe aller Gewichte, die für diesen Punkt zur Anwendung kommen, zu summieren (also auch die für die Weichzeichnung von Punkten in der Umgebung, die auf den fraglichen Punkt "ausstrahlen"). Die Helligkeit jedes Punktes (also alle Farbkanäle) wird danach mit dieser Gewichtssumme normiert. Dies erfordert leider, dass man eine weitere Matrix anlegt in der die Gewichte für jeden Punkt gesammelt, d.h. aufsummiert werden.

    Ohne Normierung der Gesamtgewichtungen
    Ohne Adaption der Gaussfunktion
    Mit Gesamtgewichtsnormierung und Adaption der Gaussfunktion
    Mit Gesamtgewichtsnormierung und Adaption der Gaussfunktion
    Ohne Normierung der Gesamtgewichte (und ohne Gauß-Adaption): helle und dunkle "Auren" zeigen sich
     
    Ohne Gewichts-Adaption: schärfere Bereiche werden in Randzonen vom Hintergrund (zu stark) überstrahlt
     
    Mit Normierung der Gesamtgewichte und Gewichts-Adaption: schön erkennbarer Verlauf der Tiefenschärfe
     
    Kein negativer Einfluss auf den Hintergrund (gleichbleibend unscharf, auch an den Objekträndern)

    Aber selbst dann bleibt noch ein weiteres Problem: Punkte die sehr unscharf sind strahlen auch auf scharfe Punkte in ihrer Nähe aus und überstrahlen diese dadurch teilweise recht stark. Dies ist oben im zweiten Bild zu erkennen. Die Objektränder scheinen, trotz ihres unterschiedlichen Abstands, fast gleichmäßig unscharf zu sein. Diese Unschärfe kommt aber hauptsächlich durch die Überstrahlung durch den durchweg maximal unscharfen Hintergrund (das Problem wird dadurch verstärkt, dass der Hintergrund sehr hell ist). Um dieses Problem etwas abzumildern verwende ich folgenden Trick: die Gaussfunktion wird mit einem Faktor multipliziert, so dass breitere Verteilungen noch weiter abgeflacht werden, was zur Folge hat, dass schärfere Punkte weniger überstrahlt werden können:

    Adaptiertes_Gewicht(x,y) = Gewicht(x,y) * sqrt(Gewicht(0,0)

    Also Normierungsfaktor dient der Maximalwert der Gewichtsfunktion, der ja an der Stelle (0,0) auftritt. Die Verwendung der Wurzel dieses Wertes mildert das ganze nur wieder etwas ab. Andere Ideen (z.B. Adaption der Gewichte je nachdem wie die Werte in der Nachbarschaft sind) hatten teilweise zur Folge, dass in der Umgebung scharfer Objektteile z.B. auch der Hintergrund schärfer wurde. Dies ist bei dem oben beschriebenen Verfahren, wie man im rechten Beispiel sieht, nicht der Fall.

    Beide Workarounds entbehren sicherlich weitgehend jeder physikalischen Grundlage, ergeben aber recht ansehnliche Resultate...

Anwendungen
>Die 3D-Fraktale zu deren Visualisierung der Algorithmus ursprünglich entstanden ist sind hier näher beschrieben. Später habe ich dann auch noch Objekte aus Bezier-Flächen damit gerendert, wie hier beschrieben ist (einige der erzeugten Bilder sind auf den Seiten auch zu sehen). Programme (3D-Fraktal-Generator, Bezier-Flächen-Programm, 3D-Text-Erzeugungsprogramm), die den hier beschriebenen Renderer benutzen sind hier zum Download bereitgestellt.
Fußnoten
1)
Übrigens werden die Lichtstrahlen dabei in der umgekehrten Richtung "verfolgt", was den großen Vorteil hat, dass man nur die Lichtstrahlen tatsächlich berechnet, die wirklich das "Auge" erreichen.
2)
Es wird dabei einfach ein rechteckiger Bereich von xi = [x-radius...x+radius] und yi = [y-radius...y+radius] abgearbeitet. Die Bezeichnung "Radius" ist also etwas irreführend. Die "Rundheit" der Unschärfe kommt allein durch die Form der zweidimensionalen Normalverteilung zur Gewichtung.
3)
Wobei eine Normierung der Summe aller Gewichte des Gausschen Weichzeichners für einen Punkt auf 1 vorausgesetzt wird.
Feedback
Freue mich über eure Meinungen, Anregungen, usw.
Mail-Adresse.

Feedback-Formular
Patrick Rammelt, 11/2010