Arbeit als PDF - Fraunhofer-Institut für Techno
Transcription
Arbeit als PDF - Fraunhofer-Institut für Techno
Diplomarbeit Verbesserung der Darstellungsqualität einer texturbasierten Volumenvisualisierung unter Verwendung moderner Shadertechnologie Oliver Nacke Mat.Nr.:4200148 Rudolf-Breitscheidt Straße 83 67655 Kaiserslauten <olivernacke@gmx.de> 23. April 2006 I E R K L Ä R U N G • Die Diplomarbeit entstand in Zusammenarbeit mit einer Institution außerhalb der Fachhochschule Oldenburg / Ostfriesland / Wilhelmshaven. • Soweit meine Rechte berührt sind, erkläre ich mich einverstanden, dass die Diplomarbeit Angehörigen der Fachhochschule Oldenburg / Ostfriesland / Wilhelmshaven für Studium / Lehre / Forschung uneingeschränkt zugänglich gemacht werden kann. EIDESSTATTLICHE E R K L Ä R U N G Hiermit erkläre ich an Eides statt, dass ich die vorliegende Diplomarbeit bis auf die offizielle Betreuung selbst und ohne fremde Hilfe angefertigt habe und die benutzten Quellen und Hilfsmittel vollständig angegeben sind. Datum, Unterschrift II Diplomarbeit im Studiengang Informatik der Fachhochschule Oldenburg Ostfriesland Wilhelmshaven von: Oliver Nacke Rudolf-Breitscheidt Straße 83 67655 Kaiserslautern Telefon: 0177/8296349 e-Mail: olivernacke@gmx.de erstellt am: Fraunhofer Institut Technound Wirtschaftsmathematik Fraunhofer-Platz 1 67663 Kaiserslautern Betreuung Fachhochschule: Prof. Dr.-Ing. Dietrich Ertelt Betreuung Diplomarbeitsstelle: Dipl.-Inform. (FH) Falco Hirschenberger Dr. Katja Schladitz Inhaltsverzeichnis Erklärung I 1 Einleitung 1.1 Ziele der Volumenvisualisierung . . . . . . . . . . . . . . . . . 1.2 Flexibilität durch programmierbare Grafikhardware . . . . . . 1 1 3 2 Grundlagen 2.1 Beleuchtung und Schatten . . . . . . . . . . . . . . . . . . 2.1.1 Licht und Materie . . . . . . . . . . . . . . . . . . . 2.1.2 Die BRDF . . . . . . . . . . . . . . . . . . . . . . . 2.1.3 Der differentielle Raumwinkel . . . . . . . . . . . . 2.1.4 Definition einer BRDF . . . . . . . . . . . . . . . . 2.1.5 Kategorien und Eigenschaften von BRDFs . . . . . 2.1.6 Die BRDF Beleuchtungsgleichung . . . . . . . . . . 2.2 Reflexionsmodelle und Schattierung in der Computergrafik 2.2.1 Die ambiente Komponente . . . . . . . . . . . . . . 2.2.2 Die diffuse Komponente . . . . . . . . . . . . . . . 2.2.3 Die spiegelnde Komponente . . . . . . . . . . . . . 2.2.4 Die Schattierung . . . . . . . . . . . . . . . . . . . 2.3 Globale und lokale Beleuchtungsmodelle . . . . . . . . . . 2.3.1 Das Radiosity Verfahren . . . . . . . . . . . . . . . 2.3.2 Raytracing . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Das Phong Modell . . . . . . . . . . . . . . . . . . 2.4 Schatten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Schatten und Lichtquelle . . . . . . . . . . . . . . . 2.4.2 Schattenberechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 6 6 7 8 9 10 11 12 13 13 14 16 18 19 20 20 21 22 22 3 Techniken 3.1 Shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Der Vertexprozessor . . . . . . . . . . . . . . . . . . . 3.1.2 Der Fragmentprozessor . . . . . . . . . . . . . . . . . . 25 25 26 30 III . . . . . . . . . . . . . . . . . . . INHALTSVERZEICHNIS 3.2 3.3 3.4 3.5 IV 3.1.3 Shadersprachen . . . . . . . . . . . . . . . . . . . . . Anwendung des Beleuchtungsmodells auf das Volumen . . . 3.2.1 Polygonbasiertes Rendering und texturbasiertes Volumenrendering . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Per Pixel lighting einer texturbasierten Volumenvisualisierung mit dem Beleuchtungsmodell von Phong . . Schattengenerierung . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Strahlenverfolgung . . . . . . . . . . . . . . . . . . . 3.3.2 Shadow Volumes . . . . . . . . . . . . . . . . . . . . 3.3.3 Shadow Maps . . . . . . . . . . . . . . . . . . . . . . Isoflächen Rendering . . . . . . . . . . . . . . . . . . . . . . Progressives Rendering . . . . . . . . . . . . . . . . . . . . . . 32 . 33 . 34 . . . . . . . 35 38 38 39 40 44 47 4 Implementierung 4.1 MAVI und das Voxel-Sculpture Modul 4.2 Die Klassenstruktur . . . . . . . . . . . 4.2.1 Die Shaderklassen . . . . . . . . 4.2.2 Die Renderklassen . . . . . . . 4.2.3 Progressives Rendering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 50 51 52 55 69 5 Ergebnisse 5.1 Messungen . . . . . . . . . . . 5.1.1 Die Beleuchtungsklasse 5.1.2 Die Isoflächenklasse . . 5.1.3 Auswertung . . . . . . 5.1.4 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 77 79 80 80 85 . . . . . . 87 87 89 91 93 95 97 A Gallerie A.1 Sinterkupfer . . . . A.2 Schädel . . . . . . A.3 Feuerbeton . . . . A.4 Motorblock . . . . A.5 Menschlicher Kopf A.6 Aluminiumschaum Abbildungsverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 Quelltextverzeichnis 101 Literaturverzeichnis 102 Kapitel 1 Einleitung 1.1 Ziele der Volumenvisualisierung Der Bereich Volumenvisualisierung befasst sich mit der grafischen Darstellung dreidimensionaler Datensätze. Solche Datensätze werden meist mit bildgebenden Verfahren wie der Kernspintomographie (MRI, Magnetic Resonance Imaging) oder der Computertomographie (CT, Computed Tomography) erzeugt. Die Hauptanwendungsgebiete liegen in der medizinischen Diagnostik und der Qualitätskontrolle. Damit der Mensch in der Lage ist diese Daten zu erfassen, müssen sie ihm in geeigneter Weise präsentiert werden. ” Ein Bild sagt mehr als tausend Worte.“ Wie schon dem obigen Sprichwort zu entnehmen, ist der Mensch visuell orientiert. Anstatt die Daten auf komplizierte Weise zu beschreiben, wird einfach ein Bild des Objekts geliefert, was durch die Daten beschrieben wird. Dadurch ist der Mensch in der Lage auch riesige Datensätze schnell und intuitiv zu erfassen. Je detaillierter und hochwertiger die Darstellung ist, desto mehr Informationen können aus ihr gewonnen werden. Seit der Antike (ca. 1200v.Chr. - 600n.Chr.) ist beispielsweise bekannt, dass die Verteilung von Licht und Schatten helfen kann, Tiefeninformationen zu vermitteln. In der Malerei werden Licht und Schatten ganz bewusst eingesetzt um dem flachen Bild eine gewisse Plastizität und Tiefenwirkung zu verleihen. Leonardo da Vinci (1452-1519) beschrieb als erster die Prinzipien, nach denen Licht und Schatten in der Malerei einzusetzen sind. Die Darstellung einer Szene durch den Künstler auf einer Leinwand, unterscheidet sich nur im Bezug auf die verwendeten Mittel von der Darstellung ei- 1 KAPITEL 1. EINLEITUNG 2 ner Szene auf dem Computerbildschirm. Die Darstellungsqualität einer, vom Computer berechneten, Volumenvisualisierung profitiert genauso von dem Einsatz von Licht und Schatten wie ein Gemälde. Das Hauptproblem bei der Darstellung liegt in der Grösse der Daten. Eine Grösse von 5123 Werten pro Datensatz ist keine Seltenheit, woraus ein enormer Rechenaufwand bei der Darstellung resultiert. Aus diesem Grund war es bis vor wenigen Jahren nicht möglich, solche Datensätze auf herkömmlicher PC-Hardware mit akzeptabler Geschwindigkeit, auch ohne Beleuchtung, zu realisieren. Durch die rasche Entwicklung der Grafikkarten in den letzten Jahren, wurden diese so leistungsfähig, dass sie für die Volumenvisualisierung eingesetzt werden können. Das stellt einen grossen Vorteil dar, da keine teuren Spezialsysteme angeschafft werden müssen. Die Entwicklung der Grafikprozessoren lässt sich am eindrucksvollsten anhand einer vor 40 Jahren von Gordon E. Moore aufgestellten These zeigen, welche besagt dass sich die Anzahl der Transistoren auf einem Chip alle 24 Monate verdoppelt. Diese These ist durch die Entwicklung der CPUs1 längst bewiesen, wurde aber durch die modernen GPUs2 heutiger Grafikkarten sogar überboten. Aktuelle Grafikchips besitzen wesentlich mehr Transistoren als aktuelle CPUs3 . Die Basis für diese Arbeit bildet eine bereits vorhandene texturbasierte Volumenvisualisierung. Die Möglichkeiten dieser Visualisierung sind sehr begrenzt und die Qualität der Darstellungen somit vergleichsweise eher mässig. Hauptziel dieser Arbeit ist es, den visuellen Eindruck zu verbessern und weiterhin eine akzeptable Geschwindigkeit zu erzielen. Zur Verbesserung der Darstellung wird die Visualisierung um Beleuchtung erweitert. Durch die Beleuchtung wird die Darstellung der Oberfläche eines Objekts maßgeblich verbessert. Ohne Beleuchtung wirkt diese häufig verwaschen, so dass feine Strukturen nicht erkennbar sind. Aus der Beleuchtung ergeben sich auch Schatten, welche das Objekt auf sich selbst wirft. Durch Schatten wird die räumliche Darstellung verbessert. Schatten helfen dabei, die räumlichen Beziehungen der Strukturen eines Objekts zueinander herzustellen. Eine weitere Möglichkeit der Darstellung besteht darin, Objekte halbtransparent zu visualisieren. Dadurch können eingeschlossene Teile eines Objekts bequem innerhalb ihres Gesamtkontexts gezeigt werden. 1 Central Processing Unit Graphics Processing Unit 3 Zum Vergleich: Der NV40 Grafikchip einer GeForce 6800 Ultra besteht aus 222 Millionen Transistoren, ein Intel Pentium 4 Prescott“ dagegen nur aus 125 Millionen ” 2 KAPITEL 1. EINLEITUNG 1.2 3 Flexibilität durch programmierbare Grafikhardware Die ständige Weiterentwicklung der Grafikkarten erhöhte vor allem deren Geschwindigkeit. Dadurch stieg die Datenmenge, die verarbeitet werden kann. Aufgrund der steigenden Leistung, konnten theoretisch auch rechenintensivere Algorithmen, zum Beispiel für die Beleuchtungsberechnung, eingesetzt werden. Praktisch funktionierte das aber nur bedingt, da die Grafikkarten nicht programmierbar waren. Im Jahre 2001 führte NVidia[NV] mit der GeForce3 und ATI[ATI] mit der Radeon 8500 die ersten programmierbaren Grafikprozessoren ein. Man war fortan in der Lage, Grafikkarten flexibel über kleine Programme, den sogenannten Shadern zu programmieren. Dadurch entstand die Möglichkeit, eigene Algorithmen zu implementieren und von der Grafikhardware ausführen zu lassen. Die Möglichkeiten sind hierbei sehr vielfältig. Einige Beispiele, um einen Eindruck der bestehenden Möglichkeiten zu erhalten, zeigt Abbildung 1.1. KAPITEL 1. EINLEITUNG 4 Abbildung 1.1: Die Bilder zeigen einige mögliche Verwendungszwecke für Shader. Bump Mapping und Reflexion (o.li.), Cell Shader (Toon Shader) (o.re.), Realistische Wassereffekte (u.li.), Lichtbrechung und Reflexion (u.re.) Quelle: http://www.nvidia.com, Cell Shader: http://xiii.ubisoft.de KAPITEL 1. EINLEITUNG 5 Die Idee der Shader ist dabei keineswegs neu, sie stammt aus den Studios der Animationsfilme. Die Firma Pixar schuf in den späten 80er Jahren für ihr Rendering-Interface Renderman[Pix] eine eigene Shadersprache, die Renderman Shading Language. Diese Anwendung war jedoch für das Berechnen einzelner Bilder eines Films gedacht. Dabei spielt die Rechenzeit nur eine untergeordnete Rolle. Einige bekannte Filmtitel sind etwa Toy Story oder Finding Nemo. Bei Grafikkarten besteht das Ziel darin, diese Shader möglichst schnell auszuführen um flüssige Animationen zu erhalten. Diese Arbeit beschäftigt sich mit der Verbesserung der Darstellungsqualität einer texturbasierten Volumenvisualisierung. Dazu werden verschiedene Methoden zur Realisierung von Beleuchtung und Schatten für klassische, polygonbasierte, Darstellungen vorgestellt. Dabei wird untersucht, ob sich diese Verfahren auch für die Volumenvisualisierung eignen und welche Anpassungen vorgenommen werden müssen. Ferner wird eine Methode zur direkten Darstellung sogenannter Isoflächen vorgestellt, mit der sich Volumen auch halbtransparent visualisieren lassen. Programmierbare Grafikkarten stellen dabei ein nützliches Werkzeug für die Realisierung dar. Um weiterhin eine Visualisierung mit akzeptabler Geschwindigkeit zu ermöglichen, wird ein Verfahren vorgestellt, mit dem die Geschwindigkeit temporär, zu lasten der Bildqualität, erhöht werden kann. Kapitel 2 Grundlagen 2.1 Beleuchtung und Schatten Ziel der Volumenvisualisierung ist es, dreidimensionale Daten so darzustellen, dass der Anwender die gewünschten Informationen direkt erfassen kann. Dies muss nicht immer auf eine besonders realitätsnahe Visualisierung herauslaufen, sondern auch Methoden wie zum Beispiel die Darstellung in Falschfarben können den Informationsgehalt erhöhen. Wird aber eine möglichst realistische Visualisierung gewünscht, dann spielt die Beleuchtung dabei eine entscheidende Rolle. Insbesondere die Oberflächenstruktur eines Objektes kommt erst durch eine geeignete Beleuchtung zur Geltung. Auch die dabei entstehenden Schatten können helfen, die Struktur eines Objektes besser zu erfassen. 2.1.1 Licht und Materie Die Beleuchtung eines Objekts ist eine Licht-Materie Interaktion. Diese Interaktion ist im allgemeinen sehr komplex und hängt von vielen physikalischen Parametern, sowohl des Lichts, als auch der Materie ab. Ein Spiegel reflektiert das Licht völlig anders als Schmirgelpapier. Eine allgemeine LichtMaterie Interaktion ist in Abbildung 2.1 skizziert. Wenn Licht von einem Medium in ein anderes wechselt, können drei verschiedene Effekte auftreten: Reflexion, Absorbtion und Transmission. Reflektiertes Licht kann dabei, abhängig von den Eigenschaften der Materie, in bestimmte Richtungen unterschiedlich stark gestreut werden. Absorbiertes Licht wird von der Materie in Form von Energie aufgenommen. Transmittiertes Licht ist der Anteil des einfallenden Lichts, der die die Materie durchdringt. Beim 6 KAPITEL 2. GRUNDLAGEN 7 Reflexion Absorption Transmission Abbildung 2.1: Licht-Materie Interaktion allgemein Austritt aus der Materie treten obige Effekte wieder auf, da das Licht wieder das Medium wechselt. Da Licht eine Form von Energie ist, gilt der Energieerhaltungssatz. L i = L r + La + L t (2.1) Li ist das gesamte Licht das auf die Oberfläche trifft, Lr ist der reflektierte Anteil, La der absorbierte Anteil und Lt der transmittierte Anteil. Für opake1 Materialien wird der grösste Teil des einfallenden Lichts reflektiert und absorbiert. Was ein Betrachter sieht ist das reflektierte Licht. Der Anteil des reflektierten Lichts wird durch die sogenannte Bidirectional Reflectance Distribution Function (im Folgenden BRDF abgekürzt) bestimmt [Wyn]. 2.1.2 Die BRDF Der Anteil des reflektierten Lichts hängt maßgeblich von der Position der Lichtquelle und des Betrachters, relativ zur Normalen der beleuchteten Fläche, ab. Daraus resultiert, dass die BRDF eine Funktion der einfallenden2 und ausfallenden3 Richtung, relativ zum Interaktionspunkt ist. Ausserdem werden verschiedene Wellenlängen (Farben) des Lichts unterschiedlich stark reflektiert, absorbiert und transmittiert. Zwangsläufig muss die BRDF also auch eine Funktion der Wellenlänge sein. Eine letzte wichtige Eigenschaft ist die unterschiedliche Beschaffenheit des Materials an verschiedenen Punkten, auch bezeichnet als positional variance. Aufgrund der unterschiedlichen Zusammensetzung des Materials an verschiedenen Stellen wird Licht dort auch unterschiedlich reflektiert. Als Beispiel kann man sich Holz vorstellen. Die 1 Opazität ist das Gegenteil der Transparenz Von der Lichtquelle kommend 3 Zum Betrachter hin 2 KAPITEL 2. GRUNDLAGEN 8 Maserung variiert häufig sehr stark. Allgemein kann die BRDF folgendermaßen beschrieben werden: BRDFλ (Θi , φi , Θr , φr , u, v) (2.2) Der Index λ kennzeichnet die Abhängigkeit von der Wellenlänge. Θi , φi bestimmen die Einfallsrichtung und Θr , φr die Reflexionsrichtung in sphärischen Koordinaten. u und v bestimmen die aktuelle Position in Texturkoordinaten. Dadurch kann die Heterogenität des Materials nachgebildet werden, indem eine Textur benutzt wird, die die strukturelle Beschaffenheit des Objektes beschreibt. Häufig werden BRDFs aber auch ohne diese beiden Parameter beschrieben. Solche BRDFs werden als position-invariant oder shift-invariant bezeichnet: BRDFλ (Θi , φi , Θr , φr ) (2.3) 2.1.3 Der differentielle Raumwinkel Bisher wurde angenommen das das einfallende Licht aus genau einer RichW tung kommt. Licht wird aber gemessen als Energie pro Fläche ( m 2 ). Es ist daher zweckmäßiger anzunehmen, dass Licht aus einer kleinen Region von Richtungen kommt. ~n ωi dω x Abbildung 2.2: Licht trifft aus Richtung ωi auf den Interaktionspunkt x Der Raumwinkel dω beschreibt die Menge Licht, die auf den Punkt x trifft. Ein Raumwinkel ist die dreidimensionale Erweiterung eines zweidimensionalen Winkels. Der differentielle Raumwinkel kann als eine kleine rechteckige KAPITEL 2. GRUNDLAGEN 9 sinΘ dΘ dω Θ Φ dΦ Abbildung 2.3: Der differentielle Raumwinkel dω als Fläche auf der Einheitskugel Fläche auf der Einheitskugel verstanden werden: Abbildung 2.3 veranschaulicht die Zusammenhänge. dΘ und dφ sind kleine differentielle Änderungen des Winkels. Dadurch entsteht ein pyramidenförmiges Volumen das die Lichtmenge beinhaltet. Die Fläche die durch den Schnitt dieses Volumens mit der Einheitskugel entsteht ist der differentielle Raumwinkel dω: dω = sin Θ · dΘ · dφ (2.4) Die Einheit des Raumwinkels ist steradians (sr ). 2.1.4 Definition einer BRDF Betrachtet man eine Einfallsrichtung wi = (Θi , φi ) und eine Ausfallsrichtung wr = (Θr , φr ), relativ zu einem kleinen Flächenstück, dann ist die BRDF definiert als das Verhältnis von der reflektierten Strahlungsdichte Lr in Richtung wr zur Beleuchtungsstärke Ei aus Richtung wi : BRDFλ (wi , wr ) = Lr Ei (2.5) Da der differentielle Raumwinkel relativ klein ist, kann er als eine flache Fläche betrachtet werden. Diese wird gleichmäßig stark beleuchtet. Die Beleuchtungsstärke beträgt Li · dωi . Die Beleuchtungsstärke bezieht sich aber KAPITEL 2. GRUNDLAGEN 10 auf den differentiellen Raumwinkel und nicht auf die eigentlich zu beleuchtende Fläche. Um die Beleuchtungsstärke für die betrachtete Fläche zu erhalten, muss das einfallende Licht noch auf selbige projeziert werden. Dazu wird der Ausdruck mit cos Θi = ~n · w ~ i multipliziert (Abbildung 2.4). Für die BRDF aus Gleichung 2.5 ergibt sich: BRDFλ (wi , wr ) = Lr Li · cos Θi · dωi (2.6) ~n ω~i dω Θi da Abbildung 2.4: Projektion des Raumwinkels dω auf die relevante Fläche da 2.1.5 Kategorien und Eigenschaften von BRDFs Grundsätzlich unterscheidet man zwei Klassen von BRDFs, anisotropische und isotropische. Isotropische BRDFs beschreiben Reflexionseigenschaften die invariant bezüglich einer Rotation der Fläche um ihre Normale sind. Plastik hat häufig isotropische Reflexionseigenschaften. Im Gegensatz dazu beschreiben anisotropische BRDFs Reflexionseigenschaften die sich bzgl. einer Rotation der Fläche um ihre Normale ändern. Ein Beispiel hierzu wäre gebürstetes Metall. BRDFs die auf physikalischen Gesetzen beruhen besitzen darüber hinaus zwei weitere Eigenschaften: Umkehrbarkeit und Energieerhaltung. Umkehrbarkeit meint das der Wert der BRDF sich nicht ändert, wenn man die Einfalls- und Ausfallsrichtung vertauscht. Mathematisch kann diese Eigenschaft folgendermaßen ausgedrückt werden: BRDFλ (Θi , φi , Θr , φr ) = BRDFλ (Θr , φr , Θi , φi ) (2.7) Mit Energieerhaltung ist gemeint das die Menge des reflektierten Lichts die Menge des einfallenden Lichts nicht übersteigen darf. Allgemein ist es so, KAPITEL 2. GRUNDLAGEN 11 dass Licht das aus einer bestimmten Richtung auf eine Fläche trifft in verschiedene Richtungen gestreut wird. Die Summe des gestreuten Lichts darf also die Menge des einfallenden Lichts nicht überschreiten. Für die BRDF bedeutet das, dass die Summe aller Ausfallsrichtungen multipliziert mit dem projezierten Raumwinkel maximal 1 werden darf. Mathematisch ausgedrückt: Z BRDFλ (Θi , φi , Θr , φr ) · cos Θr dωr ≤ 1 (2.8) Ω R Ω bedeutet eine Integration über die gesamte Hemisphäre. 2.1.6 Die BRDF Beleuchtungsgleichung In der realen Welt ist es so, dass die gesamte Szene an der Beleuchtung eines Punktes auf einer Fläche beteiligt ist: Betrachter ωr Abbildung 2.5: Das gesamte einfallende Licht bestimmt die Intensität in Reflexionsrichtung ωr Abbildung 2.5 zeigt einen Punkt auf einer Fläche der von der gesamten Szene beleuchtet wird. Ein Beobachter ist in Richtung wr positioniert. Die Menge des Lichts das in Richtung wr reflektiert wird ist eine Funktion über sämtliche Einfallsrichtungen wi und der BRDF des Oberflächenpunkts. Für die Menge Lr des ausgehenden Lichts gilt also: Z Lr = Lri (wi , wr )dwi (2.9) Ω Lri (wi , wr ) ist dabei der Anteil des Lichts aus Richtung wi , das in Richtung wr reflektiert wird. Ω beschreibt wieder die gesamte Hemisphäre der einfallenden Lichtstrahlen. Für den diskreten Fall ergibt sich aus Gleichung 2.9: X Lr = Lri (wi , wr ) (2.10) in KAPITEL 2. GRUNDLAGEN 12 Der reflektierte Teil des Lichts aus einer Richtung wi ergibt sich genau aus der entsprechenden BRDF. Daher gilt für Lri : Lri = BRDF (Θi , φi , Θr , φr ) · Ei (2.11) Ei ist die Beleuchtungsstärke. Allerdings bezieht sie sich wieder auf den differentiellen Raumwinkel und nicht auf das beleuchtete Flächenstück. Um die korrekte Beleuchtungsstärke für das Flächenstück zu erhalten, muss wieder mit cos Θi = ~n · w ~ i projeziert werden (Siehe auch Abbildung 2.4): Ei = Li · cos Θi · dwi (2.12) beziehungsweise für den diskreten Fall: Ei = Li · cos Θi (2.13) Für die reflektierte Lichtmenge in Richtung des Betrachters wr aus Einfallsrichtung wi ergibt sich: Lri = BRDF (Θi , φi , Θr , φr ) · Li · cos Θi (2.14) Das gesamte reflektierte Licht, als Summe der reflektierten Lichtmengen aller Einfallsrichtungen wi , ist somit definiert als: X Lr = BRDF (Θi , φi , Θr , φr ) · Li · cos Θi (2.15) in Man kann sich Gleichung 2.15 als die Summe vieler Punktlichter vorstellen, wobei jede Punktlichtquelle genau eine Einfallsrichtung repräsentiert. Statt über die gesamte Hemisphäre zu integrieren, was sehr rechenaufwändig wäre, könnnen so einige wenige Punktlichtquellen benutzt, um die gesamte reflektierte Lichtmenge näherungsweise zu berechnen. Für eine einzige Punktlichtquelle ergibt sich das reflektierte Licht in Richtung des Betrachters aus: Lr = BRDF (Θi , φi , Θr , φr ) · Li · cos Θi (2.16) Gleichung 2.16 ist die allgemeine BRDF Beleuchtungsgleichung für eine einzelne Punktlichtquelle. 2.2 Reflexionsmodelle und Schattierung in der Computergrafik Um eine Visualisierung am Computer in Echtzeit zu ermöglichen, muss eine Möglichkeit gefunden werden die BRDF möglichst effizient zu berechnen. KAPITEL 2. GRUNDLAGEN 13 Häufig gilt: Je näher die BRDF an der physikalischen Realität liegt, desto aufwändiger ist es, sie zu berechnen. Eine Möglichkeit um eine hohe Realitätsnähe zu wahren und dennoch akzeptable Laufzeiten zu erzielen, liegt darin, die BRDF gar nicht erst als analytische Funktion zu modellieren, sondern eine Tabelle mit real gemessenen Daten als Grundlage für einen Lookup zu nehmen. Die andere Möglichkeit besteht darin, einfachere Modelle zu verwenden. Einfache Modelle sind häufig empirischer1 Natur und spiegeln die physikalische Realität somit nur bedingt wieder. Man geht also immer einen Kompromiss zwischen Realitätsnähe auf der einen und verfügbarer Rechenleistung auf der anderen Seite ein. Durch die stetig steigende Rechenleistung heutiger CPUs und insbesondere GPUs verschiebt sich dieser Kompromiss aber zusehends in Richtung Realitätsnähe. Gerade in einfacheren Modellen wird die Reflexion häufig getrennt in drei unabhängigen Komponenten berechnet: Der ambienten Komponente, der diffusen Komponente und der spiegelnden Komponente. 2.2.1 Die ambiente Komponente Die ambiente Komponente bestimmt den Teil des reflektierten Lichts der indirekt auf das Objekt trifft. In der Realität ist es so, dass ein Objekt nicht nur direkt von einer Lichtquelle beleuchtet wird, sondern auch vom emittierten/gespiegelten Licht anderer Objekte. Man stelle sich dazu einen Raum vor, der nur ein Fenster hat. Licht das durch dieses Fenster auf den Boden des Raums trifft erhellt zunächst den Boden direkt. Das Licht wird aber vom Boden reflektiert und beleuchtet so auch die Decke und die Wände mit abgeschwächter Intensität. Die ambiente Komponente bildet die Grundintensität. Ohne sie wären Objekte, die nicht direkt von einer Lichtquelle bestrahlt werden, schlicht schwarz. 2.2.2 Die diffuse Komponente Bei einer diffusen Reflexion wird Licht, abhängig vom Einfallswinkel, gleichverteilt in alle Richtungen reflektiert. Die Intensität der Reflexion ist somit abhängig vom Einfallswinkel des Lichts, aber unabhängig vom Betrachtungswinkel, da gleichverteilt in sämtliche Richtungen. Die diffuse Reflexion kann mit dem Lambertschen Kosinusgesetz beschrieben werden: Ird = Ir0 · cos φ = Ir0 · (n~0 · l~0 ) (2.17) Die diffus reflektierte Intensität Ird ist abhängig vom Winkel φ zwischen der normierten Flächennormalen n~0 und dem normierten Lichtvektor l~0 des 1 Empirische Modelle basieren auf Beobachtungen, nicht auf physikalischen Grundlagen KAPITEL 2. GRUNDLAGEN 14 einfallenden Lichts. Ir0 beschreibt die maximale reflektierte Intensität (für φ = 0). Da die Reflexion nur für Einfallwinkel 0 ≤ φ ≤ π2 definiert ist ergibt sich aus Gleichung 2.17: Ird = Ir0 · max((n~0 · l~0 ), 0) (2.18) ~n l~2 Betrachter l~1 φ2 φ1 v~2 v~1 Abbildung 2.6: Diffuse Reflexion Abbildung 2.6 veranschaulicht zwei diffuse Reflexionen mit jeweils unterschiedlichen Einfallswinkeln φ. Man erkennt das die reflektierte Intensität für zunehmende Einfallswinkel φ gemäß des Kosinus abnimmt. Die Halbkreise veranschaulichen die reflektierte Intensität für beide Einfallswinkel φ1 und φ2 . Beispielhaft sind die Vektoren v~1 und v~2 in Richtung des Betrachters skizziert. Abhängig vom Material des Objektes werden unterschiedliche Wellenlängen auch unterschiedlich stark reflektiert. Der diffuse Reflexionskoeffizient kd (λ) bestimmt wie stark eine bestimmte Wellenlänge reflektiert wird und ist somit eine Funktion der Wellenlänge, definiert als f : A → B, mit A ∈ IR und B ∈ [0, 1] (2.19) Somit erweitert sich Gleichung 2.18 zu Ird (λ) = Ir0 (λ) · kd (λ) · max((n~0 · l~0 ), 0) 2.2.3 (2.20) Die spiegelnde Komponente Die spiegelnde Komponente erzeugt sogenannte Glanzlichter (Highlights) auf der Oberfläche. Dabei gilt: Je glatter die Oberfläche des Objekts, desto stärker die Intensität des Glanzlichts. Bei dem Glanzlicht handelt es sich um KAPITEL 2. GRUNDLAGEN 15 eine Spiegelung der Lichtquelle auf der Oberfläche des Objekts. Im Gegensatz zur diffusen Reflexion ist die spiegelnde Reflexion blickwinkelabhängig, da eine Spiegelung immer richtungsgebunden ist. Bekannte Modelle wie das von Phong[Pho75] verwenden folgende Gleichung zur Berechnung des Glanzlichts: Irs = coss ρ = (~ r0 · v~0 )s (2.21) r~0 ist der normierte Reflexionsvektor und v~0 der normierte Vektor in Richtung des Betrachters. ρ ist der aufgespannte Winkel beider Vektoren. Der Exponent s wird häufig als shininess bezeichnet. Er bestimmt die Grösse des Glanzlichts. Der Begriff shininess ist eigentlich irreführend, da der Faktor nicht die Materialeigenschaft der Fläche beschreibt, sondern die Grösse des Glanzlichts betimmt. Objekte mit kleinem Glanzlicht wirken aber spiegelnder als Objekte mit grossem Glanzlicht, daher hat der Begriff shininess eine breite Akzeptanz in der Literatur gefunden. Die spiegelnde Reflexion ist, analog zur diffusen Reflexion, ebenfalls nur für Winkel 0 ≤ ρ ≤ π2 definiert, daher ergibt sich aus Gleichung 2.21: Irs = (max(~ r0 · v~0 ), 0)s ~n (2.22) ~r ~l φ φρ Betrachter ~v Abbildung 2.7: Spiegelnde Reflexion für s = 1, 2, 10, 20, 40, 80, 160 In Abbildung 2.7 werden die Intensitäten der spiegelnden Reflexion für verschiedene Werte für s veranschaulicht. Je kleiner der Wert s, desto stärker nimmt die Intensität mit steigendem Winkel ρ ab. Der Vektor ~v ist beispielhaft für eine Reflexion mit s = 2 eingezeichnet. Ebenfalls wird deutlich das die Intensität am stärksten ist, wenn die Blickrichtung gleich dem Vektor ~r ist, also ρ = 0. Für die spiegelnde Reflexion existiert ebenfalls ein Reflexionskoeffizient ks , KAPITEL 2. GRUNDLAGEN 16 welcher die Menge des gespiegelten Lichts angibt. Er ist definiert als ks ∈ [0, 1] (2.23) Im Gegensatz zum diffusen Reflexsionskoeffizient, ist er unabhängig von der Wellenlänge, damit das Glanzlicht immer in der Farbe der Lichtquelle erscheint. Folglich ergibt sich aus Gleichung 2.22: Irs = ks · (max(~ r0 · v~0 ), 0)s 2.2.4 (2.24) Die Schattierung Bisher wurde nur erläutert wie der Farbwert (das reflektierte Licht) eines Oberflächenpunktes berechnet werden kann. Allerdings wurde noch nicht genauer spezifiziert, wie das Beleuchtungsmodell auf die Polygone eines Objekts angewendet wird. Dieser Vorgang wird als Schattierung (shading) bezeichnet und bestimmt letztlich die Farbe eines jeden Pixels. Es existieren drei gängige Schattierungsverfahren: Flat shading, Gouraud shading und Phong shading. Flat Shading Komplexes dreidimensionale Modelle setzen sich immer aus einzelnen Dreicken zusammen, welche die Grafikhardware weiterverarbeitet. Dieses Vorgehen hat viele Vorteile, insbesondere da drei Punkte immer eine Fläche im dreidimensionalen Raum beschreiben. Dadurch wird es ermöglicht, Werte, die pro Vertex1 angegeben werden, wie Flächennormalen, linear über eine Dreiecksfläche zu interpolieren. Beim Flat Shading wird das Reflexionsmodell immer pro Dreieck angewendet, das heisst es wird die Normale der Dreiecksfläche bestimmt und mit dem Reflexionsmodell verrechnet. Der daraus resultierende Farbwert wird dann für das gesamte Dreieck verwendet. Das Flat Shading zeichnet sich durch seine hohe Effizienz aus. Für jedes Dreieck muss nur einmal die Flächennormale bestimmt und das Reflexionsmodell angewendet werden. Die beiden großen Nachteile des Verfahrens sind das, insbesondere bei gekrümmten Oberflächen, facettenhafte Aussehen und die sehr schlechte Darstellung von Glanzpunkten. 1 Eckpunkt eines Polygons KAPITEL 2. GRUNDLAGEN 17 ~n ~v1 4 ~v2 Abbildung 2.8: Flat Shading: Das Linke Bild schematisiert das Prinzip für ein einzelnes Polygon ∆. Das rechte Bild zeigt das Resultat am Beispiel. Gouraud Shading Beim Gouraud Shading wird das Reflexionsmodell für jedes Vertex angewendet. Die resultierenden Farbwerte werden dann über die gesamte Dreiecksfläche linear interpoliert. Gouraud Shading hat eine ähnliche Effizienz wie das Flat Shading, liefert aber wesentlich ansprechendere Resultate. Das facettenhafte Aussehen, des Flat Shading, entfällt durch die lineare Interpolation vollständig. Insgesamt wird die Reflexionsgleichung für jedes Dreieck dreimal (an jedem Vertex) angewendet. Die lineare Interpolation der Farbwerte über die Dreiecksfläche wird bei heutigen Grafikkarten vollständig in der Hardware ausgeführt. ~n1 ~n2 ~i ~v1 4 ~v2 Abbildung 2.9: Gouraud Shading: Das Linke Bild schematisiert das Prinzip für ein einzelnes Polygon ∆. Das rechte Bild zeigt das Resultat am Beispiel. Beim Gouraud Shading entfällt die facettenhafte Erscheinung, allerdings werden Glanzpunkte nicht sauber dargestellt, da sie in die Farbinterpolation benachbarter Dreiecksflächen mit einbezogen werden. Ebenfalls kann es passieren das Glanzlichter gar nicht dargestellt werden, falls diese genau mittig in ein Dreieck fallen und kein Vertex berühren. KAPITEL 2. GRUNDLAGEN 18 Phong Shading Das qualitativ hochwertigste Schattierungsmodell ist das Phong Shading, nicht zu verwechseln mit dem Phong Beleuchtungsmodell. Ein Schattierungsmodell gibt immer an wie ein Beleuchtungsmodell auf ein Objekt angewendet wird1 . Beim Phong Shading werden zunächst die Normalen der Vertices einer Dreiecksfläche bestimmt. Diese werden dann über die Dreiecksfläche linear interpoliert. Das Reflexionsmodell wird dann für jedes Pixel anhand der interpolierten Normalen angewendet. Man spricht auch von per pixel lighting. ~n1 ~n2 ~n ~v1 4 ~v2 Abbildung 2.10: Phong Shading: Das Linke Bild schematisiert das Prinzip für ein einzelnes Polygon ∆. Das rechte Bild zeigt das Resultat am Beispiel. Das Phong Shading bietet die höchste Qualität, hat aber auch eine vielfach schlechtere Laufzeit gegenüber den anderen beiden Modellen. Glanzlichter werden akkurat dargestellt, da die Beleuchtung pro Pixel berechnet wird. Es ist prinzipiell möglich, gleichwertige Bilder mittels Gouraud Shading zu erzielen, indem man das Modell so fein macht das die Grösse eines einzelnen Dreiecks kleiner ist als die eines Pixels. Das ist aber nicht praktikabel, da der Aufwand dreimal so hoch wäre wie beim echten Phong Shading, da ja für jedes Dreieck (welches nun die Größe eines Pixels hat) drei Normalen berechnet werden (eine pro Ecke) auf die dann das Reflexionsmodell angewendet wird. Beim Phong Shading wird das Reflexionsmodell nur einmal pro Pixel angewendet. 2.3 Globale und lokale Beleuchtungsmodelle Grundsätzlich unterscheidet man in der Computergrafik zwei Arten von Beleuchtungsmodellen, den globalen und den lokalen Modellen. Lokale Beleuchtungsmodelle lassen die Interaktion zwischen Objekten ausser 1 Man kann z.B. das Phong Beleuchtungsmodell zusammen mit Gouraud Shading verwenden. Das Standarbeleuchtungsmodell von OpenGL verwendet das Phong Beleuchtungsmodell wahlweise mit Flat- oder Gouraud Shading KAPITEL 2. GRUNDLAGEN 19 Acht. Sie werden auch als first-order Modelle bezeichnet, da sie nur die direkte, also erste Reflektion des Lichtstrahls, ausgehend von einer Lichtquelle, betrachten. Licht das von einem anderen Objekt reflektiert wurde, wird nicht weiter berücksichtigt. Die ambiente Komponente wird in lokalen Modellen also nicht berücksichtigt. Damit unbeleuchtete Objekte nicht völlig schwarz erscheinen wird die ambiente Komponente häufig durch eine Konstante, die Grundhelligkeit, ausgedrückt. Durch die Mißachtung der Objektinteraktionen sind Spiegelungen oder Schatten mit lokalen Beleuchtungsmodellen nicht direkt realisierbar. Insbesondere in Echtzeitanwendungen werden häufig lokale Beleuchtungsmodelle aufgrund ihres besseren Laufzeitverhaltens eingesetzt. Globale Beleuchtungsmodelle unterscheiden sich von den lokalen Modellen dadurch, dass die Interaktion von verschiedenen Objekten untereinander mit berücksichtigt wird. Objekte interagieren durch Reflexion, Transparenz oder Schatten miteinander. Globale Beleuchtungsmodelle sind wesentlich rechenaufwändiger, da sie diese Interaktionen berücksichtigen müssen. Im Folgenden werden einige bekannte globale und lokale Beleuchtungsverfahren vorgestellt. 2.3.1 Das Radiosity Verfahren Das Radiosity Verfahren beruht auf dem Energieerhaltungssatz und stammt ursprünglich aus der Thermodynamik. Es ist neben dem Raytracing eines der bekanntesten globalen Beleuchtungsmodelle. Dabei wird die gesamte Szene in Flächen (Patches) aufgeteilt. Die Lichtmenge die eine Fläche emittiert, ist die Differenz zwischen der empfangenen und absorbierten Menge Licht. Insbesondere kann eine Fläche auch selbstleuchtend sein, also eine Lichtquelle repräsentieren. Es wird davon ausgegangen das eine Fläche, Licht immer ideal diffus, also gleichverteilt in alle Richtungen, emittiert (Lambertsche Fläche). Ziel ist es ein Gleichungssystem zu lösen, dessen Gleichungen die emittierte Lichtmenge, und somit die Helligkeit, einer jeden Fläche beschreibt. Der aufwändigste Teil ist die Bestimmung sogenannter Formfaktoren. Ein Formfaktor beschreibt die ausgetauschte Strahlung zwischen zwei Flächen. Dazu muss die Sichtbarkeit und Ausrichtung sämtlicher Flächen zueinander bestimmt werden[WW92]. Ein Vorteil des Verfahrens ist seine Blickwinkelunabhängigkeit. Das Gleichungssystem muss nur einmal berechnet werden, danach lässt sich die Szene in Echtzeit rendern. Nachteile sind vor allem die hohe Vorverarbeitungszeit sowie die mangelnde Unterstützung für spiegelnde und transparente Objekte. Das Radiosity Verfahren wurde weitestgehend von modernen Raytracingverfahren verdrängt, findet aber noch Anwendung insbesondere im Echtzeitrendering von Architekturmodellen. KAPITEL 2. GRUNDLAGEN 2.3.2 20 Raytracing Raytracing ist das wohl bekannteste globale Beleuchtungsverfahren. Beim Raytracing werden Strahlen genutzt um die Szene abzutasten. Für jedes darzustellende Pixel wird ein Strahl, ausgehend vom Betrachter, durch das entsprechende Pixel der Projektionsebene, in die Szene geschickt und verfolgt. Trifft der Strahl auf ein Objekt, so kann er, abhängig von den Eigenschaften des Objektes, gebrochen, reflektiert oder absorbiert werden. Für diese Berechnungen wird wieder auf lokale Verfahren zurückgegriffen. Diese Strahlen können rekursiv weiter verfolgt werden (rekursives Raytracing). Die Vorteile liegen beim Raytracing vor allem in seiner leichten Erweiterbarkeit und prinzipiellen Parallelisierbarkeit. 2.3.3 Das Phong Modell Das Phong Modell zählt zu den bekanntesten lokalen Beleuchtungsmodellen. Obwohl es an die Wellentheorie des Lichts angelehnt ist handelt es sich bei dem Phong Modell um ein empirisches Modell. Die reflektierte Intensität einer Fläche entspricht der Summe der ambienten, diffusen und spiegelnden Komponente, die Grundformel lautet: ~ ·N ~ ) + Iin · ks · ((R ~ · V~ )s ) Ir = Ia · ka + Iin · kd · (L (2.25) Die Summanden repräsentieren die ambiente, die diffuse und die spiegelnde Komponente. Die ambiente Komponente besteht aus der Intensität des ambienten Lichts, Ia , sowie dem ambienten Reflexionskoeffizienten ka . Da es sich beim Phong Modell um ein lokales Beleuchtungsmodell handelt, wird für die ambiente Komponente eine Konstante verwendet. Die diffuse Komponente ist ein Produkt aus der Intensität des einfallenden Lichts Iin , dem diffusen Reflexionskoeffzienten kd und dem Winkel zwischen ~ und Flächennormale N ~ , ausgedrückt durch das Skalarprodukt Lichtvektor L beider Vektoren. Die spiegelnde Komponente ist das Produkt aus der Intensität des einfallenden Lichts Iin , dem Reflexionskoeffzienten ks für spiegelnde Reflexion und ~ und der Richtung dem Winkel zwischen der Reflexionsrichtung des Lichts R ~ ~ ~ des Betrachters V . Das Skalarprodukt R · V wird noch mit s, der shininess potenziert. Dadurch wird die Grösse des Glanzlichts kontrolliert (siehe auch Kapitel 2.2). KAPITEL 2. GRUNDLAGEN 21 Die Formel kann auch grafisch veranschaulicht werden: Abbildung 2.11: Phong Komponenten (Quelle: http://en.wikipedia.org/wiki/Phong%5Fshading) 2.4 Schatten Schatten sind ein weiterer wichtiger Bestandteil der Beleuchtung. Die Bedeutung von Schatten für die Wahrnehmung einer Szene wird einem oft erst bewusst, wenn diese nicht vorhanden sind. Schatten ermöglichen es erst, die räumliche Beziehung von Objekten untereinander zu erkennen. Auch lassen Schatten Rückschlüsse auf die Richtung vorhandener Lichtquellen zu. Wie wichtig Schatten für die Wahrnehmung sind verdeutlicht Abbildung 2.12. a) b) c) Abbildung 2.12: Dieselbe Szene mit verschiedenen Schatten: a) Die Szene ohne Schatten. b) Die Szene mit Schatten direkt unterhalb des Objekts. c) Die Szene mit Schatten weiter entfernt vom Objekt. In Bild a) ist es unmöglich eine Aussage bezüglich der räumlichen Anordnung des Torus zur Ebene zu machen. In Bild b) ist deutlich zu erkennen, dass der Torus auf der Ebene liegt und in Bild c) schwebt der Torus weit über der Ebene. Die Bilder unterscheiden sich lediglich durch die Position KAPITEL 2. GRUNDLAGEN 22 des Schattens. Ferner tritt bei konkaven Objekten noch die sogenannte Selbstschattierung auf, das heisst das Objekt wirft Schatten auf sich selbst. Während das Fehlen von Objektschatten unrealistisch wirkt (die Objekte scheinen ohne konkreten Bezugspunkt im Raum zu schweben), bedeutet das Fehlen von Selbstschattierung eine falsche Darstellung der konvexen Oberfläche aufgrund der falschen Beleuchtung. Die Darstellung von Schatten im Zusammenhang mit Beleuchtung ist somit ein wichtiger Bestandteil, um eine intuitivere und korrekte Visualisierung zu ermöglichen. 2.4.1 Schatten und Lichtquelle Mit Schatten ist eine Fläche gemeint die nicht beleuchtet wird, weil sie von einem anderen Objekt verdeckt wird. Theoretisch muss diese Fläche dann absolut schwarz sein. Praktisch ist das aber fast nie der Fall. Das liegt daran, dass in der Realität Licht praktisch aus allen Richtungen kommt, entweder direkt von einer Lichtquelle, oder indirekt über Reflexion. Daher werden Objekte in der Realität meist nur teilweise schattiert. Man spricht von Kernschatten und Halbschatten (weiche Schatten). Kernschatten sind Gebiete die vollständig schattiert sind und Halbschatten entsprechend die Gebiete die nur teilweise schattiert sind. Selbst wenn nur eine einzige Lichtquelle existiert ergeben sich Halbschatten sobald diese Lichtquelle eine räumliche Ausdehnung hat: Abbildung 2.13 skizziert die Zusammenhänge zwischen der Grösse der Lichtquelle und der Schattierung. Je kleiner die Lichtquelle, desto kleiner wird auch das Verhältnis von Halbschatten zu Kernschatten. Wird eine Punktlichtquelle verwendet, existieren gar keine Halbschatten. Punktlichter werfen immer harte Schatten. 2.4.2 Schattenberechnung Der Begriff Schattenberechnung macht, im Gegensatz zur Reflektionsberechnung, zunächst wenig Sinn, da Schatten ein natürlicher Bestandteil globaler Beleuchtung sind. Sie ergeben sich direkt aus der Reflexionsberechnung, da auf eine schattierte Fläche entsprechend wenig Licht einfällt und dementsprechend umso weniger Licht reflektiert wird. Daraus resultiert, dass die Fläche wesentlich dunkler erscheint als eine gut beleuchtete Fläche. In der Computergrafik finden aber überwiegend lokale Beleuchtungsmodelle Verwendung. Da lokale Beleuchtungsmodelle die Objektinteraktion unberücksichtigt lassen, ist es nicht möglich Schatten direkt darzustellen. Da Szenen ohne Schatten unrealistisch wirken und im Falle der Selbstschattie- KAPITEL 2. GRUNDLAGEN 23 a1) a2) a3) b1) b2) b3) Abbildung 2.13: Kernschatten und Halbschatten in Abhängigkeit zur Größe der Lichtquelle rung sogar falsch dargestellt werden, wurden Algorithmen entwickelt, die eine getrennte Schattenberechnung ermöglichen. Ein Algorithmus zur Schattenberechnung muss im wesentlichen zwei Aufgaben erfüllen: • Bestimmung der Schattenregion • Bestimmung der Intensität des Schattens In einigen Sonderfällen ist die Bestimmung der Schattenregion recht einfach. Ein solcher Fall wäre die Bestimmung einer Schattenregion auf einer planen Fläche. Dabei werden die beteiligten Objekte auf eben diese Fläche projeziert. Im allgemeinen Fall ist es aber so, dass beliebige Objekte Schatten auf beliebige andere Objekte und, im Falle der Selbstschattierung, auch auf sich selbst werfen können. Dadurch erhöht sich die Komplexität enorm. Der zweite Punkt, die Intensität des Schattens, hängt davon ab, wieviel Licht trotz Schattenwurf noch auf das Gebiet trifft. Dies kann indirekt reflektiertes Licht sein. Aber auch die Grösse der Lichtquelle spielt eine Rolle, wie in Kapitel 2.4.1 gezeigt. Häufig wird die Intensitätsberechnung der Schattenregion auch vernachlässigt und stattdessen nur eine konstante ambiente Intensität verwendet. Damit die Schattenberechnung nicht zu komplex wird, werden häufig Einschränkungen gemacht. Oft wird eine Punktlichtquelle, also eine Lichtquelle KAPITEL 2. GRUNDLAGEN 24 ohne räumliche Ausdehnung, als Berechnungsgrundlage verwendet. Die indirekte Beleuchtung, also die ambiente Komponente, wird auch häufig ausser Acht gelassen und als konstant angenommen. Als Konsequenz ergeben sich harte Schatten. Eine Übersicht über verschiedene Schattengenerierungsalgorithmen wird in Kapitel 3.3 gegeben. Im folgenden Kapitel werde einige Verfahren vorgestellt, mit denen die behandelten Grundlagen umgesetzt werden können. Das resultierende Laufzeitverhalten stellt dabei das Hauptkriterium dar. Um die Interaktivität insgesamt zu erhöhen wird das progressive Rendering eingeführt. Ferner wird noch ein Verfahren zur direkten Visualisierung sogenannter Isoflächen vorgestellt. Kapitel 3 Techniken In diesem Kapitel werden verschiedene Techniken für die jeweiligen Aufgaben wie Beleuchtung, Schattengenerierung oder der Isoflächendarstellung erläutert. Es werden jeweils die Vor- und Nachteile diskutiert und begründet warum ein bestimmtes Verfahren für die Implementierung verwendet wurde. Als Basis sämtlicher Verfahren dient die 3d-Grafikbibliothek OpenGL [GL]. Da die meisten Aufgaben über Shader realisiert sind, wird ebenfalls eine Einleitung in diese Technik gegeben. 3.1 Shader Als Shader werden Programme bezeichnet, die vollständig auf der GPU der Grafikhardware ausgeführt werden. Shader können dazu dienen, Objekte zu deformieren, oder die Farbe einzelner Fragmente zu bestimmen. Als Fragment wird dabei ein Pixel bezeichnet, dem noch kein Farbwert zugeordnet ist. Dieser wird erst in einer weiteren weiteren Verarbeitungsstufe, dem Shading, anhand von Texturen oder Beleuchtungsgleichungen ermittelt. Moderne Grafikkarten arbeiten wie eine Pipeline, das heisst es existieren verschiedene Verarbeitungsstufen die in einer festen Reihenfolge abgearbeitet werden. Die Daten werden zwischen diesen einzelnen Stufen weitergereicht. Die konkrete Realisierung dieser Pipeline in Hardware ist Sache der Hersteller und geheim. Der logische Ablauf ist in Abbildung 3.1 dargestellt. Der Grafikkartentreiber stellt die Abstraktion zwischen Hardware und OpenGL her. Die Eingabedaten bestehen aus Vertexdaten, den OpenGL-Primitiven. Zusätzlich können jedem Vertex noch zusätzliche Informationen mitgegeben werden, wie etwa die Farbe oder eine Normale. Diese Daten durchlaufen die verschiedenen Verarbeitungsstufen, Vertexdaten können rotiert, skaliert oder translatiert werden. In einem weiteren Schritt werden diese Daten dann 25 KAPITEL 3. TECHNIKEN 26 rasterisiert, also in Fragmente zerlegt, eventuell mit Texturen versehen und im Framebuffer abgelegt. Dieser sehr grobe Ablauf ist in [Hir05] genauer erläutert. Die ersten 3d-Beschleunigerkarten waren nicht programmierbar. Man spricht auch von der sogenannten fixed function pipeline, da alle Algorithmen fest in der Hardware verankert waren. Der Nachteil dieser festen Pipeline ist die fehlende Flexibilität. Mit der fortschreitenden Entwicklung der Grafikkarten wurden diese auch immer flexibler. Der Grafikkartenhersteller NVidia führte mit der GeForce256 die Register-Combiner ein. Diese erlaubten es die OpenGL-Textureinheit zu umgehen und stattdessen eine Reihe hintereinandergeschalteter Registeroperationen durchzuführen. Auf diese Weise waren komplexe Verknüpfungen zwischen verschiedenen Texturen möglich. Allerdings kann man bei den Register-Combinern noch nicht von einer frei programmierbaren Einheit sprechen. Vielmehr bestehen sie aus einer Auswahl verschiedener, fest programmierter, aber frei parametrisierbarer Operationen. Aktuelle Grafikkarten besitzen zwei programmierbare Einheiten, eine programmierbare Geometrieeinheit (Vertexprozessor) und eine programmierbare Rasterisierungseinheit (Fragmentprozessor). 3.1.1 Der Vertexprozessor Die Transformation und Beleuchtung der Vertices wurde ursprünglich auf der CPU ausgeführt. Um die CPU zu entlasten wurde die T&L-Einheit (transform and lighting) eingeführt. Diese führt die entsprechenden Berechnungen in der Hardware auf der Grafikkarte aus. Das hat den Vorteil, dass die Transformation und Beleuchtung nun sehr schnell berechnet werden kann, bringt aber gleichzeitig den Nachteil das die Beleuchtungsgleichungen fest in der Hardware verankert sind. Um zum Einen Flexibilität zu gewährleisten und zum Anderen die die CPU zu entlasten, wurde der Vertexprozessor eingeführt. Beim Vertexprozessor handelt es sich um eine frei programmierbare Geometrieeinheit auf der Grafikkarte. Er ersetzt die klasssische T&L-Einheit. Der Vertexprozessor ist über sogenannte Vertexshader frei programmierbar. Beim Vertexshader handelt es sich um ein kleines Programm, welches auf der Grafikkarte vom Vertexprozessor ausgeführt wird. Der Vertexshader wird für jedes Vertex genau einmal ausgeführt. Es können dabei weder neue Vertices erzeugt, noch vorhandene Vertices gelöscht werden. Es besteht nun die Möglichkeit entweder die klassische fest verdrahtete T&L- KAPITEL 3. TECHNIKEN 27 Abbildung 3.1: Schematischer Aufbau der Grafikpipeline Einheit zu verwenden oder einen Vertexshader. Beides in Kombination ist nicht möglich. Wird ein Vertexshader verwendet, muss er daher neben der eigentlichen Aufgabe zusätzlich die Funktion der T&L-Einheit erfüllen. Im einfachsten Fall bedeutet das, er muss mindestens die Vertextransformation durchführen. Die T&L-Einheit führt, je nach Bedarf, folgende Operationen durch: • Vertextransformation (Multiplikation mit der Modelview- und Projektionsmatrix) • Normalentransformation und Normalisierung • Generierung von Texturkoordinaten • Texturkoordinatentransformation • Beleuchtungsberechnungen für jedes Vertex • Anwendung von Farbmaterialwerten (für die Beleuchtung) KAPITEL 3. TECHNIKEN 28 Dabei hat der Vertexshader immer nur Kenntnis vom aktuellen Vertex. Geometrieoperationen die Kenntnis anderer Vertices voraussetzen, wie Frustumoder Userclipping werden nicht durch die Vertexeinheit ersetzt. Unter OpenGL muss ein view-frustum definiert werden. Dabei handelt es sich um ein kegelförmiges Objekt1 , das den Sichtbereich der Kamera darstellt. Alle Objekte innerhalb dieses Kegels werden gerendert. Liegt ein Objekt sowohl innerhalb, als auch ausserhalb dieses Bereichs, wird es entsprechend abgeschnitten. Man spricht dabei von clipping. Zusätzlich können weitere Flächen definiert werden, an denen ein Objekt abgeschnitten werden soll. Man spricht dann von user clip planes. Da die Vertexeinheit eine Verarbeitungsstufe in der Renderingpipeline ist, erhält sie bestimmte Daten von der vorherigen Stufe und muss auch bestimmte Daten an die nächste Stufe weitergeben. Die Vertexeinheit erhält ihre Daten direkt von der Applikation. Im einfachsten Fall bestehen die Daten nur aus der Position des aktuellen Vertex. Je nach verwendeten OpenGL Funktionen können aber noch zusätzliche Parameter zugänglich gemacht werden wie Vertexnormalen, Texturkoordinaten oder die Farbe des diffusen und Glanzlichts. Zusätzlich können dem Vertexshader noch benutzerdefinierte Parameter zugänglich gemacht werden. Vertexshader können dazu benutzt werden um Objekte zu deformieren, indem die Position des Vertex in bestimmter Weise manipuliert wird. Die Ergebnisse der Berechnungen werden den weiteren Stufen der Pipeline (und somit auch dem Fragmentprozessor) zugänglich gemacht. Abbildung 3.2 zeigt schematisch den Aufbau eines DirectX 8.0 konformen Vertexprozessors. Modernere Vertexprozessoren arbeiten nach demselben Schema, nur besitzen sie häufig mehr Register und einen erweiterten Befehlssatz. Der Vertexprozessor erhält seine Daten über die Eingaberegister v0..v15. Sämtliche Register haben eine Breite von 128 Bit. Diese werden in 4*32 Bit float Werte unterteilt. Jedes Register kann somit einen Vektor, bestehend aus vier 32 Bit float Komponenten, speichern. Dieses Datenformat ist sehr nützlich, da Transformationen üblicherweise mit 4x4 Matrizen durchgeführt werden. Auch bestehen Vertexkoordinaten tatsächlich aus 4 Komponenten. Das hat den Grund, dass Transformationen wie Rotation und Skalierung zwar mit einer zusammengefassten 3x3 Matrix durchgeführt werden können, die Translation aber nicht. Eine 4x4 Matrix ermöglicht sämtliche Transformationen mit einer einzigen zusammengefassten Matrix[Len02]. Damit diese 1 Für die orthogonale Projektion ist das Objekt quaderförmig. KAPITEL 3. TECHNIKEN 29 Abbildung 3.2: Schematische Darstellung eines Vertexprozessors 4x4 Matrix auf die dreidimensionalen Koordinaten angewendet werden kann, werden diese künstlich erweitert. Die 4. Komponente nimmt dabei meist den Wert 1 an. Die Eingaberegister enthalten die Koordinaten des aktuellen Vertex, sowie dessen Farbwert. Zusätzlich können sie, abhängig von der Applikation, weitere Werte wie etwa Vertexnormalen oder Farbwerte für diffuses Licht enthalten. Der Vertexprozessor kann diese Register ausschliesslich lesen. Zusätzlich ist es möglich weitere, beliebige, Parameter an den Vertexprozessor zu schicken. Diese werden im Konstantenregister gehalten. Auch hier hat der Vertexprozessor ausschliesslich Leserechte, allerdings ist es möglich das Konstantenregister mit Hilfe des Adressregisters indirekt zu addressieren. Die temporären Register dienen zur Speicherung von Zwischenergebnissen, der Vertexprozessor kann sie sowohl lesen als auch schreiben. Daten die an die nächste Verarbeitungsstufe geschickt werden müssen, werden im Ausgaberegister abgelegt. Dies sind in jedem Fall die transformierten Vertexkoordinaten, aber es können auch andere Daten weitergeleitet werden. Der Vertexshader kann beispielsweise dazu benutzt werden, Texturkoordinaten zu berechnen. Diese können weitergeleitet werden um sie einem Fragmentshader zugänglich zu machen. Der Fragmentprozessor erhält dann für jedes Fragment automatisch die interpolierten Texturkoordinaten vom Vertexprozessor. In die Ausgaberegister kann ausschliesslich geschrieben werden. Die Anzahl der Ausgaberegister variiert abhängig von der verwendeten Gra- KAPITEL 3. TECHNIKEN 30 fikhardware. 3.1.2 Der Fragmentprozessor Beim Fragmentprozessor handelt es sich um eine weitere frei programmierbare Einheit auf der Grafikkarte. Er kann anstelle der fest verdrahteten Multitextureinheit moderner Grafikkarten benutzt werden. Programme die auf dem Fragmentprozessor ausgeführt werden, heissen Fragmentshader (auch Pixelshader genannt). Aufgabe des Fragmentshaders ist es, die Farbe und Transparenz eines Fragments zu bestimmen. Dies kann über Texturen geschehen, wobei auch mehrere Texturen verknüpft werden können, oder es können komplexe Beleuchtungsberechnungen pro Pixel durchgeführt werden. Analog zum Vertexshader wird ein Fragmentshader genau einmal für jedes Fragment ausgeführt. Auch besitzt der Fragmentshader keinerlei Informationen über andere Fragmente ausser dem aktuell behandelten. Die Eingabedaten erhält der Fragmentshader von der vorherigen Verarbeitungsstufe der Grafikpipeline. Auf diese Weise ist es auch möglich, dem Fragmentshader Ausgabedaten eines Vertexshaders zugänglich zu machen. Zum Beispiel kann der Vertexprozessor Vertexnormalen linear über eine Dreiecksfläche interpolieren und in einem speziellen Register speichern. Der Fragmentshader kann nun auf dieses Register zugreifen und erhält so automatisch die interpolierte Normale für den aktuellen Pixel. Auf diese Weise lässt sich zum Beispiel eine Beleuchtung pro Pixel realisieren, bei dem für jedes Pixel die entsprechende Normale bekannt sein muss, da diese üblicherweise in die Beleuchtungsberechnung mit einfliesst (Kapitel 2.2.4). Der Vorteil liegt darin, das die lineare Interpolation der Normalen im Vertexprozessor praktisch keine Rechenzeit kostet. In der Tat ist es häufig so, dass man Vertex- und Fragmentshader als Einheit betrachtet um die höchste Effizienz zu erzielen, da der Vertexshader wesentlich seltener ausgeführt wird (pro Vertex) als der Fragmenshader (pro Fragment). Bei Daten die linear über eine Fläche interpoliert werden können, beispielsweise Texturkoordinaten oder Flächennormalen, macht es also durchaus Sinn, diese in einem Vertexprogramm zu generieren. Diese werden dann automatisch über die Dreiecksfläche interpoliert und den weiteren Verarbeitungsstufen der Pipeline zugänglich gemacht. Zusätzlich können dem Fragmentshader benutzerdefinierte Parameter aus der Applikation übergeben werden. Abbildung 3.3 zeigt den schematischen Aufbau eines DirectX 8.0 konformen Fragmentshaders. Modernere Fragmentshader besitzen eine höhere Anzahl KAPITEL 3. TECHNIKEN 31 Register, einen erweiterten Befehlssatz und können bis zu 8 Texturen adressieren. Abbildung 3.3: Schematische Darstellung eines Fragmentprozessors Hauptaufgabe des Fragmentshaders ist es, die Farbe für ein Fragment zu bestimmten. Dazu erhält er die Grundfarbe des aktuellen Fragments von der vorherigen Verarbeitungsstufe über die nur lesbaren Farbregister v0 und v1. Der Zugriff auf Texturen erfolgt über die Texturregister tn. Sie enthalten eine Referenz auf die jeweilige Textur. Beim Zugriff auf eine Textur wird der entsprechende Texel1 automatisch, mittels entsprechendem Filter, zwischen den benachbarten Pixeln rekonstruiert. Als Filter stehen meist der Box-Filter (nearest-neighbour Interpolation) und der Tent-Filter (lineare Interpolation) zu Verfügung. In den Konstantenregistern werden die benutzerspezifischen Parameter gespeichert. Der berechnete Farbwert des Fragments wird in das Ausgaberegister geschrieben. Dieses dient gleichzeitig als temporäres Register, so dass sowohl Leseals auch Schreibzugriffe möglich sind. 1 Texture Element KAPITEL 3. TECHNIKEN 3.1.3 32 Shadersprachen Die ersten frei programmierbaren Vertex- und Fragmentprozessoren waren noch sehr beschränkt in ihrem Funktionsumfang. Die entsprechenden Shader wurden in Assembler programmiert. Das genügte zunächst, da die Shader nur eine begrenzte (kurze) Anzahl an Instruktionen enthalten durften. Ein Vertexshader der ersten Generation unter DirectX 8.1 [MDX] durfte aus maximal 128 Befehlen bestehen und der Sprachumfang bestand aus 17 verschiedenen Instruktionen [AMH02]. Ebenso kannten die ersten Shader keine Statements zur Flußkontrolle, wie etwa if, for, while oder goto. Mit dem Erscheinen immer neuerer Grafikkarten wuchs auch der Umfang der entsprechenden Shaderfunktionalität immer weiter und die Nachteile der in Assembler programmierten Shader wurden offensichtlich. Moderne Shader können mittlerweile eine beliebige Anzahl an Instruktionen enthalten. Ebenso werden konditionale Statements wie if-Anweisungen und Schleifen unterstützt. Auch können heutige Shader Unterprogramme aufrufen. In Assembler geschriebene Shader haben den Nachteil, dass sie eine längere Entwicklungszeit benötigen und schwerer zu lesen und zu warten sind. Ein weiterer entscheidender Nachteil ist, dass sie nicht portabel sind. Mittlerweile existieren drei gängige Hochsprachen für die Shaderprogrammierung. Im Funktionsumfang sind sie sich recht ähnlich, auch die Syntax ist bei allen drei Sprachen stark an C angelehnt, jedoch hat jede Sprache ihre Vor- und Nachteile. Die OpenGL Shading Language Die OpenGL Shading Language (GLSL) [KBR] ist seit der Version 2.0 Bestandteil von OpenGL. GLSL kann somit nur in Verbindung mit OpenGL benutzt werden. Eine Integration in eine andere Grafik-API wie DirectX ist nicht möglich. Diesem Nachteil steht der Vorteil gegenüber, dass Shader die in GLSL geschrieben wurden direkten Zugriff auf wichtige OpenGL Variablen haben, wie den diversen Matrizen oder Beleuchtungsparametern. Die High Level Shading Language Die High Level Shading Language (HLSL) [HLS] wurde von Microsoft[MS] entwickelt und ist Bestandteil von DirectX seit Version 9.0. HLSL kann ausschliesslich in Verbindung mit DirectX benutzt werden und ist somit auch an das Betriebssystem Windows gebunden. Plattformunabhängige Shader, wie sie mit GLSL möglich sind (sofern die Zielplattform OpenGL 2.0 unterstützt), sind mit HLSL nicht möglich. KAPITEL 3. TECHNIKEN 33 C for Graphics C for Graphics, oder kurz Cg [CG05] ist eine von NVidia entwickelte Hochsprache für Shader. Der Name deutet bereits an das die Sprache stark an die Programmiersprache C angelehnt ist. Cg zeichnet sich vor allem durch die hohe Universalität aus. Cg Shader sind nicht an eine bestimmte Grafikbibliothek gebunden. Derzeit können Cg Shader sowohl mit DirectX als auch mit OpenGL benutzt werden, dabei wird der Shader für ein bestimmtes Profil kompiliert. Es existieren Profile für sämtliche DirectX und OpenGL Shaderversionen. Der Entwickler muss natürlich sicherstellen, dass die Shader die gegebenen Beschränkungen des jeweiligen Profils nicht überschreiten. Um die Plattformunabhängigkeit zu realisieren benötigen Cg Shader eine Laufzeitbibliothek. Die Laufzeitbibliothek besteht aus zwei Teilen, der Core Cg Runtime und einer Anbindung an die entsprechende Grafikbibliothek, also derzeit entweder die Direct3D Cg Runtime (für DirectX) oder der OpenGL Cg Runtime (für OpenGL). Auf diese Weise können die Shader auch direkt von der Installation einer neueren (optimierten) Cg Runtime profitieren. Es existiert die Möglichkeit, Shader erst zur Laufzeit zu kompilieren, dadurch kann das beste vorhandene Profil der Zielplattform genutzt werden. Sämtliche während dieser Arbeit entwickelten Shader sind mit Cg realisiert. 3.2 Anwendung des Beleuchtungsmodells auf das Volumen Die in Kapitel 2.1 vorgestellten Verfahren zur Beleuchtung und Schattierung beziehen sich auf polygonbasierte dreidimensionale Objekte. In dieser Arbeit müssen die Verfahren aber auf ein texturbasiertes Volumenrendering angewendet werden. Dazu ist es unerlässlich, die grundsätzlichen Unterschiede zwischen beiden Renderingverfahren zu kennen. Dazu wird im nächsten Kapitel das Prinzip des texturbasierten Volumenrenderings kurz erläutert und auf die Unterschiede zum polygonbasierten Rendering eingegangen. Im darauf folgenden Kapitel wird erläutert, wie die auftretenden Probleme gelöst werden können. KAPITEL 3. TECHNIKEN 3.2.1 34 Polygonbasiertes Rendering und texturbasiertes Volumenrendering Beim klassischen polygonbasierten Rendering werden dreidimensionale Objekte anhand von geometrischen Primitiven im Raum modelliert. Man spricht auch vom sogenannten Drahtgittermodell (engl. Wireframe). Jedem Vertex können zusätzliche Informationen mitgegeben werden, wie Farbwerte, Texturkoordinaten oder Normalenvektoren. Dadurch erhält das Objekt seine Farbe und kann bei Bedarf beleuchtet werden. Es ist wichtig festzuhalten, dass die Objektgeometrie der tatsächlichen Form des Objektes entspricht. Abbildung 3.4 verdeutlicht den Zusammenhang zwischen der Modellgeometrie und dem gerenderten Objekt am Beispiel einer Kugel. a) b) Abbildung 3.4: Bild a) zeigt die Geometrie des Objekts und b) das fertig gerenderte Objekt. Das texturbasierte Volumenrendering dagegen verfolgt einen anderen Ansatz. Das dreidimensionale Objekt wird in Scheiben (Slices) zerlegt. Diese Scheiben werden als Texturen verwendet und hintereinander von hinten nach vorne gerendert. Die einzelnen Texturscheiben werden mit einem geeigneten Operator verknüpft (blending). Die Objektgeometrie stimmt dabei nicht mit der Form des gerenderten Objekts überein. Die Objektgeometrie besteht vielmehr nur aus einzelnen Rechtecken, auf denen die Texturen gerendert werden. Das genaue Verfahren des texturbasierten Volumenrenderings ist in [Hir05] erläutert. Zum Vergleich zeigt Abbildung 3.5 die Objektgeometrie und das gerenderte Objekt beim texturbasierten Volumenrenderung. Die Objektgeometrie bildet den entscheidenden Unterschied zwischen beiden Verfahren. Daraus resultiert, dass sämtliche angewandten klassischen Algorithmen, wie Beleuchtung oder Schattengenerierung, auf diesen Unterschied KAPITEL 3. TECHNIKEN a) 35 b) Abbildung 3.5: Bild a) zeigt die Geometrie des Objekts1 und b) das fertig gerenderte Objekt. hin angepasst werden müssen. 3.2.2 Per Pixel lighting einer texturbasierten Volumenvisualisierung mit dem Beleuchtungsmodell von Phong Um das Reflexionsmodell von Phong (siehe Kapitel 2.3.3) auf jedes Pixel anzuwenden bedarf es die Kenntnis einiger Parameter: • Die Normale ~n des zu beleuchtenden Pixels. • Die Position ~l der Lichtquelle. • Die Position des Betrachters ~v . • Weitere Parameter wie die Farbe der Lichtquelle, die Farbe des zu beleuchtenden Materials, etc. Die meisten der obigen Parameter werden entweder vorgegeben (Farbe der Lichtquelle, Position der Lichtquelle, Farbe des Materials, etc.) oder können aus den Parametern der aktuellen Szene ermittelt werden (Blickrichtung des Betrachters). Dei Hauptaufgabe besteht in der Bestimmung der Flächennormalen. Beim klassischen polygonbasierten Rendering geht man so vor, dass die Normale pro Vertex in der Applikation mit angegeben wird. Die Geometrieeinheit der Grafikkarte interpoliert diese Normalen linear zwischen den Vertices. Auf 1 Aus Gründen der Übersicht wurde nur jede 20. Scheibe gerendert. KAPITEL 3. TECHNIKEN 36 diese Weise hat man im Fragmentshader Zugriff auf die (interpolierte) Normale eines jeden Fragments. Dieses Verfahren entspricht dem Phong Shading aus Kapitel 2.2.4. Beim texturbasierten Volumenrendering macht genau das keinen Sinn, weil die Objektgeometrie, wie im vorherigen Kapitel beschrieben, keinerlei Bezug zur tatsächlichen Objektform hat. Die Objektgeometrie besteht ja nur aus einzelnen Scheiben auf denen die Texturen gerendert werden. Die Normalen dieser Scheiben wären alle gleich und stünden in keinem Zusammenhang zu den Normalen des tatsächlichen Objekts. Die Beleuchtung würde also völlig falsch berechnet. Es existieren verschiedene Verfahren für die Normalenbestimmung von Volumendaten. Eine Methode ist das grey-level shading [Win02]. Das grey-level shading zeichnet sich durch eine hohe Präzision aus, bietet die Möglichkeit Normalen auch für halbtransparente Medien zu berechnen und ist Blickwinkelunabhängig. Der Nachteil ist das es für Daten versagt für die kein Gradient berechnet werden kann, beispielsweise Binärdaten[YCK]. Ein Normalenvektor für einen Punkt (x, y, z) eines diskreten Volumens V ist die partielle Ableitung von V nach x, y und z: ∇V (x, y, z) = ( ∂V ∂V ∂V , , ) ∂x ∂y ∂z (3.1) Bei einem dreidimensionalen Datensatz V (x, y, z) handelt es sich um diskrete Abtastwerte, den sogenannten Voxeln1 . Im Allgemeinen ist die Funktion V (x, y, z) nicht bekannt, daher wird der Gradient aus den diskreten Voxeln bestimmt. Wird der Gradient für eine Stelle benötigt, die nicht einem diskreten Abtastpunkt entspricht, muss ein Rekonstruktionsfilter verwendet werden um die Ableitung für jede Richtung zu bestimmen. Mit Hilfe des Sinc Filters lassen sich Signale ohne Informationsverlust rekonstruieren, solange bei der Abtastung das Nyquist-Theorem eingehalten wurde, welches besagt, dass die Abtastfrequenz eines Signals größer als die doppelte Bandbreite fg des Signals sein muss. Diskrete Voxeldaten entsprechen im Allgemeinen nicht dem Nyquist-Theorem, da sie nicht bandbegrenzt sind. Kanten von Polygonen oder Schattengrenzen erzeugen sehr hohe Frequenzen, die auch bei Erhöhung der Abtastfrequenz nicht optimal abgetastet werden können. Trotzdem bildet der Sinc Filter den optimalen Rekonstruktionsfilter. Er liefert eine stetige Funktion V 0 (x, y, z) die sich (leicht) von der Originalfunktion unterscheidet. Da der Gradient die partielle Ableitung der Funktion V (x, y, z) ist, ist der 1 von Volume Element in Anlehnung an das Pixel für Picture Element KAPITEL 3. TECHNIKEN 37 optimale Rekonstruktionsfilter ebenfalls die Ableitung des Sinc Filters, Cosc: cos (πx)−Sinc(πx) x 6= 0 x Cosc(x) = (3.2) 0 sonst Praktisch kann der Cosc Filter aber nicht verwendet werden, da er unendliche Ausdehnung besitzt. Eine andere Möglichkeit die Normalen zu bestimmen, ist die Methode der zentralen Differenzen. Dabei wird ∇V über die Differenz benachbarter Werte in der jeweiligen Dimension berechnet: • Zentrale Differenz: V (x + 1, y, z) − V (x − 1, y, z) ∂V = ∂x 2Dx (3.3) • Vorwärtsdifferenz: ∂V V (x + 1, y, z) − V (x, y, z) = ∂x Dx (3.4) • Rückwärtsdifferenz: ∂V V (x, y, z) − V (x − 1, y, z) = ∂x Dx (3.5) Die Distanz zweier benachbarter Pixel in x-Richtung beträgt Dx . Die Vorwärtsund Rückwärtsdifferenz wird an den Kanten des Datensets angewendet, da und ∂V werden dort keine entsprechenden Nachbarpixel existieren. Für ∂V ∂y ∂z die Formeln analog verwendet. Die Berechnung der Normalen mittels der zentralen Differenzen kann komplett im Fragmentshader realisiert werden. Der Volumendatensatz wird dem Shader in Form einer dreidimensionalen Textur zugänglich gemacht. Anschliessend wird die Beleuchtungsgleichung auf das Fragment angewendet. Um die Performance zu erhöhen kann man die Normalen auch in einem Vorverarbeitungsschritt berechnen und in Form einer Lookup-Tabelle speichern. Die Performance kann dadurch spürbar verbessert werden, allerdings benötigt eine solche Lookup-Tabelle auch sehr viel Speicher, da die Normale eines jeden Pixels gespeichert werden muss. Ein 8-Bit Grauwertbild mit den Dimensionen 5123 benötigt alleine schon 128MB Speicher. Würde man die Normalen in Form einer RGB-Textur mit den Dimensionen 5123 speichern, so würden zusätzliche 384MB Speicher benötigt. Diese Grössenordnungen stellen selbst moderne Grafikkarten vor Probleme. Letztlich entscheidet die Rechenleistung, die Größe des Volumendatensatzes und Speicherausstattung der Grafikkarte darüber, ob eine Berechnung zur Laufzeit sinnvoll ist. KAPITEL 3. TECHNIKEN 3.3 38 Schattengenerierung Im vorherigen Kapitel wurde erläutert wie die Normale zu jedem Fragment ermittelt werden kann um die Beleuchtung zu realisieren. Die gesamte Berechnung geschieht im Fragmentshader. Folglich wird die Beleuchtungsberechnung für jedes Fragment ausgeführt. Da es sich bei dem verwendeten Modell um ein lokales Modell handelt sind Schatten, wie in Kapitel 2.4.2 bereits erwähnt, nicht direkt realisierbar. Jedes Pixel des Volumens würde also beleuchtet werden, was zu einer falschen Darstellung, insbesondere bei konkaven Objekten (Kapitel 2.4) führt. Um eine Schattendarstellung zu realisieren muss eine Möglichkeit gefunden werden, im Fragmentshader zu entscheiden ob das aktuelle Fragment verdeckt ist (also im Schatten liegt) oder nicht. Es existieren eine Vielzahl von Schattengenerierungsalgorithmen, jeder mit spezifischen Vor- und Nachteilen. In der Praxis ist es so, dass auch tatsächlich eine Vielzahl dieser Algorithmen verwendet wird. Man kann also derzeit keinen klaren Favoriten nennen. Die folgenden Kapitel stellen einige Verfahren vor und zeigen die jeweiligen Vor- und Nachteile auf. 3.3.1 Strahlenverfolgung Das Prinzip der Strahlenverfolgung ist trivial. Für jedes Pixel wird ein Strahl zur Lichtquelle verfolgt. Wird dieser Strahl von einem anderen Objekt blockiert, dann liegt der betreffende Pixel im Schatten, andernfalls ist er beleuchtet. Um das Verfahren zu benutzen bedarf es keiner Vorverarbeitung der Daten. Das Hauptproblem dieses Algorithmus liegt in dem enormen Rechenaufwand. Für jedes Pixel muss ein Strahl gegen sämtliche Polygone (beziehungsweise Fragmente) der Szene getestet werden. Prinzipiell ist es möglich dieses Verfahren für die Schattengenerierung im Kontext einer texturbasierten Volumenvisualisierung zu verwenden. Aufgrund des hohen Aufwands ist dies aber nicht interaktiv möglich. Für das Echtzeitrendering bietet sich das Verfahren daher nicht an, allerdings existieren diverse Techniken um die Performance zu steigern, beispielsweise können light buffer verwendet werden um die Performance des Schattentests auf Kosten eines Vorverarbeitungsschritts und des zusätzlich benötigten Speichers zu senken [WW92]. KAPITEL 3. TECHNIKEN 3.3.2 39 Shadow Volumes Ursprünglich vorgestellt von Franklin Crow[Cro77], basiert diese Methode darauf, dass zu jedem Polygon ein sogenanntes Schattenvolumen (engl. shadow volume) erzeugt wird. Dazu wird für jede Kante eines Polygons jeweils ein Strahl von der Lichtquelle aus, durch die Kantenpunkte, Richtung Unendlich geschickt. Das Schattenpolygon erhält man, indem man die Strahlen in maximaler Entfernung abschneidet und durch eine Kante verbindet. Sämtliche Schattenpolygone eines Objektpolygons bilden das Schattenvolumen. Die Schattenpolygone werden nicht gezeichnet, sie werden aber wie gewöhnliche Polygone behandelt, das heisst sämtliche Transformationen werden auch auf ihnen ausgeführt. Die Entscheidung, ob ein Pixel im Schatten liegt, wird anhand eines Zählers ermittelt. Dabei wird die Strecke, ausgehend vom Beobachter, zum fraglichen Pixel hin untersucht. Jedesmal wenn man dabei auf die Vorderseite eines Schattenpolygons trifft wird der Zähler um 1 erhöht da ein Schattenvolumen betreten wird. Trifft man auf die Rückseite eines Schattenpolygons wird der Zähler entsprechend um 1 erniedriegt, da ein Schattenvolumen verlassen wurde. Trifft man auf den betreffenden Pixel und der Zähler ist gleich 0, dann ist der Pixel beleuchtet, andernfalls liegt er im Schatten. Bei der Initialisierung des Zählers ist darauf zu achten, dass er mit der Anzahl der Schattenvolumina initialisiert wird in denen sich der Betrachter befindet. Der Algorithmus in der obigen Grundform ist sehr rechenaufwändig, da sehr viele zusätzliche Polygone erzeugt werden. Es existieren viele Ansätze zur Verbesserung der Performance. Anstatt das Schattenvolumen für jedes einzelne Polygon zu berechnen, kann es für zusammenhängende Objekte berechnet werden [WW92]. Dazu muss die Silhouette des Objekts aus der Richtung der Lichtquelle bestimmt werden. Das Schattenvolumen wird dann für die Kanten der Silhouette berechnet. Dadurch kann die Zahl der Schattenpolygone beträchtlich reduziert werden, allerdings ist die Bestimmung der Silhouette nicht trivial. Aktuelle Implementationen verwenden auch häufig den stencil buffer für die Realisierung des Schattenzählers. Dieses Verfahren ist auch bekannt als Stencil Shadows, basiert aber ebenfalls auf Schattenvolumen. Mittels Schattenvolumen ist es möglich Schatten in Echtzeit zu realisieren. Für die interaktive Volumenvisualisierung scheidet das Verfahren dennoch prinzipiell aus, da die Schattenvolumen die zugrunde liegende Geometrie der Objekte für die Berechnung verwenden, diese aber bei der texturbasierten Volumenvisualisierung nicht vorhanden ist (siehe Kapitel 3.2.1). KAPITEL 3. TECHNIKEN 3.3.3 40 Shadow Maps Ein ebenfalls weit verbreitetes Verfahren ist die Verwendung von Shadow Maps [Wil78]. Um Shadow Maps zu verwenden wird ein Z-Buffer (auch als depth buffer bezeichnet) benötigt. Der Z-Buffer wurde ursprünglich entwickelt um einen pixelgenauen Sichtbarkeitstest beim Rendern einer dreidimensionalen Szene durchzuführen. Dazu verwendet man einen Buffer, der dieselben Dimensionen besitzt wie der Framebuffer. Der Z-Buffer wird zunächst mit dem maximalen Wert initialisiert. Nach der Projektion der Szene auf die Betrachtungsebene entspricht die ZKoordinate eines Pixels der Entfernung zum Betrachter, beziehungsweise der Projektionsebene. Die Z-Koordinate wird beim Rendern des Pixels mit dem korrespondierenden Wert im Z-Buffer verglichen. Ist die Z-Koordinate des Pixels kleiner als der Eintrag im Z-Buffer, dann bedeutet das, dass dieser Pixel näher am Betrachter ist. Er wird also gerendert und der Eintrag im Z-Buffer wird aktualisiert. Ist die Z-Koordinate grösser als der Eintrag im Z-Buffer, dann bedeutet das, dass es bereits einen Pixel gibt der näher am Betrachter ist und der aktuelle Pixel wird nicht gerendert. Nachdem die gesamte Szene gerendert wurde, enthält der Z-Buffer die Abstände sämtlicher gerenderter Pixel zum Betrachter. Der Ansatz der Shadow Map ist dabei folgender: Die Szene wird zunächst aus Sicht der Lichtquelle gerendert. Alle Pixel die dabei gerendert werden sind beleuchtet. Der Z-Buffer enthält nun die Entfernungen sämtlicher der Lichtquelle nächsten Pixel. Der gesamte Inhalt des Z-Buffers bildet die Shadow Map. Diese Informationen werden gespeichert und die Szene wird ein weiteres Mal, diesmal aus Sicht des Betrachters, gerendert. Dabei wird die ZKoordinate jedes zu rendernden Pixels mit dem entsprechenden Eintrag aus der Shadow Map verglichen. Ist der Wert in der Shadow Map gleich, handelt es sich um einen beleuchteten Pixel. Ist er kleiner, wird der Pixel von einem anderen verdeckt und ist nicht beleuchtet. Abbildung 3.6 zeigt das Vorgehen schematisch und die dabei erzeugten Resultate. Da die Pixel der Shadow Map im Projektionsraum der Lichtquelle liegen und die tatsächlich zu Rendernden im Projektionsraum des Betrachters, müssen die Koordinaten vor dem Vergleich noch transformiert werden: Abbildung 3.7 schematisiert die beteiligten Transformationen beim Shadowmapping. Der grüne Pfeil symbolisiert die Transformation der Koordinaten im Kameraraum zu dem der Lichtquelle, um den Vergleich mit der Shadow Map durchzuführen. In OpenGL besteht die Möglichkeit Texturkoordinaten automatisch zu ge- KAPITEL 3. TECHNIKEN 41 Lichtquelle Shadow Map Betrachter Lichtquelle Shadow Map a b va a) b) c) d) vb Abbildung 3.6: Bild a) zeigt wie die Shadow Map aus Sicht der Lichtquelle erzeugt wird. Bild c) zeigt die Shadow Map, hellere Pixel sind weiter von der Lichtquelle entfernt. Bild b) zeigt den Vergleich der Z-Werte. Das Objekt wird vom Betrachter in Punkt va getroffen. Der korrespondierende Eintrag in der Shadow Map, an Stelle a, entspricht dem Abstand des Punktes va . Der Punkt ist somit beleuchtet. Der Eintrag an Stelle b in der Shadow Map, für den korrespondierenden Punkt vb ist wesentlich kleiner als die Distanz zwischen vb und Lichtquelle, da dieser bereits von einem anderen Pixel verdeckt ist. Der Pixel ist somit verdeckt und liegt im Schatten. Bild d) zeigt das fertig gerenderte Objekt mit Schatten. nerieren. Dazu wird eine Matrix benötigt die die entsprechenden Parameter zur Texturkoordinatengenerierung enthält. Diese Matrix setzt sich wie folgt zusammen: T = Pl · Vl · Vv−1 (3.6) Die Texturmatrix T ist das Produkt aus der Projektionsmatrix der Lichtquelle Pl , der Viewmatrix der Lichtquelle Vl und der inversen Viewmatrix des Betrachters Vv−1 . Man kann die Transformation anhand von Abbildung 3.7 nachvollziehen. Es gibt aber noch einen einfacheren Weg um die Transformation durchzuführen. Da die Transformationen innerhalb der Grafikpipeline in der Geometrieeinheit durchgeführt werden, bieten sich Vertexshader an, um die Transformation explizit durchzuführen. Der Vertexshader erhält die Vertexkoordi- KAPITEL 3. TECHNIKEN 42 Modelview Matrix des Betrachters Modelview Matrix der Lichtquelle Objektkoordinaten Model Matrix Weltkoordinaten View Matrix des Betrachters Koordinatenraum des Betrachters Projection Matrix des Betrachters Sichtfeld des Betrachters View Matrix der Lichtquelle Koordinatenraum der Lichtquelle Projection Matrix der Lichtquelle Sichtfeld der Lichtquelle Abbildung 3.7: Schematische Darstellung der benötigten Transformationen beim Shadowmapping mittels OpenGL. Der grüne Pfeil symbolisiert die Transformation, die durch die Texturmatrix durchgeführt wird. naten in Form von Objektkoordinaten. Diese müssen im Vertexshader mit der kombinierten Modelview-Projektionsmatrix des Betrachters multipliziert werden um Koordinaten im Projektionsraum des Betrachters zu erhalten. Wie aus Abbildung 3.7 ersichtlich, kann der Vertexshader die Objektkoordinaten aber zusätzlich auch mit der kombinierten Modelview-Projektionsmatrix der Lichtquelle multiplizieren. Auf diese Weise erhält man direkt die korrespondierenden Koordinaten im Projektionsraum der Lichtquelle. Diese kann der Vertexshader als Texturkoordinaten in entsprechende Register schreiben und so einem Fragmentshader zugänglich machen, der dann das entsprechende Fragment mit dem korrespondieren Eintrag aus der Shadow Map vergleicht. Die Shadow Map wird dem Fragmentshader in Form einer Textur übergeben. Beim Shadowmapping handelt es sich um ein bildbasiertes Verfahren, das heisst es werden keine Geometriedaten der Szene für den Schattentest benötigt, sondern lediglich die Tiefeninformationen der Szene aus Sicht der Lichtquelle. Dadurch ergeben sich einige entscheidende Vorteile gegenüber anderen Verfahren: • Es können beliebig große Szenen verarbeitet werden. • Der Algorithmus benötigt keinerlei Geometrieinformationen der Szene. KAPITEL 3. TECHNIKEN 43 Er kann praktisch alle Szenen verarbeiten die sich rendern lassen. • Der Algorithmus hat nur ein lineares Kostenwachstum bzgl. der Komplexität der Szene [Wil78]. • Die Entscheidung ob ein Pixel beleuchtet ist oder nicht benötigt nur einen Lookup in der Shadow Map, ist somit konstant. Aufgrund der oben genannten Vorteile bietet sich der Algorithmus für die texturbasierte Volumenvisualisierung an. Er benötigt keine Geometrieinformationen, was eine Voraussetzung ist, da diese für das Volumen nicht vorhanden sind, und er ist vergleichsweise schnell. Allerdings wird für die Erstellung der Shadow Map ein zusätzlicher Renderzyklus benötigt. Die Tatsache, dass es sich um ein bildbasiertes Verfahren handelt, bringt aber auch zwei Nachteile mit: • Die Qualität der Schatten hängt direkt, sowohl von der Auflösung als auch der Präzision des Z-Buffers ab (Abbildung 3.8a). • Die Shadow Map wird beim Vergleich gesampelt. Dadurch ergeben sich Alias-Effekte, besonders in Nähe der Schattenränder. Auch fangen Objekte aufgrund dieser Ungenauigkeit an, sich selbst zu schattieren (Abbildung 3.8b). a) b) Abbildung 3.8: Bild a) verwendet eine sehr niedrig aufgelöste Shadow Map. Der Alias-Effekt ist deutlich zu erkennen. Bild b) zeigt die fehlerhafte Selbstschattierung des Objekts. KAPITEL 3. TECHNIKEN 44 Um obige Probleme zu reduzieren (ganz vermeiden lassen sie sich nicht), wird ein Bias-Faktor, also ein kleiner Fehler mit eingeführt. Anstatt zu überprüfen ob zwei Z-Werte gleich sind, was bei float-Werten generell schlecht ist, wird überprüft ob die Differenz beider Werte kleiner als eine bestimmte Schranke (der Bias-Faktor) ist. Es ist nur schwer möglich eine allgemeine Aussage zu treffen, wie groß dieser Faktor genau sein soll. Allgemein gilt: Je höher die Genauigkeit des Z-Buffers, desto niedriger kann der Bias ausfallen. Die Genauigkeit des Z-Buffers spielt eine wichtige Rolle. Die Werte im ZBuffer werden immer zwischen 0 und 1 skaliert. In OpenGL muss ein sogenanntes viewing volume definiert werden. Alles was sich innerhalb diese Volumens befindet kann der Betrachter sehen und wird gerendert. Zwei Begrenzungsebenen des viewing volume spielen dabei eine besondere Rolle, die near clipping plane und die far clipping plane. Sie begrenzen das viewing volume aus Sicht des Betrachters nach vorne und hinten. Der Abstand dieser Ebenen zueinander entspricht dem Wertebereich den der Z-Buffer abdecken muss. Man kann die effektive Genauigkeit des Z-Buffers erhöhen, indem den Abstand zwischen near- und far clipping plane möglichst klein macht. Beim Erzeugen der Shadow Map sollte die near plane immer möglichst weit von der Lichtquelle entfernt und die far plane möglichst nahe zur Lichtquelle plaziert werden. 3.4 Isoflächen Rendering Eine Isofläche ist definiert als eine Menge von Punkten innerhalb eines Volumens, welche einen gemeinsamen Wert haben. Volumendaten, wie sie ein Computertomograph liefert, repräsentieren Dichtewerte. Sämtliche Punkte die denselben Dichtewert besitzen entsprechen somit einer Isofläche. Isoflächen werden häufig durch Polygone angenähert. In einem Vorverarbeitungsschritt wird dabei die Isofläche aus dem Volumendatensatz extrahiert. Bei dieser Vorgehensweise spricht man von indirektem Isoflächenrendering, weil die Isofläche nicht direkt visualisiert wird, sondern zunächst in eine andere Form, ein Polygon, überführt wird. Ein bekanntes indirektes Verfahren um Isoflächen zu erzeugen ist das Marching-Cubes-Verfahren[LC87]. Dabei wird das zuvor binarisierte Volumen mit einem kubusförmigen Element abgetastet. Schneidet eine Isofläche den Kubus, wird diese durch ein Polygon angenähert. Nachdem das gesamte Volumen abgetastet wurde erhält man so die, durch Polygone angenäherte, Isofläche. Isoflächen können aber auch direkt, also ohne vorherige Bestimmung von Geometrieinformationen, visualisiert werden. Um eine Isofläche unter OpenGL zu rendern, kann der Alpha-Test benutzt werden. Bevor ein Pixel endgültig in KAPITEL 3. TECHNIKEN 45 den Framebuffer geschrieben wird, kann der Alphawert des Pixels mit Hilfe einer Vergleichsfunktion auf einen Referenzwert überprüft werden. Verläuft der Test erfolgreich wird der Pixel gerendert, andernfalls wird er verworfen. Die Vergleichsfunktion und der Referenzwert lassen sich mit Hilfe der OpenGL Funktion glAlphaFunc() setzen. Folgende Vergleichsfunktionen stehen dabei zur Verfügung: OpenGL Konstante GL NEVER GL LESS GL EQUAL GL LEQUAL GL GREATER GL NOTEQUAL GL GEQUAL GL ALWAYS Bedeutung Jeder Pixel wird verworfen. < = ≤ > 6= ≥ Jeder Pixel wird gezeichnet. Um eine bestimmte Isofläche zu rendern kann die Vergleichsfunktion GL EQUAL benutzt werden, so das nur Pixel in den Framebuffer geschrieben werden, deren Alphawert gleich dem Referenzwert ist. Häufig sind die Ergebnisse mittels GL EQUAL unbefriedigend, da real gemessene Volumendaten häufig nicht völlig homogen sind und somit Dichteschwankungen enthalten. Die so gerenderte Isofläche enthält häufig Artefakte oder ist nicht zusammenhängend. Eine bessere Möglichkeit zur direkten Darstelleung von Isoflächen besteht darin, ein Intervall von Isowerten anzugeben, welche gerendert werden sollen. Diese Funktionalität wird nicht direkt von OpenGL unterstützt, kann aber mit mehreren Renderzyklen, unter Benutzung des Stencil-Buffers, erreicht werden [Bau00]. Der Einsatz eines Fragmentshaders erlaubt ebenfalls eine effiziente Möglichkeit Isoflächen zu rendern. Dabei ist es sogar möglich, beliebig viele Isoflächen in nur einem einzigen Renderzyklus zu visualisieren. Der Fragmentshader erhält dabei ein Array von Intervallen, welche die Isowerte enthalten, die gerendert werden sollen. Ein Intervall wird dabei durch einen float2-Datentyp repräsentiert, also einem zweidimensionalen Vektor. Die x-Komponente des Vektors enthält die untere Grenze und die y-Komponente die obere Grenze des Intervalls. Der Rückgabewert des Shaders, also das RGBA-Tupel des Fragments, wird mit einem Alphawert 0.0 initialisiert. Mit einer Schleife kann dann das Array mit den Intervallen iteriert werden. Bei KAPITEL 3. TECHNIKEN 46 jedem Iterationsschritt wird geprüft, ob der Grauwert des aktuellen Fragments innerhalb des Intervalls liegt. Ist das der Fall, wird dem Fragment das RGBA-Tupel der Transferfunktion zugewiesen. Sollte das aktuelle Fragment nicht in einem der Intervalle liegen, so behält es seinen initialen Wert 0.0. Es wird also völlig transparent gerendert. Der Fragmentshader sorgt also dafür, dass sämtliche Fragmente die nicht gerendert werden sollen, den Alphawert 0.0 erhalten. Fragmente die gerendert werden sollen, erhalten automatisch den entsprechenden Farbwert aus der Transferfunktion. Mit Hilfe des shaderbasierten Isoflächenrenderers lassen sich halbtransparente Darstellungen leicht realisieren. Zur Veranschaulichung ist in Abbildung 3.9 ein Motorblock halbtransparent dargestellt. Die inneren Strukturen (rot eingefärbt) bestehen aus einem anderen Material als der umgebende Motorblock und besitzen eine andere Dichte. Sie wurden mit einer hohen Opazität gerendert. Dem umgebenden Motorblock wurde nur eine geringe Opazizät zugewiesen, so das er halbtransparent wirkt. Abbildung 3.9: Semitransparente Darstellung eines Motorblocks KAPITEL 3. TECHNIKEN 3.5 47 Progressives Rendering Die benötigte Rechenzeit für die Visualisierung eines Datensatzes hängt direkt mit dessen Größe zusammen. Je größer der Datensatz desto mehr Rechenzeit wird benötigt. Effekte wie Beleuchtung wirken sich dabei zusätzlich negativ auf die Performance aus. Sinkt die Framerate dabei auf nur sehr wenige Frames pro Sekunde, leidet die Interaktivität spürbar. Das System reagiert nur noch sehr träge auf Benutzereingaben. Progressives Rendering hilft, die Interaktivität auf Kosten der Bildqualität zu erhöhen. Dazu wird die darzustellende Datenmenge bei der Interaktion reduziert, was die Performance erhöht. Wie die Datenmenge genau reduziert wird hängt stark von der Visualisierungsmethode ab. Beim Raycasting besteht die Möglichkeit, die Anzahl der ausgesandten Strahlen zu reduzieren, indem man ein groberes Bildraster wählt. Der Rendervorgang wird dadurch beschleunigt, das Bild ist durch das grobere Bildraster entsprechend grober aufgelöst. Ist die Interaktion beendet, wird das Bild nach und nach verfeinert (progressive refinement). Die Performance der Visualisierung beim texturbasierten Volumenrendering hängt insbesondere von zwei Faktoren ab: • Anzahl der Texturscheiben Je höher die Anzahl der verwendeten Texturscheiben bei der Visualisierung ist, desto besser ist auch die resultierende Bildqualität. Dabei gilt: Je grösser der zugrunde liegende Datensatz ist, desto mehr Texturscheiben sollten verwendet werden. Um die Performance zu erhöhen, wäre es möglich, die Anzahl der Texturscheiben beim Rendern während einer Interaktion zu erniedrigen. • Anzahl der zu rendernden Fragmente Insbesondere unter Verwendung von Fragmentshadern beim Rendern, kann ein grosser Performancegewinn erzielt werden, indem eine Möglichkeit gefunden wird, weniger Fragmente zu rendern. Ziel ist es also, die Anzahl der Aufrufe eines verwendeten Fragmentshaders zu reduzieren, da dieser für jedes Fragment einmal ausgeführt werden. Die Grösse des Datensatzes ist dabei nicht notwendigerweise ein Kriterium für die Anzahl der tatsächlich gerenderten Pixel, da ein Objekt gezoomt werden kann. Ein kleinerer Datensatz kann dabei durchaus, bei einem entsprechend hohen Zoomfaktor, mehr Pixel erzeugen als ein grösserer Datensatz mit entsprechend kleinerem Zoomfaktor. KAPITEL 3. TECHNIKEN 48 Eine Reduzierung der Anzahl der Texturscheiben während der Interaktion ist relativ einfach zu implementieren. Dazu wird einfach der Abstand der Scheiben beim Rendern entsprechend vergrößert. Die Anzahl der zu rendernden Pixel wird dadurch ebenfalls reduziert, da die ausgelassenen Texturscheiben nicht gerendert werden. Allerdings wirken die Objekte bei einem zu grossen Abstand zwischen den Texturscheiben nicht mehr plastisch und es enstehen grobe Artefakte, insbesondere an den Rändern des Objekts. Eine optisch ansprechendere Methode, sowohl die Anzahl der Texturscheiben als auch die der Pixel senken, bestehet darin, durch Subsampling einen weiteren Datensatz mit einer entsprechend niedrigeren Auflösung zu generieren. Während einer Interaktion wird dieser kleine Datensatz gerendert. Das gerenderte Bild wird dann, in Form einer Textur, der Grafikpipeline erneut zugänglich gemacht und, entsprechend hochskaliert, in den Framebuffer gerendert. Der eigentliche, rechenintensive, Rendervorgang wird anhand des kleinen Datensatzes durchgeführt. Der kleine Datensatz benötigt einerseits weniger Texturscheiben zur Darstellung und reduziert andererseits die Anzahl der zu berechnenden Pixel, bei gleichem Zoomfaktor, beträchtlich1 . Trotz des zusätzlich benötigten Renderzyklus ist diese Variante bedeutend schneller als das direkte Rendern des hochaufgelösten Datensatzes, da der zweite Renderzyklus nur die zuvor erzeugte Textur in den Framebuffer rendern muss, was vergleichsweise sehr schnell geschieht. In Abbildung 3.10 ist dieser Vorgang schematisch dargestellt. Durch das 16 12 Offscreen Buffer 32 24 32 24 Framebuffer Framebuffer Abbildung 3.10: Das linke Bild zeigt den Inhalt des Framebuffers nach dem Rendern des hochaufgelösten Datensatzes. Im rechten Bild wird der Vorgang beim Rendern eines niedriger aufgelösten Datensatzes gezeigt. Der kleinere Datensatz wird zunächst in einen (kleineren) Offscreen-Buffer gerendert. Dieser wird in einem weiteren Renderzyklus als Textur verwendet und, entsprechend hochskaliert, in den eigentlichen Framebuffer gerendert. 1 Ein Datensatz der Größe 1283 besteht aus 2097152 Werten, ein verkleinerter Datensatz der Größe 643 besitzt nur noch 81 der Werte, also 262144 KAPITEL 3. TECHNIKEN 49 Hochskalieren der Textur wirkt das Volumen natürlich grobpixelig. Die Erhöhung der Interaktivität geht immer zu Lasten der Qualität, allerdings bleibt bei diesem Verfahren die Plastizität des Objekts erhalten. Üblicherweise werden gleich mehrere solcher kleinen Datensätze, abgestuft nach Grösse, erzeugt. Beim Rendern des Volumens werden die Datensätze sukzessiv, beginnend mit dem kleinsten, gerendert. Nach jedem Renderdzyklus erhöht sich dabei die Bildqualität. Bei einer Interaktion des Anwenders wird die Renderfolge abgebrochen und das Volumen, mit den neuen Parametern, wieder sukzessiv beginnend mit dem kleinsten Datensatz gerendert. Man spricht dann von progressivem Rendering, da die Bildqualität kontinuierlich, mit jedem Renderzyklus, verbessert wird. Abbildung 3.11 zeigt die Schrittweise Verbesserung in drei Stufen: Abbildung 3.11: Drei Stufen des progressiven Renderings. Links: niedrige Auflösung. Mitte: mittlere Auflösung. Rechts: Höchste Auflösung. Das nächste Kapitel behandelt die Implementierung der hier vorgestellten Techniken, sowie die Integration in das Modul Voxel-Sculpture welches bereits eine texturbasierte Volumenvisualisierung bereitstellt. Kapitel 4 Implementierung Dieses Kapitel erläutert die Implementierungsdetails der einzelnen Aufgaben. Sämtliche entwickelten Klassen sind Bestandteil des Moduls Voxel-Sculpture. Zunächst wird eine kurze Übersicht über Voxel-Sculpture gegeben. In den weiteren Kapiteln werden die einzelnen Aufgaben näher erläutert und das Modul schrittweise erweitert. 4.1 MAVI und das Voxel-Sculpture Modul MAVI [MAV] ist ein 3d-Bildverarbeitungsframework. Es lädt über einen Plugin-Mechanismus verschiedene Module, die Bildverarbeitungsalgorithmen oder Visualisierungsverfahren enthalten. Diese können dann einfach in MAVI aufgerufen werden. Für die 3d-Visualisierung existiert das Modul VoxelSculpture (VS). Es ermöglicht die Visualisierung dreidimensionaler Datensätze mit Hilfe eines texturbasierten Renderingverfahrens. VS wurde komplett in C++ realisiert. Für die Anzeige wurde OpenGL verwendet und die Benutzerschnittstelle wurde mit Qt [Tro] realisiert. MAVI, und damit auch VS, ist sowohl unter Linux als auch Windows lauffähig. Aufgrund einiger verwendeter herstellerspezifischer OpenGL-Erweiterungen funktionierte VS nur mit Grafikkarten des Herstellers NVidia, durch die Erweiterung des Moduls um shaderbasierte Renderklassen läuft das Modul nun auch auf shaderfähigen Grafikkarten anderer Hersteller. 50 KAPITEL 4. IMPLEMENTIERUNG 4.2 51 Die Klassenstruktur Da VS bei dieser Arbeit als Basis verwendet wurde, wird die Struktur kurz erläutert. Der genaue Aufbau und die Funktionsweise von VS ist in [Hir05] beschrieben. Einen Überblick über das Modul gewährt Abbildung 4.1. Die 1 VS::VoxelSculpture QGLWidget 1 1 ITWM::CImage 1 VS::CRenderWidget 1 1 VS::CRenderEngine VS::C2d3tsRenderEngine VS::CTexture VS::C3dtRenderEngine 1 1 n VS::CTexture2d VS::CTexture3d 1 ITWM::CBox 1 VS::CMultiTexture2d n VS::CBrick 1 n VS::StIntersectionPoint Abbildung 4.1: Statisches UML-Klassendiagramm der Kernklassen des Moduls VoxelSculpture oberste Klasse ist CVoxelSculpture. Sie hält eine Referenz auf ein CImageObjekt, welches das darzustellende Bild enthält. Die Klasse CImage ist Bestandteil einer externen Bibliothek und im Namensraum ITWM definiert. Für die Darstellung erzeugt CVoxelSculpture ein Objekt der Klasse CRenderWidget, welches von der Klasse QGLWidget abgeleitet ist. QGLWidget ist Bestandteil der GUI-Bibliothek Qt und Zuständig für die Anzeige der mit OpenGL erzeugten Visualisierung. Das CRenderWidget erzeugt, abhängig von der zur Verfügung stehenden Hardware, eine Renderklasse. Die Renderklasse ist dabei von der gemeinsamen Basisklasse CRenderEngine abgeleitet. KAPITEL 4. IMPLEMENTIERUNG 52 Ursprünglich existierten zwei verschiedene Renderklassen: C2d3tsRenderEngine und C3dtRenderEngine, jeweils verantwortlich für die Visualisierung mittels zweidimensionaler- beziehungsweise dreidimensionaler Texturen. Dazu aggregieren sie Texturklassen, welche die einzelnen Texturscheiben (Slices) kapseln. Dies ist der grundsätzliche Aufbau von VS. Es existieren noch eine Reihe weiterer Hilfsklassen wie etwa verschiedene Dialoge, um das Rendering zu konfigurieren. 4.2.1 Die Shaderklassen Sämtliche im Rahmen dieser Arbeit entwickelten Shader, sind in der Shadersprache Cg geschrieben. Die Endung solcher Quellcodedateien ist üblicherweise .cg. Um die Shader zu verwenden, müssen folgende Schritte durchgeführt werden: 1. Erzeugen eines Cg Contexts. Ein Cg Context ist eine Art Container der mehrere Cg Shader (sowohl Vertex- als auch Fragmentshader) enthalten kann. 2. Erzeugen eines Cg Programs. Das Cg Programm abstrahiert einen bestimmten Shader. Liegt der Shader als Quellcode vor wird er bei diesem Schritt automatisch kompiliert. Das Cg Programm wird dabei einem Cg Context zugeordnet. 3. Laden des Programms. Der Objektcode des Shaders wird so der jeweiligen 3d-API1 zugänglich gemacht. 4. Übergabe von Parametern an den Shader (soweit benötigt). 5. Aktivieren des entsprechenden Profils für das der Shader kompiliert wurde und binden des Programs. Danach wird der Shader automatisch für jedes Vertex (Vertexshader) oder Fragment (Fragmentshader) ausgeführt. Die Cg-Laufzeitbibliothek enthält die nötigen Funktionen um die oben beschriebenen Schritte durchzuführen. Um die Arbeit mit Shadern zu vereinfachen, wurde das Modul VS um einige Hilfsklassen erweitert. Es wurde eine Klasse CCgShaderManager eingeführt, die eine Reihe von CCgShader-Objekten verwaltet. Bei der Klasse CCgShader handelt es sich um eine allgemeine Basisklasse. Es existieren zurzeit zwei weitere Spezialisierungen: CCgVertexShader und CCgFragmentShader. 1 Application Programming Interface KAPITEL 4. IMPLEMENTIERUNG 53 Der Aufbau der Klassen ist in Abbildung 4.2 dargestellt. Die oberste Klasse VS::CCgShaderManager 1 n VS::StShader 1 1 VS::CCgShader VS::CCgVertexShader VS::CCgFragmentShader Abbildung 4.2: Statisches UML-Klassendiagramm der beteiligten Shaderklassen CCgShaderManager enthält ein Array von StShader-Objekten. Bei StShader handelt es sich lediglich um eine Struktur die ein CCgShader-Objekt beschreibt. Die Struktur beinhaltet, neben der Referenz auf das CCgShaderObjekt den Typ des Shaders (Vertex- oder Fragmentshader) sowie einen, vom Aufrufer definierten, Namen in Form eines Strings, anhand dessen ein Shader identifiziert werden kann. Der Shadermanager besitzt vier Methoden um Shader hinzuzufügen: AddVertexShader(), AddVertexShaderFromFile(), AddFragmentShader() und AddFragmentShaderFromFile(). Die Shader können entweder in Form einer Datei hinzugefügt werden oder in Form eines Strings, der den Shadercode enthält. Ausserdem kann der Shader bereits vorkompiliert als Objektcode vorliegen oder als Quellcode. Liegt der Shader als Quellcode vor, wird er zur Laufzeit kompiliert. Der Shadermanager besitzt ausserdem je einen Zeiger auf ein CCgVertexShaderObjekt und auf ein CCgFragmentShader-Objekt. Diese beiden Shaderobjekte sind jeweils der aktuell ausgewählte Vertex- beziehungsweise Fragmentshader. Diese Zeiger können abgefragt werden um Zugriff auf die aktuellen Shader zu erhalten. Diese können dann weiter konfiguriert und aktiviert werden. KAPITEL 4. IMPLEMENTIERUNG 54 Es ist immer nur möglich einen Vertexshader und/oder einen Fragmentshader gleichzeitig zu benutzen. Der Shadermanager übernimmt also Schritt 1, er erzeugt intern einen Cg Context, welcher die weiteren Shader aufnimmt. Beim Hinzufügen eines Shaders zum Manager wird automatisch dessen Init()Methode aufgerufen. Init() ist eine pur-virtuelle Methode der Basisklasse CCgShader. Die entsprechende Implementierung in den abgeleiteten Klassen (CCgVertexShader, CCgFragmentShader) sorgt dafür das der Shader kompiliert und geladen wird. Die benötigten Parameter bekommt das Shaderobjekt vom Shadermanager. In diesem Schritt wird das Cg Program erzeugt. Anschliessend wird das Programm geladen, also der 3d-API übergeben. Die Init()-Methode der Shader wurde bewusst mit dem Attribut protected versehen, um zu verhindern das diese Methode direkt aufgerufen wird. Die Shaderobjekte sollen nur in Zusammenhang mit dem Shadermanager verwendet werden. Dieser ist als friend-Klasse der Shaderklassen deklariert, hat somit Zugriff auf deren privaten Methoden. Beim Initialisieren des Shaderobjekts werden Schritt 2 und 3 durchgeführt. Um die Parameter eines Shaders zu setzen und ihn zu aktivieren, wird eine Referenz auf das Shaderobjekt benötigt. Mit der Methode SelectShader() des Shadermanagers wird der aktuelle Vertex- beziehungsweise Fragmentshader gesetzt. GetShader() liefert dann einen Zeiger auf den aktuellen Vertexoder Fragmentshader. Über diesen Zeiger kann direkt auf das Shaderobjekt zugegriffen werden. Die Basisklasse CCgShader besitzt entsprechende Methoden um die verschiedenen Parametertypen zu setzen. Folgende Datentypen werden sowohl vom Vertex- als auch vom Fragmentshader unterstützt: • float, double - Ein einfacher float- beziehungsweise double-Parameter. • float2, double2 - Ein zweidimensionaler float- beziehungsweise doubleVektor. • float3, double3 - Ein dreidimensionaler float- beziehungsweise doubleVektor. • float4, double4 - Ein vierdimensionaler float- beziehungsweise doubleVektor. • floatAxB, doubleAxB - Eine float- beziehungsweise double-Matrix. A und B dürfen maximal 4 sein. Ein Sonderfall bilden die sogenannten KAPITEL 4. IMPLEMENTIERUNG 55 OpenGL State-Matrizen, also etwa die Modelview-Matrix. Dabei handelt es sich immer um float4x4 Matrizen, welche direkt gesetzt werden können. Mit Ausnahme der State-Matrizen dürfen alle oben aufgeführten Parametertypen auch als Array verwendet werden. Die Klasse CCgVertexShader erlaubt es noch sogenannte varying Parameter zu setzen. Dabei handelt es sich um ein Array von Werten, wobei jeder Wert an ein bestimmtes Vertex gebunden ist. Der Vertexshader hat dann immer nur Zugriff auf den entsprechenden Wert der varying Parameter für das aktuelle Vertex. Die CCgFragmentShader-Klasse besitzt die Möglichkeit Texturparameter zu setzen, um im Shader Zugriff auf entsprechende Texturen zu haben. Eine Liste der Namen der verwendeten Parameter kann mit der Methode GetParameterList() abgefragt werden. Um den Shader zu aktivieren, wird die Methode Enable() der Shaderklasse aufgerufen. Diese Methode aktiviert das entsprechende Profil und bindet den Shader. Der Shader wird für alle folgenden OpenGL Zeichenfunktionen aufgerufen. Um ihn zu deaktivieren steht entsprechend die Methode Disable() zur Verfügung. 4.2.2 Die Renderklassen Ursprünglich enthielt VS zwei Implementierungen vom Interface CRenderEngine. Die Klasse C2d3tsRenderEngine für die Visualisierung mittels 2D-Texturen und die Klasse C3dtRenderEngine für die entsprechende Visualisierung mittels 3D-Texturen. Aufgrund des gemeinsamen Interfaces bleibt die Schnittstelle beider Renderklassen für andere Klassen transparent. Weitere Renderklassen, die im Laufe dieser Arbeit entstanden sind, implementieren ebenfalls das Interface der Basisklasse. Insgesamt sind drei weitere Renderklassen hinzugekommen: Die Klasse C3dtCgRenderEngine, C3dtCgLightingRenderEngine und C3dtCgIsoValueRenderEngine. Wie das Prefix Cg im Klassennamen bereits andeutet basieren alle drei Klassen auf Cg Shadern. Die erweiterte Klassenstruktur von VS zeigt Abbildung 4.3. Die Klasse C3dtCgLightingRenderEngine implementiert die shaderbasierte Beleuchtung und Schattengenerierung und die Klasse C3dtCgIsoValueRenderEngine das Isoflächenrendering. Die Klasse C3dtCgRenderEngine implementiert die Volumenvisualisierung mittels 3D-Textur. Die Funktionalität ist dabei dieselbe wie bei der bereits vorhandenen Klasse C3dtRenderEngine. Der einzige Unterschied besteht in der Anwendung der Transferfunktion. In der Klasse C3dtRenderEngine ge- 1 1 VS::VoxelSculpture 1 VS::CTexture2d n 1 n VS::CBrick ITWM::CBox VS::StIntersectionPoint 1 1 VS::CCgVertexShader VS::CCgFragmentShader VS::CCgShader 1 1 VS::StShader n 1 VS::CCgShaderManager VS::C3dtCgLightingRenderEngine VS::C3dtCgRenderEngine VS::C3dtCgIsoValueRenderEngine VS::C3dtRenderEngine VS::CTexture3d 1 VS::CTexture VS::CRenderEngine 1 1 1 VS::CRenderWidget QGLWidget VS::CMultiTexture2d n VS::C2d3tsRenderEngine ITWM::CImage 1 KAPITEL 4. IMPLEMENTIERUNG 56 Abbildung 4.3: Statisches UML-Klassendiagramm der neuen Renderklassen. Die alten Klassen grau dargestellt. KAPITEL 4. IMPLEMENTIERUNG 57 schah dies über die OpenGL Erweiterung, GL EXT paletted texture. Dabei wurden die Grauwerte der 3D-Textur des Volumens als Indizes für eine Farbtabelle, der Transferfunktion, interpretiert und entsprechend ersetzt. Aktuelle Grafikkarten unterstützen diese OpenGL Erweiterung nicht mehr. Deshalb benutzt die Renderklasse C3dtCgRenderEngine einen Fragmentshader um die Transferfunktion anzuwenden. Die Transferfunktion wird dem Shader in Form einer eindimensionalen Textur zugänglich gemacht. Der Shader besorgt sich aus der 3D-Textur zunächst den Grauwert des Volumens für das aktuelle Fragment und benutzt diesen als Index für einen weiteren Texturzugriff auf die Transferfunktion. Der resultierende RGBA-Wert wird dem Fragment zugewiesen. Auf die Implementierung dieser Klasse wird hier nicht genauer eingegangen, da der einzige Unterschied zur C3dtRenderEngine in der Benutzung eines Shaders besteht. Diese werden ausreichend anhand der Beleuchtungsund Isoflächenrenderklasse erläutert. Ein wichtiger Unterschied zwischen beiden Renderklassen besteht dennoch: Die palettenbasierte Renderklasse wendet die Transferfunktion vor der Filterung der Textur an (pre-classification), die shaderbasierte Renderklasse hingegen erst danach (post-classification). Beide Verfahren führen zu unterschiedlichen Ergebnissen. Im allgemeinen liefert die post-classification bessere, kontrastreichere Ergebnisse. Die post-classification ist auch das kor” rektere“ Verfahren, weil zunächst das Voxel des Volumens mittels trilinearer Filterung rekonstruiert wird und dann erst die Transferfunktion auf das rekonstruierte Voxel angewendet wird. Bei der pre-classification dagegen wird nicht der ursprüngliche Voxel, also das ursprüngliche Signal, rekonstruiert, sondern ein Farbwert der benachbarten Voxel, auf die bereits die Transferfunktion angewendet wurde. Abbildung 4.4 verdeutlicht wie die unterschiedlichen Resultate zustande kommen. Die beiden Funktionen stellen die Transferfunktion dar. Der zu rekonstruierende Voxel liegt genau mittig zwischen den beiden diskreten Abtastwerten v0 und v1 . Bei der pre-classification werden die beiden Abtastwerte zunächst durch den korrespondierenden Wert der Transferfunktion, t0 , beziehungsweise t1 ersetzt. Anschliessend wird zwischen diesen Werten linear interpoliert. Bei der post-classification werden die Werte v0 und v1 direkt linear interpoliert. Daraus resultiert der Wert vi . Auf diesen rekonstruierten Wert des Signals wird die Transferfunktion angewendet. Das Voxel erhält somit den Wert ti . Die Transferfunktionen sind zwischen beiden Verfahren somit nicht austauschbar. Die pre-classification produziert durch die Interpolation der Farbwerte immer weiche Übergänge. Auch lassen sich sehr hohe Frequenzen mit der pre-classification nicht darstellen. KAPITEL 4. IMPLEMENTIERUNG 58 Post-Classification Pre-Classification Emission/Absorption interpolation Emission/Absorption ti t1 t0 interpolation v0 v1 Grauwert v0 vi v1 Grauwert Abbildung 4.4: Im linken Bild wird die Transferfunktion zunächst auf die Voxelwerte angewendet und dann interpoliert (pre-classification). Im rechten Bild wird zunächst der Voxelwert interpoliert und dieser dann als Index für die Transferfunktion benutzt (postclassification) Prinzipbedingt zeigen sich bei der post-classification allerdings Artefkate an Objektübergängen bei Binär- und Labelbildern, insbesondere wenn eine hohe Opazität eingestellt ist (Abbildung 4.5). Um die Effekte aus Abbildung 4.5 zu erklären, stelle man sich zunächst ein gespreiztes Binärbild vor (obere Reihe), das heisst Vordergrundpixel haben einen Grauwert von 255 und Hintergrundpixel entsprechend 0. Ein mittels linearer Interpolation rekonstruierter Wert an einer Objektgrenze besitzt demnach einen entsprechend gewichteten Grauwert, etwa 100. Aufgrund der eingestellten Transferfunktion bekommen Pixel mit einem Grauwert von 255 einen RGBA-Wert von (255, 255, 255, 255), also weiß mit maximaler Opazität. Sämtliche Objekte (oder Vordergrundpixel) werden also in weiß gerendert, erkennbar an den weißen Flächen an den Seiten des Volumens. Ein Pixel an der Objektgrenze, mit dem beispielhaften Grauwert 100, erhält aufgrund der linearen Transferfunktion einen RGB-Wert von (100, 100, 100) mit einer ebenfalls sehr hohen Opazität. Der Randpixel ist also deutlich dunkler als das Objekt selbst. Dadurch entstehen dunkle Artefakte an den Objektgrenzen. Schlimmer äussert sich das Problem bei Labelbildern, bei denen den einzelnen Labeln eine eigene Farbe zugewiesen wird. Labelbilder sind ähnlich den Binärbildern, nur das durch die Segmentierung mehr als zwei Repräsentativwerte verwendet werden. Objekte können beispielsweise in Größenklassen unterteilt werden, wobei sämtliche Objekte einer Größenklasse denselben Grauwert (Label) erhalten. Die Transferfunktion in Abbildung 4.5, untere Reihe, färbt Objekte mit verschiedenen Labeln unterschiedlich ein. Betrachtet man KAPITEL 4. IMPLEMENTIERUNG 59 Abbildung 4.5: Die linke Spalte zeigt ein Binär- und ein Labelbild mittels pre-classification visualisiert. Die mittlere Spalte zeigt dieselben Bilder mittels post-classification. Die Artefakte an den Objekträndern sind deutlich zu erkennen. Die rechte Spalte zeigt die verwendete Transferfunktion für die jeweiligen Bilder. Die gelbe Kurve bezeichnet die Opazität, Die rote, grüne und blaue Kurve jeweils die entsprechende Farbe. zunächst ein grün eingefärbtes Objekt (im Histogramm durch einen türkisfarbenen Balken markiert) so hat dieses ungefähr einen Grauwert (Label) von 70. An der Objektgrenze herrschen wieder hohe Frequenzen (Grauwert 0). Bei der linearen Filterung könnte dann ein Grauwert mit dem Betrag 35 entstehen (im Histogramm durch einen magentafarbenen Balken markiert). Dem Grauwert 35 wird aber über die Transferfunktion ein sattes Rot zugewiesen, da dieses Label einem anderen Objekt entspricht. Dadurch erscheinen rote Artefakte an den Objektgrenzen eigentlich grüner Objekte. Andersfarbige Objekte vermischen die Farben entsprechend anders. Bei der pre-classification entstehen diese Artefakte nicht, da die Transferfunktion auf die ungefilterten Pixel angewendet wird. Der Farbe des gefilterten Pixel ist also eine lineare Gewichtung der Farbwerte der benachbarten Pixel. Dadurch können keine Sprünge“ entstehen wie bei der post-classification. ” Dieses Problem der post-classification ist nur sehr schwierig zu lösen. Eine Möglichkeit wäre, pro zu rendernder Texturscheibe für jedes Label einen eigenen Renderzyklus zu benutzen, bei dem die Transferfunktion sämtlichen Grauwerten ausser dem gerade zu renderndem Label volle Tranzparenz zuweist [Chr01]. Ein solches Verfahren ließe sich aber kaum mit interaktiven Frameraten realisieren. KAPITEL 4. IMPLEMENTIERUNG 60 Die Beleuchtungsklasse Für die Beleuchtung wurde eine neue Renderklasse C3dtCgLightingRenderEngine von der Klasse C3dtCgRenderEngine abgeleitet. Die C3dtCgLightingRenderEngine erzeugt zunächst eine Shadowmap aus Sicht der Lichtquelle (siehe Kapitel 3.3.3). Dazu wird die Szene normal gerendert. Der Framebuffer wird dabei allerdings aus Performancegründen mittels glColorMask( GL FALSE, GL FALSE, GL FALSE, GL FALSE ) deaktiviert, da für die Shadowmap nur der Inhalt des Z-Buffers von Interesse ist. Anschliessend wird der Inhalt des Z-Buffers ausgelesen und in Form einer 2D-Textur gespeichert. Im folgenden Rendervorgang, diesmal mit aktiviertem Framebuffer, wird die Shadowmap einem Fragmentshader zugänglich gemacht, damit dieser entscheiden kann ob ein Pixel beleuchtet ist. Ist das der Fall, wird die Normale des Pixels anhand der zentralen Differenzen (siehe Kapitel 3.2.2) bestimmt und anschliessend das Beleuchtungsmodell angewendet. Liegt der Pixel im Schatten, wird stattdessen einfach der Farbwert der Transferfunktion für diesen Pixels benutzt. Aufgrund der Verwendung von einfachen Shadowmaps können keine omnidirektionalen Lichtquellen verwendet werden. Stattdessen muss ein Spotlight verwendet werden. Das hat zur Folge das die Lichtquelle nicht innerhalb des Volumens platziert werden kann. Da ein Spotlight mit konstantem Öffnungswinkel verwendet wird, spielt die Entfernung zum Objekt ebenfalls eine entscheidende Rolle um den zur Verfügung stehenden Platz der Shadowmap optimal auszunutzen wie Abbildung 4.6 verdeutlicht. Damit der Anwender sich Far clipping plane Far clipping plane Objekt Far clipping plane Objekt Near clipping plane Objekt Near clipping plane Near clipping plane Lichtquelle Lichtquelle a) b) c) Lichtquelle Abbildung 4.6: In Bild a) ist die Lichtquelle zu nah am Objekt platziert. Das Objekt kann durch den festen Öffnungswinkel nur teilweise erfasst werden. In b) ist der Abstand der Lichtquelle zum Objekt für den gegebenen Öffnungswinkel und der gegebenen Objektgrösse optimal. In c) ist die Lichtquelle zu weit vom Objekt entfernt. Das Objekt wird komplett erfasst, allerdings wird die Shadowmap nicht optimal ausgenutzt. KAPITEL 4. IMPLEMENTIERUNG 61 nicht um diese Details bei der Positionierung der Lichtquelle kümmern muss, wurde das Interface so gewählt, dass der Anwender vielmehr nur die Richtung bestimmt aus der das Licht kommt. Die Renderklasse berechnet dann anhand der Grösse des Volumens und des vorgegebenen Öffnungswinkels der Lichtquelle automatisch die optimale Entfernung für die gegebene Lichtquelle. Die near- und far-clipping planes werden ebenfalls jeweils möglichst dicht an das Objekt gelegt, um eine möglichst hohe relative Genauigkeit des ZBuffers zu erreichen (siehe auch Kapitel 3.3.3). Bevor das Volumen gerendert wird, wird unterschieden ob die Beleuchtung aktiviert ist, Erst dann wird eine Shadowmap generiert, andernfalls wird das Objekt ohne Beleuchtung gerendert (siehe Quelltext 4.1). Quelltext 4.1 Bestimmung ob mit Beleuchtung gerendert wird. 1 2 3 4 5 6 7 8 9 if( m_bLightingEnabled ) { GenerateShadowMap(); DrawShadedVolume(); } else { DrawVolume(); } Die Methode GenerateShadowMap() erzeugt die Shadowmap für den aktuellen Frame (siehe Quelltext 4.2). Zunächst wird der Viewport auf die Grösse der Shadowmap gesetzt und der Z-Buffer gelöscht. Nachdem der Framebuffer deaktiviert wurde und der Betrachter auf die Position der Lichtquelle gesetzt wurde, wird ein Fragmentshader initialisiert. Der Fragmentshader hat lediglich die Aufgabe, die Transferfunktion anzuwenden. Das Volumen wird zwar nicht in den Framebuffer gerendert, aber der Z-Buffer enthält auf diese Weise nach dem Rendervorgang die korrekten Werte. Der Inhalt des Z-Buffers wird nun, mit Hilfe der OpenGL-Funktion glCopyTexSubImage2D() in den Speicherbereich einer Textur kopiert. Diese Textur, also die Shadowmap, wird im folgenden Renderzyklus dazu benutzt, zu bestimmen, ob ein Pixel beleuchtet ist. Der eigentliche Rendervorgang mit Beleuchtung findet in der Methode DrawShadedVolume() statt. Die Methode benutzt einen Vertex- und einen Fragmentshader zum Rendern des Volumens. Der Vertexshader berechnet KAPITEL 4. IMPLEMENTIERUNG 62 Quelltext 4.2 Erzeugen der Shadowmap 1 2 3 4 5 void C3dtCgLightingRenderEngine::GenerateShadowMap() { glViewport( 0, 0, m_aShadowMapWidth, m_aShadowMapHeight ); // Clear depth buffer glClear( GL_DEPTH_BUFFER_BIT ); 6 ... 7 8 // Disable writing to framebuffer since we only need the depth buffer values glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE ); 9 10 11 // Set lightview matrices glMatrixMode( GL_PROJECTION ); glPushMatrix(); glLoadMatrixf( m_aLightProjMat ); glMatrixMode( GL_MODELVIEW ); glPushMatrix(); glLoadMatrixf( m_aLightViewMat ); 12 13 14 15 16 17 18 19 ... 20 21 // Setup Shader m_cShaderManager.SelectShader( "FS_GenerateShadowMap" ); CCgFragmentShader* pFS = (CCgFragmentShader*)m_cShaderManager.GetShader( SHADER_FRAGMENT ); pFS->SetTextureParameter( "transferFunc", m_ColorTableTexture ); pFS->EnableTextureParameter( "transferFunc" ); pFS->Enable(); 22 23 24 25 26 27 28 29 ... 30 31 // Render the volume m_pTexture->Draw( aLightViewVec ); 32 33 34 // Disable Shader pFS->DisableTextureParameter( "transferFunc" ); pFS->Disable(); 35 36 37 38 // Restore matrices glPopMatrix(); glMatrixMode( GL_PROJECTION ); glPopMatrix(); glMatrixMode( GL_MODELVIEW ); 39 40 41 42 43 44 // Copy Z-Buffer to texture (shadowmap) glBindTexture( GL_TEXTURE_2D, m_ShadowMap ); glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_aShadowMapWidth, m_aShadowMapHeight ); 45 46 47 48 49 // Enable framebuffer again glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); 50 51 52 } KAPITEL 4. IMPLEMENTIERUNG 63 die Texturkoordinaten im Lichtraum, welche für den Lookup der Shadowmap im Fragmentshader benötigt werden (siehe Quelltext 4.3). Der Vertexshader benötigt für seine Berechnungen die zusammengefasste ModelviewProjektionsmatrix des Betrachters, sowie der Lichtquelle. Die Vertexkoordinaten werden zunächst mit der Modelview-Projektionsmatrix des Betrachters mutlipliziert um Clip-Koordinaten im Raum des Betrachters zu erhalten (vergleiche Abbildung 3.7). Clip-Koordinaten sind normiert auf den Sichtraum des Betrachters. Diese Transformation wird generell für alle Vertices durchgeführt. Da die Shadowmap aus Sicht der Lichtquelle erstellt wurde, werden die Vertexkoordinaten ebenfalls mit der Modelview-Projektionsmatrix der Lichtquelle multipliziert um die Clip-Koordinaten des Vertex im Lichtraum zu erhalten. Die Z-Werte der Clip-Koordinaten liegen im Intervall [-1,1]. Da Texturkoordinaten üblicherweise im Intervall [0,1] liegen müssen die ClipKoordinaten noch transformiert werden. Anschliessend werden sie als Texturkoordinaten weitergereicht. Quelltext 4.3 Vertexshader für das Shadowmapping 1 2 3 4 5 6 7 vertout VS_Lighting( appin IN, in uniform float4x4 glLightViewProjMat, in uniform float4x4 glModelViewProjMat ) { vertout OUT; // Pass the texture coordinate for the volume OUT.Tex0 = IN.Tex0; 8 float4 tmp; float4 pos; pos.xyz = IN.VPos.xyz; pos.w = 1.0; 9 10 11 12 13 // Calculate homogeneous coordinates OUT.HPos = mul( glModelViewProjMat, IN.VPos ); 14 15 16 // Transform current coordinates to light space tmp = mul( glLightViewProjMat, pos ); // Map coordinates from [-1..1] to [0..1] OUT.Tex1.xyz = 0.5 * tmp.xyz / tmp.w + float3( 0.5, 0.5, 0.5 ); 17 18 19 20 21 return OUT; 22 23 } An dieser Stelle knüpft der Fragmentshader an. Er vergleicht zunächst den KAPITEL 4. IMPLEMENTIERUNG 64 Z-Wert des aktuellen Fragments (im Lichtraum) mit dem korrespondierenden aus der Shadowmap. Sind die Werte (im Rahmen der Rechenungenauigkeit) gleich, ist das Fragment beleuchtet, andernfalls nicht (siehe Quelltext 4.4). Liegt das Fragment im Schatten, wird einfach der Grauwert des Fragments aus der 3D-Textur gelesen und als Index für die Transferfunktion benutzt. Der so erhaltene RGBA-Wert wird noch mit der ambienten Intensität multipliziert. Dadurch ist es möglich die ambiente Komponente abzuschwächen um intensivere Schatten zu gewinnen. Quelltext 4.4 Bestimmung ob das Fragment beleuchtet ist 1 2 // Shadowmap lookup float4 depth = tex2D( shadowMap, IN.Tex1.xy ); 3 4 5 6 7 8 9 10 11 12 // Compare depth values if( ( IN.Tex1.z - depth.z ) <= fShadowMapBias ) { // Fragment is illuminated } else { // Fragment is in shadow } Ist das Fragment beleuchtet, wird das Reflexionsmodell angewendet. Dazu wird zunächst die Normale des Fragments mittels zentraler Differenzen bestimmt (vergleiche Kapitel 3.2.2 und Quelltext 4.5). Dazu werden die benachbarten Texel des aktuellen Fragments jeweils in x, y und z-Richtung ausgelesen und daraus die entsprechenden Komponenten der Normalen bestimmt. Nun kann die Beleuchtungsgleichung angewendet werden. Die zusätzlich benötigten Parameter wie Richtung der Lichtquelle und des Betrachters werden dem Shader als Parameter übergeben. Die Beleuchtungsberechnung des Fragments zeigt Quellcode 4.6. Die Shadersprache Cg stellt bereits eine Funktion lit() bereit, welche die Beleuchtungskoeffizienten berechnet. Die lit()-Funktion basiert auf dem Beleuchtungsmodell von Blinn. Das Blinn Beleuchtungsmodell ist dem Phong Modell sehr ähnlich, verwendet aber zur Berechnung des Glanzlichts nicht den Reflexionsvektor ~r, sondern einen normierten Halbvektor ~h. Dieser ist KAPITEL 4. IMPLEMENTIERUNG 65 Quelltext 4.5 Bestimmung der Normalen im Fragmentshader 1 2 // The normal vector float3 N; 3 4 5 6 7 8 9 10 11 12 13 // Central difference N.x = 0.5f * (tex3D(texture3d, tex3D(texture3d, N.y = 0.5f * (tex3D(texture3d, tex3D(texture3d, N.z = 0.5f * (tex3D(texture3d, tex3D(texture3d, float3(IN.Tex0.x+offs.dx, IN.Tex0.y, IN.Tex0.z)).x float3(IN.Tex0.x-offs.dx, IN.Tex0.y, IN.Tex0.z)).x ); float3(IN.Tex0.x, IN.Tex0.y+offs.dy, IN.Tex0.z)).x float3(IN.Tex0.x, IN.Tex0.y-offs.dy, IN.Tex0.z)).x ); float3(IN.Tex0.x, IN.Tex0.y, IN.Tex0.z+offs.dz)).x float3(IN.Tex0.x, IN.Tex0.y, IN.Tex0.z-offs.dz)).x ); 14 Quelltext 4.6 Anwendung der Beleuchtungsgleichung im Fragmentshader 1 float4 k = lit( abs(dot( N, L )), abs(dot( N, H )), s ); 2 3 4 5 color.rgb = tex2D( transferFunc, float2( fIndex, Ia ) ).rgb + Ip*( tex2D( transferFunc, float2( fIndex, k.y ) ).rgb + k.z ); KAPITEL 4. IMPLEMENTIERUNG definiert als: ~ ~h = l + ~v k~l + ~v k 66 (4.1) Dabei ist ~l die Richtung aus der das Licht kommt und ~v die Richtung zum Betrachter. Die lit()-Funktion benötigt dazu drei Parameter, das Skalarprodukt zwischen der Normalen des Fragments (N) und des Lichtvektors (L), das Skalarprodukt zwischen der Normalen (N) und des Halbvektors (H) und einen Parameter der die Grösse des Glanzlichts bestimmt, auch bekannt als shininess oder Phong Exponent (s). Die Funktion liefert einen Vektor bestehend aus 4 Komponenten zurück, wobei die erste Komponente die ambiente Intensität beschreibt. Diese ist immer 1. Die zweite Komponente beschreibt die diffuse Intensität und die dritte Komponente die Intensität des Glanzlichts. Die vierte Komponente wird nicht verwendet und ist immer 1. Mit Hilfe dieser Intensitäten wird eine gewichtete Summe der einzelnen Beleuchtungskomponenten gebildet. Um im Shader Rechenzeit zu sparen, ist die Transferfunktion in Form einer 2D-Textur gespeichert. In dieser ist das Produkt c · k kodiert, wobei c der Wert der 1D Transferfunktion ist und k ein Wert zwischen 0 und 1. Auf diese Weise lassen sich im Shader einige Multiplikationen sparen. Die Isoflächenklasse Für die Implementation des in Kapitel 3.4 vorgestellten shaderbasierten Isoflächenrenderers wurde eine weitere Renderklasse C3dtCgIsoValueRenderEngine von der Klasse C3dtCgRenderEngine abgeleitet und die Methode RenderVolume() überladen. Diese benutzt nun den entsprechenden Fragmentshader zum rendern der Isoflächen. Den Pseudocode eines Fragmentshaders mit der in Kapitel 3.4 vorgestellten Funktionalität zeigt Quellcode 4.7. Hinsichtlich heutiger Fragmentprozessoren besteht bei der Umsetzung des Pseudocodes noch ein technisches Problem. Selbst aktuelle Fragmentprozessoren unterstützen noch keine dynamische Verzweigung (dynamic branching). Schleifen werden beim Kompilieren komplett abgerollt, das heisst die Anzahl der Iterationen die die Schleife durchläuft muss zum Zeitpunkt der Kompilation bekannt sein. Die for-Schleife im Pseudocode des Shaders läuft von 0 bis numIntervals. Da numIntervals aber ein Parameter ist der vom Anwender eingestellt werden soll, ist dieser nicht konstant. Die Anzahl der Iterationen ist also zum Zeitpunkt der Kompilation unbekannt. Eine Methode um dieses Problem zu umgehen liegt in der Möglichkeit, den Shader erst zur Laufzeit zu kompilieren. Dadurch besteht die Möglichkeit KAPITEL 4. IMPLEMENTIERUNG 67 Quelltext 4.7 Pseudocode des Fragmentshaders zur Darstellung von Isoflächen 1 2 3 4 5 6 7 8 void FS_IsoSurface( in uniform float2 isovals[], in uniform int numIntervals, out float4 color ) { // Get the current density CurrentIndex = GetGreyValueForCurrentFragment(); // Set output color to 0.0, which is completely transparent color.rgba = 0.0; 9 // Iterate iso intervals and check current fragment for( int i = 0; i < numIntervals; i++ ) { if( ( isovals[i].x <= CurrentIndex ) && ( isovals[i].y >= CurrentIndex ) ) { // Apply transfer function color = GetRGBAFromTransferFunction(); } } 10 11 12 13 14 15 16 17 18 19 } den Shader selbst auch erst zur Laufzeit zu generieren. Da die Anzahl der Intervalle, die dargestellt werden, bekannt sein muss bevor das Volumen visualisiert wird, kann ein Shader speziell für diese Anzahl Iterationen zur Laufzeit generiert werden. Natürlich bedeutet die Neukompilation eines Shaders für jeden Frame zusätzliche Performanceeinbußen, allerdings sind diese, da die Shader sehr kurz sind, eher gering im Vergleich zu zusätzlichen Renderzyklen bei nicht-shaderbasierten Verfahren. Ausserdem kann der Shader solange ohne Neukompilation benutzt werden, wie sich die Anzahl der Intervalle nicht ändert. Eine private Methode std::string GenerateShader( int nIntervals ) der Klasse generiert den Quellcode eines solchen Shaders für die angegebene Anzahl Intervalle. Dieser wird in Form eines Strings zurückgegeben und kann dann der Klasse CCgShaderManager mittels der Methode AddFragmentShader() zugänglich gemacht werden. Einen solchen generierten Fragmentshader für zwei Intervalle zeigt Quellcode 4.8. Der Shader holt sich zunächst den Grauwert aus der 3D-Textur (fIndex). Der Ausgabewert color wird komplett auf 0.0 gesetzt. Die im Pseudocode enthaltene Schleife ist im Quellcode des generierten Shaders bereits abgerollt. Da der Beispielcode für zwei Intervalle generiert wurde, enthält er zwei if-Blöcke die prüfen, ob der Index des aktuellen Fragments im entsprechenden Intervall liegt. Ist dies der Fall, wird die Transferfunktion angewendet, KAPITEL 4. IMPLEMENTIERUNG 68 Quelltext 4.8 Generierter Shadercode für die Darstellung von zwei Isoflächen 1 2 3 4 5 6 7 8 9 void FS_IsoSurface( in uniform sampler3D texture3d, in uniform sampler1D transferFunc, in uniform float fScale, in uniform float2 isovals[2], in float3 pos : TEXCOORD0, out float4 color : COLOR ) { float fIndex = tex3D( texture3d, pos ).x; color.rgba = 0.0; 10 if( ( isovals[0].x <= fIndex ) && ( isovals[0].y >= fIndex ) ) { color = tex1D( transferFunc, fIndex ); color.rgb *= fScale; } 11 12 13 14 15 16 if( ( isovals[1].x <= fIndex ) && ( isovals[1].y >= fIndex ) ) { color = tex1D( transferFunc, fIndex ); color.rgb *= fScale; } 17 18 19 20 21 22 } KAPITEL 4. IMPLEMENTIERUNG 69 andernfalls wird der Initialwert für color beibehalten. Die zusätzliche Multiplikation des RGB-Wertes mit der Variablen fScale hat für das Isoflächenrendering selbst keine Bedeutung. Mittels der Transferfunktion lassen sich den einzelnen Isointervallen unterschiedliche Farb- und Opazitätswerte zuordnen. Dadurch lassen sich auf einfache Weise Objekte semitransparent rendern (Abbildung 3.9). 4.2.3 Progressives Rendering Um das in Kapitel 3.5 vorgestellte progressive Rendering zu realisieren, muss das Modul VS um zwei Kernkomponenten erweitert werden: • Dem progressiven Renderer • Einem parallelen Renderthread Der progressive Renderer stellt eine Klasse dar, welche das sukzessive Rendern der einzelnen Detailstufen des Volumendatensatzes steuert. Das eigentliche Rendering des Volumens wird dabei weiterhin von der jeweiligen Renderklasse übernommen. Der progressive Renderer ruft dazu die Zeichenmethode der Renderklasse mit einer entsprechenden Detailstufe (Level of Detail ) auf. Das gerenderte Bild wird der Grafikpipeline in Form einer Textur erneut zugänglich gemacht. Diese wird dann, entsprechend der Detailstufe des zuvor gerenderten Volumens, hochskaliert und in den Framebuffer geschrieben. Damit der Rendervorgang bei Interaktion durch den Anwender abgebrochen werden kann, muss das Rendern in einem eigenen Thread erfolgen. Dieser muss parallel zum Hauptthread laufen, welcher die Benutzereingaben verarbeitet. Ändert der Anwender einen Parameter, wird der Renderthread benachrichtigt den aktuellen Renderzyklus abzubrechen und mit den neuen Parametern zu rendern. Um VS um diese Funktionalität zu erweitern, wurden zwei weitere Klassen eingeführt: CRenderThread und CProgressiveRenderer. Die Integration dieser Klassen in VS zeigt Abbildung 4.7. Der progressive Renderer Ursprünglich wurde das Rendern eines Frames innerhalb der paintGL-Methode der Klasse CRenderWidget erledigt. Bei der Methode paintGL handelt es sich um einen Callback der jedesmal automatisch aufgerufen wird, sobald das GLFenster aktualisiert werden muss. Um die Funktionalität des progressiven 1 1 VS::VoxelSculpture 1 VS::CTexture2d n 1 n VS::CBrick ITWM::CBox 1 VS::CCgVertexShader VS::CCgFragmentShader VS::CCgShader 1 1 VS::StShader n 1 VS::CCgShaderManager VS::C3dtCgLightingRenderEngine VS::C3dtCgRenderEngine VS::C3dtCgIsoValueRenderEngine VS::StIntersectionPoint 1 VS::CTexture3d 1 VS::CTexture VS::CRenderEngine 1 1 VS::C3dtRenderEngine 1 1 VS::CRenderThread VS::CProgressiveRenderer 1 1 1 VS::CRenderWidget QGLWidget VS::CMultiTexture2d n VS::C2d3tsRenderEngine ITWM::CImage 1 KAPITEL 4. IMPLEMENTIERUNG 70 Abbildung 4.7: Statisches UML-Klassendiagramm des Moduls Voxel-Sculpture, erweitert um progressives Rendering KAPITEL 4. IMPLEMENTIERUNG 71 Renderings zu kapseln, wurde eine neue Klasse CProgressiveRenderer eingeführt. Der Code zum Rendern eines Frames wurde somit in die progressive Renderklasse verlagert und um entsprechende Teile erweitert. Der progressive Renderer besitzt dazu eine Methode Render(). Diese Methode entscheidet zunächst, ob progressives Rendering durchgeführt werden soll und rendert dann entsprechend den Frame (siehe Quellcode 4.9). Soll progressiv gerendert werden, wird zunächst über die private Methode RenderSingleFrame() ein Frame mit dem entsprechend niedrig aufgelösten Datensatz gerendert. Anschliessend wird überprüft, ob der Rendervorgang aufgrund von Benutzereingaben bereits abgebrochen werden soll. Ist das nicht der Fall, wird ein Datensatz mittlerer Auflösung gerendert. Dies entspricht der ersten Verfeinerung des bereits gerenderten, grob aufgelösten Volumens. Wird anschliessend immer noch nicht abgebrochen, wird der Originaldatensatz in höchster Auflösung gerendert, der letzten Verfeinerungsstufe. Das progressive Rendering besteht derzeit also aus drei Renderzyklen mit entsprechenden Qualitätsabstufungen. Der Datensatz für die niedrigste Qualitätsstufe besitzt nur ein Viertel der Grösse des Originalvolumens pro Dimension. Insgesamt reduziert sich die Anzahl der Voxel für diesen Datensatz 1 der ursprünglichen Grösse. Der mittlere Datensatz besitzt für jede so auf 64 Dimension die halbe Grösse, reduziert die Anzahl der Voxel somit auf 81 . Die Methode RenderSingleFrame() besitzt ausserdem zwei Parameter: unsigned int nLOD und bool bOffscreen. Der erste Parameter gibt das Level of Detail an, also welche Detailstufe gerendert werden soll, der zweite Parameter gibt an, ob ein Offscreen-Buffer für das Rendern benutzt werden soll, oder nicht. Im Falle der höchsten Detailstufe macht es keinen Sinn zunächst in einen Offscreen-Buffer zu rendern, weil das Resultat nicht mehr skaliert werden muss. Es entspricht ja automatisch der richtigen Grösse. Aus Performancegründen wird die höchste Detailstufe des Volumens deshalb direkt in den Framebuffer gerendert. Während die Methode Render() dafür sorgt, das die einzelnen Qualitätstufen sukzessiv gerendert werden, geschieht das Rendern in den Offscreen-Buffer und dessen anschliessende Darstellung im Framebuffer, innerhalb der Methode RenderSingleFrame(). Diese Methode ist recht umfangreich. Sie behandelt das Offscreen-Rendering sowohl für die normale, als auch für die rot/grün Darstellung[Hir05]. Weiterhin behandelt sie auch das ursprünglich implementierte, direkte Rendern in den Framebuffer. Das direkte Rendern unterstützt zur Zeit aber kein progressives Rendering. Die in Quellcode 4.10 und 4.11 gezeigten Ausschnitte dieser Methode zeigen das Rendern in den OffscreenBuffer und die anschliessende Verwendung dessen Inhalts als Textur (verglei- KAPITEL 4. IMPLEMENTIERUNG 72 Quelltext 4.9 Eine progressive Renderfolge 1 2 3 4 5 6 7 8 9 void CProgressiveRenderer::Render() { if( m_bRender ) { if( m_bRenderProgressive && !m_bDrawProxyGeometry ) { // Render progressive RenderSingleFrame( 2, true ); m_pGLWidget->swapBuffers(); 10 if( m_bStopRendering ) { m_bStopRendering = false; return; } RenderSingleFrame( 1, true ); m_pGLWidget->swapBuffers(); 11 12 13 14 15 16 17 18 if( m_bStopRendering ) { m_bStopRendering = false; return; } 19 20 21 22 23 24 // Render high quality frame directly to the framebuffer (performance reason) RenderSingleFrame( 0, false ); m_pGLWidget->swapBuffers(); m_bStopRendering = false; 25 26 27 28 } else { // Render non progressive RenderSingleFrame( 0, false ); m_pGLWidget->swapBuffers(); } 29 30 31 32 33 34 35 } 36 37 } KAPITEL 4. IMPLEMENTIERUNG 73 che auch Abbildung 3.10). Prinzipiell ist es ebenso möglich, anstelle eines Offscreen-Buffers den normalen Framebuffer zum Rendern zu benutzen. Der Inhalt kann anschliessend ausgelesen und in den Speicherbereich einer Textur kopiert werden. Die Funktionalität wäre dieselbe. Diese Vorgehensweise wäre aber langsamer, da der Inhalt des Framebuffers kopiert werden muss. Die OpenGL-Erweiterung GL EXT framebuffer object erlaubt es sogenannte framebuffer-attachable images als Renderziel anzugeben. Wird als Renderziel eine Textur angegeben, erlaubt diese Erweiterung das direkte Rendern in eben diese Textur. Dadurch wird das Kopieren des Inhalts des Framebuffers in eine Textur vermieden und erlaubt eine wesentlich effizientere Implementierung. Der Renderthread Damit die Interaktivität während des progressiven Renderings erhöht wird, muss eine Möglichkeit bestehen, den aktuellen Rendervorgang bei Eintritt einer Interaktion seitens des Anwenders abzubrechen. Dann kann ein neuer Rendervorgang mit den entsprechend aktualisierten Parametern gestartet werden. Damit eine Eingabe während der Rendervorgangs überhaupt verarbeitet werden kann, muss das Rendering parallel zur Hauptanwendung ausgeführt werden. Die Hauptanwendung reagiert so auf Benutzereingaben und kann entsprechende Signale an den parallel laufenden Renderthread senden. Die Klasse CRenderThread implementiert den Renderthread. Sie ist von der Klasse QThread abgeleitet. QThread ist Bestandteil der Qt-Bibliothek und liefert die Funktionalität um einen parallelen Thread zu erzeugen. Die eigentliche Funktionalität ist in der Methode run() implementiert. Bei run() handelt es sich um eine pur-virtuelle Methode der Basisklasse QThread. Wird der Thread gestartet, so läuft die run()-Methode parallel zum übrigen Programm. Sämtlicher Code, den diese Methode enthält, also auch Methodenaufrufe aggregierter Klassen, werden parallel zum Rest der Anwendung ausgeführt. Da im Renderthread nur das Rendern des Volumens stattfindet, ist die Klasse sehr einfach aufgebaut. Die run()-Methode enthält eine Schleife, die bei jedem Durchlauf einen Frame rendert. Damit keine unnötige Rechenzeit beansprucht wird, legt der Thread sich nach jedem vollständigen Schleifendurchlauf schlafen. Um einen weiteren Frame zu rendern muss er von aussen geweckt werden. Da das eigentliche Rendering von der jeweiligen Renderklasse erledigt wird, diese jedoch von der Klasse CRenderWidget, beziehungsweise jetzt indirekt vom progressiven Renderer CProgressiveRenderer, verwaltet werden, ruft KAPITEL 4. IMPLEMENTIERUNG 74 Quelltext 4.10 Progressives Rendern eines Frames (Teil 1) 1 2 3 void CProgressiveRenderer::RenderSingleFrame( unsigned int nLOD, bool bOffscreen ) { ... 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // Activate offscreen buffer VS::CGlExtensions::g_pGlBindFramebufferEXT( GL_FRAMEBUFFER_EXT, m_nFramebuffer ); switch( nLOD ) { case 0: ... case 1: ... // Bind texture to offscreenbuffer (to render into the texture) case 2: VS::CGlExtensions::g_pGlFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, m_nFBOTextureLow, 0 ); // Generate depth buffer for offscreen rendering VS::CGlExtensions::g_pGlRenderbufferStorageEXT( GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, m_nViewportWidth/4, m_nViewportHeight/4 ); // Set viewport to match offscreen buffers size glViewport( 0, 0, m_nViewportWidth/4, m_nViewportHeight/4 ); m_pEngine->SetViewport( m_nViewportWidth/4, m_nViewportHeight/4 ); break; default: ... } // Attach depth buffer to offscreen framebuffer VS::CGlExtensions::g_pGlFramebufferRenderbufferEXT( GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, m_nRenderbuffer ); // Clear the offscreen frame- and depthbuffer glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); 29 30 ... 31 32 33 // Render the volume with the given LOD to the offscreen buffer m_pEngine->RenderVolume( nLOD ); 34 35 36 37 38 39 // Rebind the normal framebuffer and reset viewport VS::CGlExtensions::g_pGlBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 ); glViewport( 0, 0, m_nViewportWidth, m_nViewportHeight ); m_pEngine->SetViewport( m_nViewportWidth, m_nViewportHeight ); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); 40 41 42 43 44 45 // Set up a orthographic projection matrix to display texture glMatrixMode( GL_PROJECTION ); ... glLoadIdentity(); glOrtho( 0.0, 1.0, 0.0, 1.0, 1.0, 10.0 ); 46 47 48 49 50 51 52 53 glMatrixMode( GL_MODELVIEW ); ... glLoadIdentity(); gluLookAt( 0.0f, 0.0f, -4.0, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f); ... KAPITEL 4. IMPLEMENTIERUNG 75 Quelltext 4.11 Progressives Rendern eines Frames (Teil 2) // Bind the texture containing the offscreen buffers content switch( nLOD ) { case 0: ... case 1: ... case 2: glBindTexture( GL_TEXTURE_2D, m_nFBOTextureLow ); break; default: ... } 1 2 3 4 5 6 7 8 9 10 ... 11 12 // Draw the texture onto a quad that matches the // Thus, the texture will be scaled accordingly glBegin( GL_QUADS ); glTexCoord2f( 0.0, 0.0 ); glVertex2f( 0.0, 0.0 glTexCoord2f( 1.0, 0.0 ); glVertex2f( 0.0, 1.0 glTexCoord2f( 1.0, 1.0 ); glVertex2f( 1.0, 1.0 glTexCoord2f( 0.0, 1.0 ); glVertex2f( 1.0, 0.0 glEnd(); 13 14 15 16 17 18 19 20 framebuffers viewport size. ); ); ); ); 21 ... 22 23 // Render the volume again, but only to the depth buffer, to restore the depth values m_pEngine->RenderVolume( nLOD, true ); 24 25 26 // Render auxiliaries m_pEngine->RenderBBox( nLOD ); m_pEngine->RenderControls(); m_pEngine->RenderAxis(); m_pGLWidget->DrawGrid(); m_pGLWidget->DrawAxis(); 27 28 29 30 31 32 33 ... 34 35 36 } KAPITEL 4. IMPLEMENTIERUNG 76 der Renderthread lediglich eine Methode RenderFrame() des Renderwidgets auf. Das Renderwidget bildet den zentralen Knoten, von dem aus die Aufrufe an die entsprechenden Instanzen weitergesendet werden, in diesem Fall an den den progressiven Renderer. Auf diese Weise ist es einfach möglich, das Modul VS um weitere Module zu erweitern, da alle zentral vom Renderwidget gesteuert werden. Da sämtliche Aufrufe innerhalb der Methode run() des Renderthreads natürlich ebenfalls parallel zur Hauptanwendung laufen, wird das Rendern des Volumens, welches indirekt durch den Renderthread angestossen wird, parallel zum Hauptthread durchgeführt. Ein Problem bei der Parallelisierung von VS besteht darin, das die 3dGrafikbibliothek OpenGL nicht threadsicher ist. Da OpenGL-Funktionen sowohl aus dem Hauptthread, als auch aus dem Renderthread aufgerufen werden, müssen diese synchronisiert werden, um Inkonsistenzen zu vermeiden. Dies geschieht über einen Mutex. Die Qt-Bibliothek stellt mit der Klasse QMutex eine solche Funktionalität bereit. Ein Mutex kann die Zustände locked oder unlocked haben. Damit beide Threads OpenGL-Funktionen nicht gleichzeitig aufrufen, müssen sämtliche Sektionen die OpenGL-Aufrufe beinhalten mit einem Mutex zunächst gesichert werden. Dabei wird der Mutex in den Zustand locked versetzt und die OpenGL-Funktionen können ausgeführt werden. Hat ein anderer Thread den Mutex bereits in den Zustand locked versetzt, blockiert dieser Aufruf solange, bis der andere Thread den Mutex wieder freigibt. Dadurch ist sichergestellt, dass OpenGL-Funktionen immer nur von einem Thread aufgerufen werden. Im nächsten Kapitel wird das Laufzeitverhalten mit eingeschalteter Beleuchtung, sowie der Isoflächendarstellung untersucht. Weiter wird gezeigt wie sich das progressive Rendering auf die Framerate auswirkt. Ausserdem werden einige Anregungen für die Weiterentwicklung gegeben. Kapitel 5 Ergebnisse In diesem Kapitel wird das Laufzeitverhalten der neu implementierten Renderklassen untersucht. Von besonderem Interesse ist dabei der Vergleich zwischen einem mit Beleuchtung gerenderten Volumen und einem ohne Beleuchtung. Bei der Isoflächendarstellung wird untersucht, wie sich die Laufzeit mit zunehmender Anzahl dargestellter Isointervalle verändert. Bei sämtlichen Messungen wird ausserdem der Einfluß des progressiven Renderings näher untersucht. Anschliessend wird ein Ausblick für die Weiterentwicklung von Voxel-Sculpture gegeben. 5.1 Messungen Um das Laufzeitverhalten zu untersuchen, wurde die Anzahl der gerenderten Bilder pro Sekunde (FPS, frames per second) unter unterschiedlichen Bedingungen für verschiedene Bilder gemessen. Dazu wurde ein Zähler integriert, der die Zeit zum Rendern von 20 Frames misst und daraus anschliessend einen Durchschnittswert für das Rendern eines einzigen Frames berechnet. Für die Messungen standen zwei Hardwarekonfigurationen zur Verfügung: Rechner A Rechner B CPU RAM Grafikkarte Betriebssystem CPU RAM Grafikkarte Betriebssystem Dual Intel Xeon 1,7GHz 2,0GB NVidia GeForce FX 5200 SuSE Linux 9.0 AMD Athlon XP 2400+ 2,0GHz 1,5GB NVidia GeForce 6800 GT SuSE Linux 9.0 Die Leistungsfähigkeit beider CPUs unterscheidet sich deutlich voneinander. Allerdings, hat sich bereits bei den Messungen in [Hir05] gezeigt, dass diese 77 KAPITEL 5. ERGEBNISSE 78 die Visualisierung nur sehr gering beeinflussen. Die erreichten Frameraten werden hauptsächlich durch den Grafikprozessor beeinflusst. Da die Renderklassen hauptsächlich Fragmentshader zur Visualisierung verwenden, ist die Leistungsfähigkeit des Fragmentprozessors von besonderem Interesse. Für die Messungen stehen zwei, von der Leistungsfähigkeit recht unterschiedliche Grafikkarten zur Verfügung. Konfiguration A ist mit der NVidia Geforce FX 5200 ausgestattet. Diese Grafikkarte bietet eine eher mäßige Performance bezüglich Fragmentshading. Die NVidia GeForce 6800 GT aus Konfiguration B dagegen, ist allgemein leistungsfähiger und führt Fragmentshader mit einer vergleichsweise hohen Geschwindigkeit aus. Die Messungen wurden mit folgenden Bildern durchgeführt: Sinterkupfer Ersteller K. Pischang, TU Dresden Abbildung A.1, Seite 88 Schädel Datenerfassung Dimensionen (Pixel) Größe Ersteller Abbildung A.2, Seite 90 Feuerbeton Datenerfassung Dimensionen (Pixel) Größe Ersteller Abbildung A.3, Seite 92 Datenerfassung Dimensionen (Pixel) Größe Computertomographie (XCT) 330 x 328 x 222 22,9MB Siemens Medical Systems, Forchheim Rotations-Röntgenscan 256 x 256 x 256 16,0MB S. Gondrom, IzfP Saarbrücken Prof. Schlegel, TU Freiburg Computertomographie (XCT) 128 x 128 x 128 2,0MB Wie in Kapitel 3.5 bereits erwähnt, ist das Laufzeitverhalten der Fragmentshader direkt von dem verwendeten Zoomfaktor abhängig, da bei größerem Zoom mehr Fragmente erzeugt werden, für die der Shader jeweils ausgeführt wird. Um vergleichbare Ergebnisse zu erhalten, wurden sämtliche Messungen mit demselben Zoomfaktor durchgeführt. Dadurch ist es möglich die Messergebnisse in Zusammenhang mit der Anzahl der Pixel der einzelnen Bilder zu setzen. Einige Besonderheiten gelten für die Messungen mit aktiviertem progressiven Rendering. Eine komplette Renderfolge des progressiven Renderers dauert natürlich länger als das Rendern eines einzelnen hochaufgelösten Frames, da der progressive Renderer das Volumen sukzessiv, zunächst in niedriger, mittlerer und anschliessend in hochaufgelösender Form rendert. Der Rendervorgang kann derzeit nur abgebrochen werden, wenn eine der drei Stufen KAPITEL 5. ERGEBNISSE 79 komplett gerendert worden ist. Dadurch ist es problematisch, die Laufzeit des progressiven Renderers, beziehungsweise dessen effektiven Beitrag zur Erhöhung der Framerate während einer Interaktion, zu messen. Bei den Messungen wurden dem Renderthread, mittels einer Endlosschleife, permanent Signale zum Neuzeichnen des Frames gesendet. Im Bezug auf das progressive Rendering bedeutet dies, dass meist nur die niedrig aufgelöste Variante gerendert wurde. Weitere Besonderheiten des progressiven Renderings werden in den entsprechenden Kapiteln behandelt. 5.1.1 Die Beleuchtungsklasse Von besonderem Interesse ist ein Vergleich der Laufzeit zwischen beleuchtetem und unbeleuchtetem Volumen. Folgende Tabellen fassen die Ergebnisse zusammen: Messungen ohne progressives Rendering Konfiguration Bild FPS mit Beleuchtung FPS ohne Beleuchtung Rechner A Sinterkupfer 0,029 0,716 Schädel 0,059 1,020 Feuerbeton 0,316 7,632 Rechner B Sinterkupfer 1,004 11,770 Schädel 1,562 14,638 Feuerbeton 11,347 78,571 Messungen mit progressivem Rendering Konfiguration Bild FPS mit Beleuchtung FPS ohne Beleuchtung Rechner A Sinterkupfer 2,166 6,210 Schädel 3,215 9,402 Feuerbeton 20,708 53,907 Rechner B Sinterkupfer 26,813 50,135 Schädel 85,450 122,583 Feuerbeton 149,105 181,851 Ein weiterer interessanter Aspekt, ist ein Vergleich der Laufzeit zwischen der ursprünglichen 3d-Renderklasse, die ohne Shader arbeitet, und der neuen 3dRenderklasse, bei der die Transferfunktion mit Hilfe eines Fragmentshaders angewendet wird. Ansonsten unterscheiden sich die beiden Renderklassen nicht. Daraus lassen sich weitere Schlüsse bezüglich des Laufzeitverhaltens der Shader ziehen. Da die GeForce 6800 GT aufgrund einer nicht mehr unterstützten OpenGL Erweiterung nur die shaderbasierte Visualisierung unterstützt, konnte der Test ausschliesslich auf Rechner A durchgeführt werden. Die Messung ergab folgende Ergebnisse: KAPITEL 5. ERGEBNISSE 80 Messungen ohne progressives Rendering Konfiguration Bild FPS ohne Shader FPS mit Shader Rechner A Sinterkupfer 1,417 0,716 Schädel 1,523 1,020 Feuerbeton 10,899 7,632 Messungen mit progressivem Rendering Konfiguration Bild FPS ohne Shader FPS mit Shader Rechner A Sinterkupfer 18,596 6,210 Schädel 21,676 9,402 Feuerbeton 92,028 53,907 5.1.2 Die Isoflächenklasse Die Isoflächenklasse generiert die verwendeten Shader, wie in Kapitel 4.2.2 beschrieben, anhand der Anzahl der Isointervalle, zur Laufzeit. Dadurch ergeben sich natürlich unterschiedliche Laufzeiten für eine verschiedene Anzahl Isointervalle. Die Messungen wurden jeweils mit 1, 2, 4 und 8 verwendeten Isointervallen durchgeführt. Messungen ohne progressives Rendering Konfiguration Bild 1 2 4 8 Rechner A Sinterkupfer 0,425 0,220 0,121 0,095 Schädel 0,671 0,313 0,173 0,090 Feuerbeton 4,542 2,483 1,383 0,722 Rechner B Sinterkupfer 8,441 7,709 4,918 2,443 Schädel 17,002 12,681 6,927 3,418 Feuerbeton 18,387 15,988 9,680 10,569 Messungen mit progressivem Rendering Konfiguration Bild 1 2 4 8 Rechner A Sinterkupfer 3,457 1,814 0,979 0,510 Schädel 5,012 2,606 1,409 0,733 Feuerbeton 30,064 18,895 10,764 5,752 Rechner B Sinterkupfer 18,278 15,360 8,890 9,375 Schädel 22,893 19,217 9,951 11,454 Feuerbeton 23,173 19,378 10,018 11,278 5.1.3 Auswertung Allgemein fallen zunächst die geringen Frameraten von Rechner A, insbesondere bei der Visualisierung grösserer Datenmengen, auf. Dies bestätigt die KAPITEL 5. ERGEBNISSE 81 Eingangs erwähnte, mäßige Shaderperformance der verwendeten Grafikkarte. Ein weiteres Indiz dafür liefert der direkte Vergleich mit der ursprünglichen 3d-Renderklasse, welche keine Shader einsetzt. Obwohl die shaderbasierte Variante im Fragmentshader lediglich zwei Texturzugriffe und eine Multiplikation durchführt, ist sie durchschnittlich etwa doppelt so langsam, sowohl mit als auch ohne progressivem Rendering. Mit zunehmender Grösse des Volumens steigt die Laufzeit durch die zunehmende Anzahl der Shaderdurchläufe weiter. Beleuchtung Mit eingeschalteter Beleuchtung erhöht sich die Laufzeit für Rechner A um etwa Faktor 22, für Rechner B um etwa Faktor 10. Für Rechner B unterscheidet sich die Laufzeit zwischen beleuchtetem und unbeleuchtetem Volumen beim Sinterkupfer um etwa Faktor 11, sinkt beim Feuerbeton aber auf etwa Faktor 7. Bei Rechner A ist der Faktor 22 relativ konstant für alle Volumen. Daraus lässt sich folgern, dass die Shader bei Rechner A in allen Fällen den Flaschenhals bilden, während sie bei Rechner B für kleine Datensätze nicht so stark ins Gewicht fallen, was auf die effektivere Shadereinheit bei Rechner B zurückzuführen ist. Auch lässt sich Erkennen, dass die Laufzeit ungefähr linear mit mit der Anzahl der gerenderten Pixel wächst. Das war zu Erwarten, da die Anzahl der Shaderaufrufe direkt von dieser Grösse abhängig ist. Durch das progressive Rendering lassen sich die Laufzeiten erheblich verkürzen. Es ist eine deutliche Erhöhung der Frameraten sowohl für Rechner A, als auch für Rechner B zu erkennen. Der Steigerungsfaktor liegt je nach Grösse des Datensatzes und aktivierter Beleuchtung zwischen 2 und 74. Insbesondere die Beleuchtung profitiert vom progressiven Rendering, da sich die Shaderaufrufe durch das niedriger aufgelöste Volumen erheblich reduzieren. Die Laufzeitunterscheide zwischen beleuchtetem und unbeleuchtetem Objekt liegen, für Rechner A, nur noch bei etwa Faktor 2.8 und bei Rechner B um etwa 1.5. Auffällig dabei ist, dass das lineare Kostenwachstum bezüglich der Anzahl der Pixel des Volumens für Rechner A weiterhin gegeben ist, für Rechner B jedoch nicht. Die Laufzeitunterschiede zwischen dem Schädel und dem Feuerbeton betragen bei Rechner A etwa den Faktor 6, während sie bei Rechner B nur etwa 1.5 betragen. Dies lässt sich durch eine Auffälligkeit des progressiven Renderings während der Messungen erklären. Wie eingangs erwähnt, werden ständig Signale zum Neuzeichnen an den Renderthread gesendet. Dadurch wird beim progressiven Rendering meist nur die niedrige Detailstufe KAPITEL 5. ERGEBNISSE 82 dargestellt. Bei Rechner B fiel während der Messungen auf, das die Grafikkarte diese niedrige Detailstufe sogar schneller rendert, als Signale zum Neuzeichnen des Volumens eintreffen. Dadurch wird nicht nach der niedrigen Detailstufe abgebrochen, sondern bereits die mittlere gerendert, erst danach wurde das entsprechende Signal empfangen und die Renderfolge abgebrochen. Da Rechner B also mehr als nur die niedrigste Detailstufe gerendert hat, sinkt die Framerate natürlich entsprechend und die Messungen zwischen beiden Systemen sind nicht mehr vergleichbar. Zum Vergleich werden die Ergebnisse in Abbildung 5.1 und 5.2 nochmal grafisch verdeutlicht. Abbildung 5.1: Ergebnisse der Frameratenmessung für die Beleuchtungsklasse Abbildung 5.2: Ergebnisse der Frameratenmessung für die Beleuchtungsklasse mit progressivem Rendering KAPITEL 5. ERGEBNISSE 83 Isoflächendarstellung Bei der Isoflächendarstellung fällt auch das bereits erwähnte, lineare Kostenwachstum bezüglich der Pixel des Volumens für Rechner A auf. Dieses gilt auch für die progressive Visualisierung. Bei Rechner B hingegen sind die Laufzeitunterschiede zwischen dem Schädel und dem Feuerbeton eher gering. Das deutet wieder darauf hin, dass die Shaderperformance bei Rechner A den Flaschenhals bildet. Für Rechner B scheint dies, zumindest für kleinere Volumendaten, nicht zuzutreffen. Die Laufzeit des Shaders nimmt mit steigender Anzahl dargestellter Isointervalle zu. Die folgenden Grafiken setzen die erreichte Framerate in Bezug zur Anzahl gerenderter Isointervalle. Die Anzahl der verwendeten Isointervalle wurde bei jeder Messung jeweils verdoppelt. Generell lässt sich erkennen, das die Laufzeit der Isoflächendarstellung un- Abbildung 5.3: Grafische Darstellung der Messergebnisse der Isoflächen für Rechner A gefähr linear mit der Anzahl der Isointervalle wächst. Verdoppelt man die Anzahl der Intervalle, halbiert sich die Framerate. Bei Rechner B bleibt die Framerate für kleine Volumendatensätze und einer hohen Intervallzahl allerdings ungefähr konstant. Eine Erklärung für dieses Phänomen konnte bisher nicht gefunden. Auch wiederholte Messungen kamen zu dem gleichen Ergebnis. KAPITEL 5. ERGEBNISSE 84 Abbildung 5.4: Grafische Darstellung der Messergebnisse der Isoflächen für Rechner B Die Tests haben gezeigt, das das Laufzeitverhalten direkt von der Leistungsfähigkeit der Grafikkarte abhängt. Insbesondere ist hierbei die Leistung der Fragmentprozessoren von Bedeutung. Der Prozessor und die CPU wirken sich kaum auf die Framerate aus. Obwohl Rechner A diesbezüglich besser ausgestattet war, wurden auf Rechner B, mit einer deutlich leistungsfähigeren Grafikkarte, wesentlich höhere Frameraten erzielt. Als Fazit kann man sagen, dass eine Volumenvisualisierung mit Beleuchtung und Schatten bei interaktiven Frameraten möglich ist. Allerdings fordert diese den Einsatz modernster Grafikkarten, da, wie am Beispiel der GeForce FX 5200 gezeigt, die Frameraten sonst zu stark sinken. Progressives Rendering hilft dabei, die Frameraten, gerade auf älteren Grafikkarten, wieder auf ein akzeptables Maß zu heben. Es ist zu erwarten, dass die Effizienz der GPUs weiter steigen wird, so dass immer grössere Datenmengen interaktiv, oder sogar in Echtzeit, mit Beleuchtung visualisiert werden können. Durch den besseren visuellen Eindruck, unter Einsatz von Beleuchtung, ist eine Entwicklung in diese Richtung gerechtfertigt. Anhang A enthält einige Visualisierungen. Dabei zeigen sich die Unterschiede zwischen beleuchtetem und unbeleuchtetem Volumen sehr deutlich. KAPITEL 5. ERGEBNISSE 5.1.4 85 Ausblick Im Hinblick auf zukünftige Entwicklungen, kann Voxel-Sculpture um weitere Renderverfahren erweitert werden. Denkbar wäre zum Beispiel ein polygonbasiertes Visualisierungsverfahren. So könnte ein, derzeit als eigenständiges Modul vorliegendes, Oberflächenrendering, welches auf dem Marching-CubesAlgorithmus basiert in Voxel-Sculpture integriert werden. Aber auch ein raycastingbasiertes Verfahren wäre denkbar. Dieses ist sehr rechenaufwändig und wird typischerweise nur auf CPUs implementiert, obwohl bereits erste Ansätze existieren, Raycaster auch auf der GPU zu implementieren. Deshalb wird es auf herkömmlicher PC-Hardware kaum interaktive Frameraten erreichen, würde aber bezüglich der Darstellungsqualität eine hochwertige Alternative zu den derzeit implementierten Verfahren darstellen. Eventuell wäre auch ein hybrides Verfahren denkbar, so könnte man das texturbasierte Rendering zum Einstellen der Parameter nutzen, um anschliessend das Bild mittels Raycaster in sehr hoher Qualität zu rendern. Bei der Implementierung der Beleuchtung haben sich auch Schwächen bezüglich des Renderklasseninterfaces gezeigt. Die Basisklasse CRenderEngine ist für eine Unterstützung beliebiger Renderklassen noch zu speziell. Insbesondere die enge Anbindung an das Renderwidget bereitete viele Probleme. Um eine flexible Einbindung beliebiger Renderklassen zu gewähren, müssten diese sämtliche Manipulationswerkzeuge für die Darstellung selbst bereitstellen. Bisher übernimmt das Renderwidget Aufgaben wie die Rotation oder das setzen benutzerdefinierter Clippingebenen. Die Funktionalität des Renderwidgets würde sich darauf beschränken, das OpenGL-Fenster bereitzustellen und die entsprechende Renderklasse zu initialisieren. Dabei gilt es einen geeigneten Mechanismus zu entwickeln, der es erlaubt, die von der Renderklasse bereitgestellten Einstellungsdialoge und Manipulationswerkzeuge automatisch auf der GUI des Renderwidgets zu plazieren. Auf diese Weise würde die Abhängigkeit zwischen Renderklasse und Renderwidget auf ein Minimum reduziert, wodurch man flexibel beliebige, völlig unterschiedliche, Renderklassen implementieren könnte. Eine Möglichkeit, die Beleuchtungsklasse zu verbessern besteht in der Verwendung von sogenannten Deep Shadow Maps[LV]. Im Gegensatz zu einfachen Shadow Maps, welche nur einen einzigen Tiefenwert für den der Kamera nächsten Pixel speichern, speichern Deep Shadow Maps den Sichtbarkeitsanteil sämtlicher Pixel. Dadurch bietet sich die Möglichkeit, Schatten auch für halbtransparente Medien korrekt darzustellen. Derzeit wird jeder dargestellte Pixel für die Schattengenerierung, unabhängig von seiner eingestellten Transparenz, als völlig opak angenommen. Ein Pixel ist dadurch entweder vollständig beleuchtet oder liegt vollständig im Schatten. Deep Shadow Maps KAPITEL 5. ERGEBNISSE 86 erlauben eine sukzessive Abschwächung der Lichtintensität anhand der Opazität der betroffenen Pixel. Auch eignen sich Deep Shadow Maps besser für die Darstellung filigraner Strukturen und bieten bei einer geringeren Auflösung eine höhere Bildqualität. Um die Visualisierung weiter zu beschleunigen, besteht die Möglichkeit zwei Grafikkarten zu bündeln. Der Hersteller NVidia bietet unter dem Namen SLI eine solche Technologie. Theoretisch lässt sich dadurch die Rechenleistung verdoppeln. Es ist zu untersuchen, ob und wie Voxel-Sculpture angepasst werden muss, damit es von dieser Methode profitiert. Moderne Grafikkarten unterstützen mittlerweile beliebige Texturgrössen. VoxelSculpture kann dies nutzen um die Geometrieberechnung zu beschleunigen. Beim progressiven Rendering fällt negativ auf, dass die Renderfolge noch nicht an beliebiger Stelle abgebrochen werden kann, sondern nur jeweils nach einer komplett fertiggerenderten Detailstufe. Es wäre möglich, die Überprüfung des Abbruchsignals nicht nach jeder Detailstufe vorzunehmen, sondern nach jeder gerenderten Texturscheibe. Der interaktive Eindruck wird dadurch weiter verbessert. Die Normalenberechnung für die Beleuchtung findet zurzeit im Fragmentshader statt. Es ist möglich diese vorzuberechnen und dem Shader als Textur zugänglich zu machen. Dadurch verringert sich die Laufzeit des Shaders. Da die Vorberechnung der Normalen nicht zeitkritisch ist, können Algorithmen verwendet werden, die bessere Ergebnisse als die derzeit verwendeten zentralen Differenzen liefern, wodurch die Qualität der Beleuchtung weiter erhöht wird. Durch die Flexibilität moderner Shader lassen sich noch weitere Effekte realisieren oder optimieren. Viele der vorgestellten Techniken liessen sich ohne Shader nicht, oder nur wesentlich ineffizienter realisieren. Anhang A Gallerie A.1 Sinterkupfer Dieses Bild wurde für die Frameratenmessung verwendet. Die Dimensionen betragen 330 x 328 x 222 Pixel. Das Volumen wurde über die Transferfunktion rot eingefärbt. Die Lichtquelle befindet sich vorne oben links. Die Oberflächenstruktur ist unter der Beleuchtung deutlich zu erkennen und das Objekt wirkt durch die Selbstschattierung wesentlich plastischer als ohne Beleuchtung. 87 ANHANG A. GALLERIE 88 Abbildung A.1: Sinterkupfer Oben: Unbeleuchtete Darstellung Unten: Von vorne oben links mit gelber Lichtquelle beleuchtet ANHANG A. GALLERIE A.2 89 Schädel Dieses Bild wurde für die Frameratenmessung verwendet. Die Dimensionen betragen 256 x 256 x 256 Pixel. Die Lichtquelle befindet sich vorne oben links. Besonders die Zähne wirken durch das Glanzlicht glatter als in der unbeleuchteten Darstellung. ANHANG A. GALLERIE 90 Abbildung A.2: Schädel Oben: Unbeleuchtete Darstellung Unten: Von vorne oben links mit blaugrüner Lichtquelle beleuchtet ANHANG A. GALLERIE A.3 91 Feuerbeton Ein kleinerer Ausschnitt von diesem Bild wurde für die Frameratenmessung verwendet. Die Dimensionen betragen 340 x 360 x 350 Pixel. In diesem Beispiel wurden die Korundeinschlüsse visualisiert. Das Volumen wurde über die Transferfunktion rot eingefärbt. Die Lichtquelle befindet sich vorne oben links. Dieses Beispiel verdeutlicht sehr gut die wesentlich verbesserte Darstellung der Oberflächenstruktur. Die Selbstschattierung hilft bei der Erkennung der einzelnen kugelförmigen Einschlüsse. ANHANG A. GALLERIE 92 Abbildung A.3: Korundeinschlüsse im Feuerbeton Oben: Unbeleuchtete Darstellung Unten: Von vorne oben links mit gelber Lichtquelle beleuchtet ANHANG A. GALLERIE A.4 93 Motorblock Dieses Bild zeigt den Teil eines Motorblocks. Die Dimensionen betragen 256 x 256 x 256 Pixel. Die Lichtquelle befindet sich rechts oben. An diesem Beispiel lässt sich die Selbstschattierung sehr gut erkennen. Auch die glatte Oberfläche wird durch das Glanzlicht gut veranschaulicht. ANHANG A. GALLERIE 94 Abbildung A.4: Motorblock Oben: Unbeleuchtete Darstellung Unten: Von rechts oben mit blaßgelber Lichtquelle beleuchtet ANHANG A. GALLERIE A.5 95 Menschlicher Kopf Dieses Bild zeigt einen menschlichen Kopf. Die Dimensionen betragen 256 x 256 x 256 Pixel. Die Lichtquelle befindet sich vorne oben links. Durch die Selbstschattierung wirkt das beleuchtete Bild wieder wesentlich plastischer. ANHANG A. GALLERIE 96 Abbildung A.5: menschlicher Kopf Oben: Unbeleuchtete Darstellung Unten: Von vorne oben links mit hellgrauer Lichtquelle beleuchtet ANHANG A. GALLERIE A.6 97 Aluminiumschaum Dieses Beispiel von der Darstellung eines Aluminiumschaums soll die Auswirkungen durch das Fehlen der Selbstschattierung verdeutlichen. Die Dimensionen betragen 400 x 400 x 400 Pixel. Die Lichtquelle befindet sich vorne oben. Das Objekt wurde über die Transferfunktion rot eingefärbt und wird mit einer gelben Lichtquelle beleuchtet. Das erste Bild wurde unter völligem Fehlen der Selbstschattierung gerendert, das heisst jeder Pixel ist beleuchtet. Dadurch fällt es schwer eine Beziehung zwischen den filigranen Strukturen herzustellen. Im zweiten Bild fällt dies aufgrund der Selbstschattierung wesentlich leichter. Besonders die große Aushöhlung am linken Rand ist ohne Schatten nur äusserst schwer wahrzunehmen. ANHANG A. GALLERIE 98 Abbildung A.6: Aluminiumschaum Oben: Darstellung ohne Selbstschattierung. Eine gelbe Lichtquelle befindet sich vorne oben. Unten: Die gleiche Darstellung wie oben mit Selbstschattierung Abbildungsverzeichnis 1.1 Shaderbeispiele . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2.1 2.2 2.3 7 8 2.6 2.7 2.8 2.9 2.10 2.11 2.12 2.13 Licht-Materie Interaktion allgemein . . . . . . . . . . . . . . Differentieller Raumwinkel . . . . . . . . . . . . . . . . . . . Der differentielle Raumwinkel dω als Fläche auf der Einheitskugel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Projektion des Raumwinkels dω auf die relevante Fläche da . Das gesamte einfallende Licht bestimmt die Intensität in Reflexionsrichtung ωr . . . . . . . . . . . . . . . . . . . . . . . Diffuse Reflexion . . . . . . . . . . . . . . . . . . . . . . . . Spiegelnde Reflexion . . . . . . . . . . . . . . . . . . . . . . Flat Shading . . . . . . . . . . . . . . . . . . . . . . . . . . . Gouraud Shading . . . . . . . . . . . . . . . . . . . . . . . . Phong Shading . . . . . . . . . . . . . . . . . . . . . . . . . Phong Komponenten . . . . . . . . . . . . . . . . . . . . . . Wirkung von Schatten . . . . . . . . . . . . . . . . . . . . . Schatten und Lichtquelle . . . . . . . . . . . . . . . . . . . . 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 Schematischer Aufbau der Grafikpipeline . . . . . . Schematische Darstellung eines Vertexprozessors . . Schematische Darstellung eines Fragmentprozessors Polygonbasiertes Rendering . . . . . . . . . . . . . Texturbasiertes Volumenrendering . . . . . . . . . . Prinzip des Shadowmapping . . . . . . . . . . . . . Transformation beim Shadowmapping . . . . . . . . Probleme beim Shadowmapping . . . . . . . . . . . Semitransparente Darstellung eines Motorblocks . . Rendern eines niedrig aufgelösten Datensatzes . . . Progressives Rendering . . . . . . . . . . . . . . . . 2.4 2.5 99 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 . 10 . . . . . . . . . 11 14 15 17 17 18 21 21 23 . . . . . . . . . . . 27 29 31 34 35 41 42 43 46 48 49 ABBILDUNGSVERZEICHNIS 4.1 4.2 4.3 4.4 4.5 4.6 4.7 100 Statisches UML-Klassendiagramm der Kernklassen des Moduls Voxel-Sculpture . . . . . . . . . . . . . . . . . . . . . . . Statisches UML-Klassendiagramm der beteiligten Shaderklassen Statisches UML-Klassendiagramm der neuen Renderklassen. Die alten Klassen grau dargestellt. . . . . . . . . . . . . . . . . Resultate bei der pre- und post-classification . . . . . . . . . . Artefakte bei der post-classification . . . . . . . . . . . . . . . Abstand der Lichtquelle beim Shadowmapping . . . . . . . . . Statisches UML-Klassendiagramm des Moduls Voxel-Sculpture, erweitert um progressives Rendering . . . . . . . . . . . . . . . 51 53 56 58 59 60 70 5.1 Ergebnisse der Frameratenmessung für die Beleuchtungsklasse 5.2 Ergebnisse der Frameratenmessung für die Beleuchtungsklasse mit progressivem Rendering . . . . . . . . . . . . . . . . . . . 5.3 Grafische Darstellung der Messergebnisse der Isoflächen für Rechner A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4 Grafische Darstellung der Messergebnisse der Isoflächen für Rechner B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 A.1 A.2 A.3 A.4 A.5 A.6 88 90 92 94 96 98 Sinterkupfer . . . . . Schädel . . . . . . . Korundeinschlüsse im Motorblock . . . . . menschlicher Kopf . Aluminiumschaum . . . . . . . . . . . . . . . Feuerbeton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 82 83 Quelltextverzeichnis 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 Bestimmung ob mit Beleuchtung gerendert wird. . . . . . . . . Erzeugen der Shadowmap . . . . . . . . . . . . . . . . . . . . Vertexshader für das Shadowmapping . . . . . . . . . . . . . . Bestimmung ob das Fragment beleuchtet ist . . . . . . . . . . Bestimmung der Normalen im Fragmentshader . . . . . . . . . Anwendung der Beleuchtungsgleichung im Fragmentshader . . Pseudocode des Fragmentshaders zur Darstellung von Isoflächen Generierter Shadercode für die Darstellung von zwei Isoflächen Eine progressive Renderfolge . . . . . . . . . . . . . . . . . . . Progressives Rendern eines Frames (Teil 1) . . . . . . . . . . . Progressives Rendern eines Frames (Teil 2) . . . . . . . . . . . 101 61 62 63 64 65 65 67 68 72 74 75 Literaturverzeichnis [AMH02] Akenine-Möller, Tomas ; Haines, Eric: Real-time Rendering. 2nd edition. A K Peters, Ltd., 2002 [ATI] ATI, Grafikkartenhersteller und Mitglied im ARB. http://www. ati.com [Bau00] Bauer, Michael. Optimierung der Volumenvisualisierung auf PCHardware. 2000 [CG05] Cg Toolkit User’s Manual 1.4. ftp://download.nvidia.com/ developer/cg/Cg 1.4/Docs/CG UserManual 1-4.pdf. 2005 [Chr01] Christof, Rezk-Salama: Volume Rendering Techniques for General Purpose Graphics Hardware, Technische Fakultät der Universität Erlangen-Nürnberg, Doktorarbeit, 2001 [Cro77] Crow, Franklin: Shadow algorithms for computer graphics. (1977). – Zitiert nach: [WW92] [GL] OpenGL, Industriestandard für Crossplattform 2D/3D Computergrafik. http://www.opengl.org [Hir05] Hirschenberger, Falco: Schnelle Volumenvisualisierung großer dreidimensionaler Bilddaten unter Verwendung von OpenGL, FHOOW, Diplomarbeit, 2005 [HLS] The High Level Shading Language. http://msdn.microsoft. com/library/default.asp?url=/library/en-us/directx9 c/ dx9 graphics reference hlsl.asp [KBR] Kessenich, John ; Baldwin, Dave ; Rost, Randi. The OpenGL Shading Language. http://oss.sgi.com/projects/ogl-sample/ registry/ARB/GLSLangSpec.Full.1.10.59.pdf 102 LITERATURVERZEICHNIS 103 [LC87] Lorensen, W. ; Cline, H.: Marching Cubes: A High Resolution 3D Surface Construction Algorithm. In: Computer Graphics (1987), S. 163–169 [Len02] Lengyel, Eric: Mathematics for 3D Game Programming & Computer Graphics. 1st edition. Charles River Media, 2002 [LV] Lokovic, Tom ; Veach, Eric: Deep Shadow Maps. [MAV] MAVI - Modular Algorithms for Volume Images. http://www.itwm. fhg.de/mab/projects/MAVI [MDX] DirectX, Microsofts multimedia API für hardwarebeschleunigte Computergrafik. http://www.microsoft.com/windows/directx [MS] Microsoft. http://www.microsoft.com [NV] NVidia, Grafikkartenhersteller und Mitglied im ARB. http://www. nvidia.com [Pho75] Phong, B.T.: Illumination for Computer Generated Pictures. In: Communications of the ACM 18 (1975) [Pix] Pixar. The RenderMan Interface Specification. renderman.pixar.com/products/rispec [Tro] Trolltech. Qt, das Multiplattform C++ GUI/API Toolkit. http: //www.trolltech.com https:// [Wil78] Williams, Lance: Casting Curved Shadows on curved Surfaces. (1978) [Win02] Winter, Andrew S.: Volume Graphics: Field-based Modelling and Rendering, University of Wales, Swansea, Ph.D. Dissertation, 2002 [WW92] Watt, Alan ; Watt, Mark: Advanced Animation and Rendering Techniques, Theory and Practice. Addison-Wesley, 1992 [Wyn] Wynn, Chris. An Introduction to BRDF-Based Lighting. http://developer.nvidia.com/attach/6568 [YCK] Yagel, Roni ; Cohen, Daniel ; Kaufman, Arie: Normal Estimation in 3D Discrete Space.