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...
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:
|
|
|
|
|
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:
|
|
|
|
|
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 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.
|
|
|
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).
|
|
|
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.
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.
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
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... :-)
|
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
|
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)
|
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) |
|
|
|
|
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 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...
>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.
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.
|