Echtzeit-Raytracing algebraischer Flächen auf der
Transcription
Echtzeit-Raytracing algebraischer Flächen auf der
Diplomarbeit Echtzeit-Raytracing algebraischer Flächen auf der Graphics Processing Unit Christian Stussak 12. Juli 2007 betreut durch Dr. rer. nat. habil. Peter Schenzel Martin-Luther-Universität Halle-Wittenberg Mathematisch-naturwissenschaftliche Fakultät Institut für Informatik Erklärung Ich versichere hiermit, dass ich diese Diplomarbeit ohne fremde Hilfe eigenständig verfasst und nur die angegebenen Quellen und Hilfsmittel benutzt habe. Wörtlich oder dem Sinn nach aus anderen Werken entnommene Stellen sind unter Angabe der Quellen kenntlich gemacht. Datum Unterschrift iii iv Inhaltsverzeichnis 1 Einleitung 1.1 Zielstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Aufbau der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Wertung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Grundlagen der GPU-Programmierung 2.1 Grafikkartenarchitektur und Programmiermodelle 2.1.1 Die Grafikpipeline . . . . . . . . . . . . . 2.1.2 Programmierbare Pipelinestufen . . . . . 2.1.3 Datenparallele Verarbeitung . . . . . . . . 2.1.4 Stream-Processing . . . . . . . . . . . . . 2.2 Programmiersprachen . . . . . . . . . . . . . . . 2.2.1 Shading-Languages . . . . . . . . . . . . . 2.2.2 Stream-Processing-Languages . . . . . . . 2.3 Einführung in die OpenGL-Shading-Language . 2.3.1 OpenGL-API für GLSL . . . . . . . . . 2.3.2 GLSL-Sprachelemente . . . . . . . . . . . 2.3.3 GLSL-Beispiel . . . . . . . . . . . . . . . 2.4 Werkzeuge und Bibliotheken . . . . . . . . . . . . 2.5 Zusammenfassung . . . . . . . . . . . . . . . . . 1 2 2 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 4 6 7 9 9 10 11 12 12 15 17 19 19 3 Grundlagen des GPU-basierten Raytracings 3.1 Der Raytracing-Algorithmus . . . . . . . . . . . . . . . . 3.1.1 Beschleunigung des Raytracings . . . . . . . . . . 3.2 Anpassung des Algorithmus für die GPU . . . . . . . . . 3.2.1 Aufgaben des Vertex-Shaders . . . . . . . . . . . 3.2.2 Aufgaben des Fragment-Shaders . . . . . . . . . 3.2.3 Koordinatensystem der Beleuchtungsberechnung 3.2.4 Alternative Ansätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 21 22 22 23 24 24 25 4 Raycasting algebraischer Flächen 4.1 Schnittpunktberechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Clipping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Flächennormale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 28 28 29 5 Berechnung der Polynomkoeffizienten 5.1 Termumformungen auf der CPU . 5.2 Termumformungen auf der GPU . 5.2.1 Polynomoperationen . . . . 5.3 Polynominterpolation . . . . . . . . 31 31 32 33 34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v 5.4 5.3.1 Lagrange-Interpolation . . . . . 5.3.2 Newton-Interpolation . . . . . . 5.3.3 Weitere Interpolationsverfahren Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Berechnung der Nullstellen univariater Polynome 6.1 Vorbemerkungen . . . . . . . . . . . . . . . . . 6.1.1 Anzahl und Vielfachheit von Nullstellen 6.1.2 Das Horner-Schema . . . . . . . . . . . 6.1.3 Polynomdivision . . . . . . . . . . . . . 6.2 Lösungsformeln . . . . . . . . . . . . . . . . . . 6.2.1 Lineare Gleichungen . . . . . . . . . . . 6.2.2 Quadratische Gleichungen . . . . . . . . 6.2.3 Kubische Gleichungen . . . . . . . . . . 6.2.4 Biquadratische Gleichungen . . . . . . . 6.3 Lokal konvergente Iterationsverfahren . . . . . . 6.3.1 Newton-Iteration . . . . . . . . . . . . . 6.3.2 Einschlussverfahren . . . . . . . . . . . . 6.4 Nullstellenisolation . . . . . . . . . . . . . . . . 6.4.1 Das D-Chain-Verfahren . . . . . . . . . 6.4.2 Der Algorithmus von Sturm . . . . . . . 6.4.3 Wertebereichanalyse . . . . . . . . . . . 6.5 Global konvergente Iterationsverfahren . . . . . 6.5.1 Die Muller-Methode . . . . . . . . . . . 6.5.2 Die Laguerre-Methode . . . . . . . . . . 6.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 36 38 38 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 41 42 44 44 44 45 45 46 47 47 48 50 50 51 52 54 55 56 57 7 Implementierung 7.1 Umsetzung des Raycasting-Algorithmus in GLSL . . . . . . . . 7.1.1 Verwendete Grafikhardware und Grafiktreiber . . . . . . 7.1.2 Modifikation der Raycasting-Algorithmen für GLSL . . 7.2 Erzeugung der Shader aus den Formeln algebraischer Flächen . 7.3 Grafische Benutzeroberfläche . . . . . . . . . . . . . . . . . . . 7.3.1 Überführung einer Flächenformel in ein gerendertes Bild 7.3.2 Transformationen und Darstellungsparameter . . . . . . 7.3.3 Flächenparameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 59 59 60 63 65 65 67 67 8 Ergebnisse 8.1 Optischer Vergleich der implementierten Verfahren . . . 8.1.1 Berechnung der Polynomkoeffizienten . . . . . . . 8.1.2 Berechnung der Polynomnullstellen . . . . . . . . 8.2 Skalierbarkeit der Raycasting-Algorithmen . . . . . . . . 8.3 Laufzeitvergleich der implementierten Verfahren . . . . . 8.3.1 Berechnung der Polynomkoeffizienten . . . . . . . 8.3.2 Berechnung der Polynomnullstellen . . . . . . . . 8.4 Abschließende Bewertung der implementierten Verfahren 8.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 69 69 70 74 74 75 75 76 77 vi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.6 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 A Renderings und Flächenbeschreibungen A.1 Zum Laufzeitvergleich genutzte algebraische Flächen . . . . . . . . . . . . . . A.2 Weitere bekannte algebraische Flächen . . . . . . . . . . . . . . . . . . . . . . 79 79 81 B Grammatik der Formeln algebraischer Flächen B.1 Grammatik für Flex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.2 Grammatik für Bison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 83 84 C Inhalt der DVD 85 Literaturverzeichnis 87 Abbildungsverzeichnis 93 Tabellenverzeichnis 95 vii viii 1 Einleitung In vielen wissenschaftlichen Bereichen ist die grafische Veranschaulichung seit jeher ein unerlässliches Hilfsmittel zum Verständnis komplizierter Strukturen und Zusammenhänge. Betrachtet man zum Beispiel eine Gleichung in drei reellen Variablen, so bilden die Lösungen der Gleichung eine Teilmenge des dreidimensionalen Raumes R3 . Die zur Lösung gehörenden Punkte setzen sich dabei häufig zu Flächen zusammen, die meist nur schwer vorstellbar sind. Einfache Flächen, wie Kugeln, Ellipsoide oder Paraboloide, sind in vielen mathematischen Modellsammlungen in Form von Draht- oder Gipsmodellen zu finden. Eine derartige Modellierung stößt jedoch für komplexere Flächen schnell an ihre Grenzen, so dass deren genaue Gestalt oftmals nur durch Verfahren der Computergrafik ermittelt werden kann. Ein wichtiges Teilgebiet in diesem Kontext ist die Visualisierung der Lösungsmengen algebraischer Gleichungen in drei Variablen, so genannter algebraischer Flächen. Derartige Flächen treten beispielsweise im Zusammenhang mit partiellen Differentialgleichungen auf, welche zur mathematischen Modellierung physikalischer und chemischer Vorgänge, sowie in vielen Ingenieurdisziplinen eingesetzt werden. Die Klassifizierung der Lösungsmengen algebraischer Gleichungen entsprechend ihrer geometrischen Eigenschaften ist daher ein wichtiges Forschungsgebiet der Mathematik. Eine geeignete Visualisierung kann in vielen Fällen die Klärung der geometrischen Eigenschaften wesentlich unterstützen. Auch die Verwendung algebraischer Flächen im Bereich des Computer Aided Design ist Gegenstand wissenschaftlicher Forschungen. Trotz der kompakten Darstellungsweise in Form von algebraischen Gleichungen ist deren Visualisierung eine rechenintensive Aufgabe. Ein häufig verfolgter Ansatz ist die Polygonalisierung der Fläche mit Hilfe des Marching-Cubes-Algorithmus. Das damit erstellte Gitternetz kann zwar performant gerendert werden, erlaubt aber in der Regel keine interaktive Veränderung von Flächenparametern. Eine andere Herangehensweise besteht im so genannten Raytracing- beziehungsweise Raycasting-Verfahren. Dabei wird ausgehend von einer virtuellen Kamera der Verlauf von Strahlen durch eine 3D-Szene untersucht. Die notwendige Berechnung der Schnittpunkte eines solchen Strahls mit einer algebraischen Fläche ist mit den üblicherweise eingesetzten Algorithmen sehr rechenaufwendig. Raytracer, wie POV-Ray [pov] oder Surf [ESSB], sind daher auch unter Verwendung aktueller Prozessoren kaum in der Lage die anfallenden Berechnungen in Echtzeit durchzuführen. Durch den rasanten Anstieg der Geschwindigkeit von Grafikhardware, verbunden mit einer ständigen Erweiterung ihrer Programmierbarkeit, entwickelte sich die Graphics Processing Unit (GPU) moderner Grafikkarten zu einer erfolgversprechenden Implementierungsplattform rechenintensiver Algorithmen. Aufgrund der Parallelrechnerarchitektur von Grafikprozessoren ist gewöhnlich eine Überarbeitung der benötigten Algorithmen erforderlich. Die neuen Möglichkeiten, aber auch die Grenzen der GPU-Programmierung müssen erörtert und analysiert werden, um eine erfolgreiche Implementierung des GPU-beschleunigten Raycastings algebraischer Flächen zu ermöglichen. 1 1 Einleitung 1.1 Zielstellung Im Rahmen dieser Diplomarbeit soll die Verwendbarkeit moderner Grafikhardware für die Visualisierung implizit gegebener, algebraischer Flächen untersucht werden. Es soll ermittelt werden, wie das Raytracing-Verfahren und insbesondere die Schnittpunktberechnungen zwischen Raytracing-Strahl und algebraischer Fläche auf Grafikprozessoren umgesetzt werden können, welche Algorithmen sich wie gut dafür eignen und welche Probleme dabei auftreten. Die Bildsynthese sollte, soweit dies möglich ist, in Echtzeit geschehen, um ein interaktives Betrachten und Verändern der Fläche zu ermöglichen. 1.2 Aufbau der Arbeit Zur Einführung in die Thematik wird im zweiten Kapitel dieser Arbeit auf die Grundlagen der GPU-Programmierung eingegangen. Es werden verschiedene Programmiermodelle und Programmiersprachen vorgestellt und die für die Implementierung genutzte OpenGL-ShadingLanguage genauer betrachtet. Das dritte Kapitel beschreibt die Grundlagen des Raytracings beziehungsweise Raycastings und deren Umsetzung auf die Grafikhardware. Im vierten Kapitel wird das Raycasting algebraischer Flächen näher untersucht. Beim Raycasting müssen die Koeffizienten gewisser univariater Polynome, sowie die Nullstellen dieser Polynome bestimmt werden. Mit den dafür verwendbaren Algorithmen beschäftigen sich die Kapitel fünf und sechs. Kapitel sieben geht auf Details und Schwierigkeiten der Implementierung der Raycasting-Algorithmen in der OpenGL-Shading-Language ein. Darauf folgt in Kapitel acht eine Bewertung der umgesetzten Verfahren bezüglich Bildqualität und Laufzeit. 1.3 Wertung Der verfolgte Ansatz des Raycastings eignet sich gut zur Visualisierung algebraischer Flächen. Der hohe Aufwand des Verfahrens kann durch leistungsfähige Rechner und Parallelisierung kompensiert werden. Durch die Verwendung moderner Grafikprozessoren kann das Raycasting algebraischer Flächen so beschleunigt werden, dass auch interaktive Anwendungen möglich sind. Mit steigender Flächenkomplexität wächst auch der Bedarf an Ressourcen, die im Grafikprozessor nur in geringem Umfang zur Verfügung stehen. Die begrenzte Codegröße und Laufzeit der Shader-Programme verhindert daher oftmals die Visualisierung komplexer algebraischer Flächen vom Grad größer als zehn. Trotz der erfolgreichen Umsetzung des Raycasting-Algorithmus stellt die Implementierung eine Herausforderung dar. Viele der untersuchten Algorithmen erfordern eine aufwendige Anpassung an das Programmiermodell der Grafikhardware. Zudem ruft die für dieses Anwendungsgebiet geringe Genauigkeit von Fließkommazahlen nicht unerhebliche numerische Probleme hervor. 2 2 Grundlagen der GPU-Programmierung Die Entwicklung von Grafikhardware wurde in den neunziger Jahren maßgeblich durch die steigenden Anforderungen von Computerspielen beeinflusst. Detailgetreue 3D-Szenen verlangten nach höheren Taktraten und zusätzlichen Funktionen der Grafikbibliotheken, die der Hardware mit jeder neuen Generation hinzugefügt wurden. Spieleentwickler konnten lange Zeit nur die fest eingebaute Funktionalität der Grafikkarten verwenden, um möglichst realistisch wirkende Bilder zu erzeugen. Die Verwendung neuartiger Effekte in 3D-Anwendungen war daher maßgeblich an die Einführung neuer Grafikkarten gebunden. Als Microsoft im Jahr 2001 seine Multimediaschnittstelle DirectX in der Version 8 verabschiedete, änderte sich dies schlagartig. Grafikkarten, die diese Schnittstelle implementierten, waren in der Lage kleine Assemblerprogramme zu verarbeiten, mit denen sich die Transformation von Punkten und Vektoren beziehungsweise die Farbe eines Pixels berechnen ließ. Da DirectX schon zum damaligen Zeitpunkt in der Spieleproduktion etabliert war, verbreiteten sich derartige Grafikkarten schnell auf dem Massenmarkt. Umfang und Komplexität der Programme waren jedoch sehr beschränkt und so forderten Spieleprogrammierer schnell verbesserte Programmiermodelle und -sprachen. [Sch] Die enorme Geschwindigkeit, mit der die Grafikprozessoren ihre neuen Aufgaben erledigten, weckte schon bald Interesse in anderen Bereichen von Wirtschaft und Forschung. Man erkannte, dass deren Anwendungsgebiete nicht länger auf die Darstellung von 3D-Szenen beschränkt waren. Schnell eröffnete sich das so genannte GPGPU-Feld (General-Purpose computation on GPUs), welches die GPU als Koprozessor für die verschiedensten rechenintensiven Aufgaben verwendet. Derzeit profitieren vorwiegend Anwendungen in der Echtzeitvisualisierung und -simulation von diesem Trend (Abbildung 2.1). Doch die ständige Verbesserung der Programmiermodelle und -möglichkeiten bereitet den Weg für eine Reihe anderer Einsatzgebiete. Grafikkarten mit Unterstützung für DirectX 10 werden beispielsweise zahlreiche Ganzzahlund Bitoperationen erlauben und damit auch für Kryptographiesysteme interessant [NVi06b]. (a) (b) (c) (d) Abbildung 2.1: Beispielanwendungen, welche von der Verwendung der GPU als Koprozessor profitieren: (a) und (b) Beschleunigung von globalen Beleuchtungsverfahren wie Radiosity [CHL04], Raytracing und Photon-Mapping [PDC+ 03]. (c) Physikalische Simulation von Flüssigkeiten, Rauch und Feuer [Cra]. (d) Interaktive Visualisierung und Segmentierung von MRT-Datensätzen [LKHW05]. 3 2 Grundlagen der GPU-Programmierung GFLOPS 300 200 NVidia GPUs ATI GPUs Intel CPUs 100 0 2002 2004 2006 Jahr Abbildung 2.2: Die Geschwindigkeit von Fließkommaoperationen auf GPUs erhöhte sich in den letzten vier Jahren im Vergleich zur CPU drastisch. Die Daten wurden aus [OLG07] entnommen. Heute übertrifft die Leistung einer aktuellen Grafikkarte die von Desktop-Prozessoren bei Weitem (Abbildung 2.2). Hinzu kommt, dass Grafikkarten meist deutlich günstiger zu erwerben sind als vergleichbare CPUs. Zum Beispiel kostete ein 3.0 GHz Intel Core2 Duo (Woodcrest Xeon 5160) im November 2006 circa $874 und besitzt dabei eine theoretische Spitzenleistung von 48 GFLOPS und eine Speicherbandbreite von 21 GB/s. Eine NVidia Geforce 8800 GTX war bereits für $599 zu haben. Mit einer empirisch bestimmten Verarbeitungsgeschwindigkeit von 330 GFLOPS und einer Speicherbandbreite von 55.2 GB/s ist sie der CPU deutlich überlegen. Hierbei darf natürlich nicht verschwiegen werden, dass GPUs ihre volle Leistungsfähigkeit architekturbedingt nur bei datenparallelen Prozessen entfalten können. [Lue] In den folgenden Abschnitten soll genauer auf die Architektur und das Programmiermodell von programmierbarer Grafikhardware eingegangen werden. Verschiedene GPU-Programmiersprachen werden vorgestellt und es wird eine Einführung in die für diese Arbeit verwendete Programmiersprache vorgenommen. 2.1 Grafikkartenarchitektur und Programmiermodelle Die Domäne interaktiver 3D-Grafik besitzt einige Charakteristika, die sich stark von anderen, weniger spezifischen Anwendungsbereichen abgrenzen. Beispielsweise benötigen interaktive 3D-Anwendungen sehr hohe Verarbeitungsgeschwindigkeiten der zugrunde liegenden Hardware, erlauben aber auch ein hohes Maß an Parallelisierung. Die Teilaufgaben beim interaktiven 3D-Rendering werden daher traditionell in Pipelines organisiert. Der Aufbau der Grafikpipeline bestimmt hierbei maßgeblich die verschiedenen Programmiermodelle. 2.1.1 Die Grafikpipeline Die Grafikpipeline besteht aus sehr vielen verschiedenen Stufen, die sich in Anzahl und Aufgabe von Hersteller zu Hersteller unterscheiden. Abbildung 2.3 zeigt eine vereinfachte Grafikpipeline, welche die wichtigsten Verarbeitungsblöcke enthält. Da die Funktionalität der einzelnen Stufen immer gleich ist, bezeichnet man diese auch als Fixed-Function-Pipeline. 4 2.1 Grafikkartenarchitektur und Programmiermodelle CPU Application GPU Per Vertex Operations Primitive Assembly Rasterizer Per Fragment Operations Video Memory (Textures, Framebuffer, etc.) Abbildung 2.3: Vereinfachter Aufbau einer Fixed-Function-Grafikpipeline. Die gebräuchlichen englischen Stufenbezeichnungen wurden übernommen. Pfeile in der Abbildung kennzeichnen Datenflüsse. Quelle: [Lue]. Die Stufe Application sendet die Eckpunkte (Vertices) grafischer Primitive, also Punkte, Linien oder Polygone, mit Zusatzattributen wie Normalen, Texturkoordinaten und Materialinformationen, von der CPU oder vom Hauptspeicher an die GPU. Die Stufe Per Vertex Operations transformiert jeweils einen Vertex in das 2D-Koordinatensystem des Bildraumes. Je nach verwendeter Grafik-API können die Eckpunkte bereits hier mit Hilfe der Normalen- und Materialinformationen beleuchtet werden. Im Primitive Assembly werden die Punkte wieder zum ursprünglichen Primitiv zusammengesetzt und geclippt1 , wodurch Primitive verschwinden und entstehen können. Für die durch Clipping neu entstandenen Eckpunkte werden alle benötigten Attribute aus den Nachbarecken interpoliert. Der Rasterizer konvertiert nun die Geometriedaten in so genannte Fragmente, also Bildpunkte mit Eigenschaften wie Normale, Texturkoordinate und Tiefenwert, welche wiederum aus den Attributen der Eckpunkte interpoliert werden. In der Stufe Per Fragment Operations wird jeweils ein einzelnes Fragment verarbeitet. Dieses wird texturiert und gegebenenfalls noch beleuchtet. Das fertige Fragment wird nun in den Framebuffer im Video Memory geschrieben. Dabei hängt der letztendliche Farbwert des Pixels von weiteren Schritten wie Alpha-Blending, Tiefen- und Stenciltest ab. Der Zugriff auf den Video Memory unterliegt hierbei gewissen Regeln. Die Stufe Application ist je nach verwendeter 3D-API in der Lage Speicherbereiche wie Texturen und Framebuffer zwischen Grafikspeicher und Hauptspeicher zu transferieren. Dabei werden bei Bedarf automatische Konvertierungen zwischen den Datentypen der GPU und der 3D-API vorgenommen. Der Datentransfer geschieht im Allgemeinen relativ langsam und sollte daher so wenig wie möglich genutzt werden. Die Stufe Per Fragment Operations ist zwar in der Lage Texturdaten zu lesen, kann aber immer nur die Daten, die zum gerade verarbeiteten Pixel gehören, in den Framebuffer oder in eine Textur schreiben. Da in der Regel mehrere Fragmente gleichzeitig verarbeitet werden, sind die Resultate häufig undefiniert, wenn man für Lese- und Schreiboperationen dieselbe Textur benutzt. In der geschilderten Darstellung der Grafikpipeline wird nicht betrachtet, wie der Datenaustausch zwischen den einzelnen Stufen erfolgt. Einige Stufen müssen darüber hinaus aufwendigere Berechnungen durchführen als andere und es ist nicht davon auszugehen, dass alle Stufen gleich ausgelastet sind. Rendert man beispielsweise ein bildschirmfüllendes Rechteck, 1 Unter Clipping versteht man das Entfernen nicht sichtbarer oder nicht benötigter Teile grafischer Primitive. Bei der Verarbeitung von Dreiecken kann durch das Abschneiden einer Ecke ein Viereck entstehen, welches anschließend wieder in zwei Dreiecke zerlegt wird. 5 2 Grundlagen der GPU-Programmierung so müssen lediglich vier Punkte transformiert und zwei Dreiecke geclippt werden. Zu jedem Dreieck gehören aber sehr viele Fragmente, wodurch die hinteren Stufen zu einem Engpass werden. Betrachtet man sehr fein unterteilte Gitternetze, deren einzelne Dreiecke sich auf die Größe eines Pixels reduzieren, so entsteht der Engpass bei der Verarbeitung der Vertices. Aus diesem Grund werden in Grafikkarten meist mehrere Pipelines implementiert, die zu einer gewissen Lastverteilung untereinander in der Lage sind. Die idealisierte Pipeline soll jedoch weiterhin als theoretisches Modell dienen. 2.1.2 Programmierbare Pipelinestufen Das grundlegende Modell der Grafikpipeline hat sich bis zur Einführung programmierbarer Grafikkarten kaum verändert. Gelegentlich wurden die 3D-APIs um neue Funktionen wie beispielsweise Bump Mapping erweitert. Anfangs wurden die benötigten Algorithmen in Hardware umgesetzt, doch die steigende Anzahl an Erweiterungen machte die Hardwareumsetzung zunehmend kompliziert. Da die Neuerungen sich meist auf die Verarbeitung einzelner Vertices oder Fragmente beschränkten, begannen die GPU-Hersteller, ihre Prozessoren flexibler zu gestalten. Die betreffenden Teile der Grafikpipeline wurden programmierbar und so konnten neue Funktionen oft schon durch Aufspielen einer neuen Firmware oder eines neuen Grafikkartentreibers realisiert werden. Die Freigabe der Programmierschnittstelle für die Entwicklergemeinde erlaubte eine Vielzahl neuer Grafikalgorithmen und Effekte, die rasch Einzug in Computerspiele hielten. Die Veränderungen der Grafikpipeline werden in Abbildung 2.4 dargestellt. Die Stufe Per Vertex Operations wurde zum so genannten Vertex Processor, die Stufe Per Fragment Operations zum Fragment Processor. Beide Stufen werden im folgenden Abschnitt erläutert. Vertex-Prozessor Der Vertex-Prozessor verarbeitet spezielle Programme, welche als Vertex-Shader, manchmal auch als Vertex-Programm, bezeichnet werden. Es dient im Allgemeinen dazu, Geometriedaten vom 3D-Weltkoordinatensystem in das 2D-Koordinatensystem der Bildebene zu transformieren. In der Fixed-Function-Pipeline werden immer alle Vertices eines Polygons der gleichen Transformation unterworfen. Dem Vertex-Shader wird aber jeder Vertex einzeln übergeben, CPU Application GPU Vertex Processor Primitive Assembly Rasterizer Fragment Processor Video Memory (Textures, Framebuffer, etc.) Abbildung 2.4: Vereinfachter Aufbau einer programmierbaren Grafikpipeline. Die gebräuchlichen englischen Stufenbezeichnungen wurden übernommen. Pfeile in der Abbildung kennzeichnen Datenflüsse. Quelle: [Lue]. 6 2.1 Grafikkartenarchitektur und Programmiermodelle welcher dann parameterabhängig verändert werden kann. So sind selbst aufwendige Deformationen und Animationen dreidimensionaler Objekte möglich. Als Eingabe für den Shader dienen die bereits erwähnten Geometriedaten. Zusätzlich kann auf Texturdaten aus dem gemeinsam genutzten Videospeicher und eine Vielzahl von Variablen und Konstanten zugegriffen werden. Die Ausgabe eines Vertex-Shaders ist mindestens ein transformierter Vertex. Zusätzlich können noch Variablen an den Fragment-Shader übergeben werden, die mit dem transformierten Vertex assoziiert werden. Wird das zugehörige Dreieck gerastert, so werden diese Variablen für jedes Fragment entsprechend interpoliert. Fragment-Prozessor Der Fragment-Prozessor verarbeitet so genannte Fragment-Shader oder Fragment-Programme. Da die Begriffe Pixel und Fragment oft synonym verwendet werden, existieren auch die Bezeichnungen Pixel-Shader und Pixel-Programm. Ein Fragment-Shader wird dazu genutzt die Farbe eines Pixels zu bestimmen. In einigen Shader-Sprachen kann zusätzlich zur Pixelfarbe auch noch ein Tiefenwert an die folgenden (in Abbildung 2.4 nicht eingezeichneten) Pipelinestufen übergeben werden. Für die Berechnungen können Texturen, die Variablen, die aus dem Vertex-Shader stammen, sowie einige globale Variablen und Konstanten verwendet werden. Die hier vorliegende Arbeit stützt sich auf die beschriebenen programmierbaren Stufen. Aufgrund der rasanten Entwicklung im Bereich der Computergrafik existieren bereits neue Architekturen mit zusätzlichen programmierbaren Prozessoren. DirectX 10 führt beispielsweise den Geometry Processor ein, welcher in der Lage ist, grafische Primitive auf der Grafikkarte selbst zu generieren. Das ungewöhnliche Programmiermodell und die schwierige Umsetzung gängiger Algorithmen für die GPU veranlassten mehrere Grafikkartenhersteller zur Entwicklung spezieller Frameworks, die die GPU als datenparallele, virtuelle Maschine abstrahieren [NVi06a, PSG06]. 2.1.3 Datenparallele Verarbeitung Die Leistungsfähigkeit moderner Grafikkarten beruht hauptsächlich auf Parallelverarbeitung unabhängiger Vertices oder Fragmente. Im Gegensatz zu den hoch getakteten, komplexen CPUs arbeitet in einer GPU eine Vielzahl einfacher Prozessoren mit verhältnismäßig geringem Takt. Bei der GPU-Konstruktion konzentriert man sich vorwiegend auf Vektoroperationen und Texturzugriffe, wobei der Hardware zur Realisierung von Programmverzweigungen meist weniger Bedeutung beigemessen wird. Daraus resultiert eine Klassifikation der GPUArchitektur ähnlich zum Klassifikationsschema von Flynn [OLG07, KGGK94, S. 16ff]. Ältere GPU-Implementierungen besitzen keinerlei Verzweigungshardware. Stattdessen werden beide Pfade der Verzweigung ausgewertet und die Ergebnisse eines Pfades abhängig von der Verzweigungsbedingung verworfen. Ähnlich wird bei Zählschleifen verfahren. Statt abhängig von der Schleifenbedingung immer wieder zum Schleifenanfang zu springen, wird der Code des Schleifenrumpfes entsprechend oft kopiert. Die Compiler von Shader-Hochsprachen sind meist in der Lage, den dafür benötigten Code automatisch zu generieren. WHILE-Schleifen stellen jedoch ein großes Problem dar, da nicht vorberechnet werden kann, wie oft die Schleife insgesamt durchlaufen wird. Daher limitieren Shader-Compiler häufig die maximale Anzahl an Durchläufen, um derartige Schleifen zumindest teilweise zu unterstützen. Wie von Flynn beschrieben, können in Parallelrechnerarchitekturen, die nach dem MultipleInstruction-Multiple-Data-Prinzip (MIMD) arbeiten, verschiedene Prozessoren auch verschie- 7 2 Grundlagen der GPU-Programmierung dene Befehle auf verschiedenen Daten durchführen und damit unterschiedlichen Verzweigungspfaden folgen. In Single-Instruction-Multiple-Data-Architekturen (SIMD) können dagegen die aktiven Prozessoren nur den gleichen Befehl auf verschiedenen Daten ausführen und sind damit auch nur bedingt für Verzweigungen geeignet. Derzeit weisen nur wenige Grafikkarten in der Vertex-Prozessor-Stufe eine echte MIMD-Architektur auf. Die Klassifizierung von FragmentProzessoren ist schwieriger. Laut [OLG07] verarbeiten GPUs Fragmente in SIMD-Gruppen. Wird für alle Fragmente innerhalb einer SIMD-Gruppe eine Verzweigungsbedingung gleich ausgewertet, so wird nur der zugehörige Verzweigungspfad durchlaufen. Unterscheiden sich die Werte, dann werden für alle Pixel beide Pfade ausgewertet und anschließend für jeden Pixel nur die benötigten Ergebnisse übernommen. Da die SIMD-Gruppen meist aus benachbarten Pixeln bestehen, können beim Entwurf von Fragment-Shadern eventuell die daraus resultierenden Kohärenzen berücksichtigt werden. Des Weiteren sind die einzelnen Shader-Einheiten für Operationen auf Quadrupeln, wie RGBA-Farbwerten oder Punkten beziehungsweise Vektoren in homogenen Koordinaten, optimiert. Abbildung 2.5 erläutert am Beispiel der NVidia GeForce 6 Serie, wie Berechnungen auf Tripeln, Paaren oder skalaren Werten organisiert sein können. GPUs verschiedener Hersteller unterscheiden sich erwartungsgemäß in solchen Details. Aus der Sicht des beschriebenen Programmiermodells haben die verschiedenen GPUs aber gemeinsam, dass weder die Vertex-Prozessoren noch die Fragment-Prozessoren untereinander kommunizieren können. Die Verarbeitung eines Vertex ist aus Softwaresicht vollkommen unabhängig von den anderen, gleichzeitig ablaufenden Vertex-Shadern. Für Fragmente gilt dies analog, so dass nach dem Auslösen einer Zeichenoperation von der Anwendung bis zum Speichern aller zugehörigen Pixel im Framebuffer keine weitere Möglichkeit zur Synchronisation besteht. Man kann also zusammenfassen, dass sich aktuelle GPU-Architekturen am besten in die SIMD-Kategorie einordnen lassen. Vertex- und Fragment-Prozessoren sind programmierbar. Ein Vertex-Shader kann zwar Variablen an den folgenden Fragment-Shader übergeben, aber es existiert keine Möglichkeit Informationen zwischen gleichartigen, gleichzeitig ablaufenden Shadern auszutauschen. Das beschriebene Programmiermodell ist ungewöhlich im Vergleich zur CPU, wodurch viele Algorithmen schwierig umzusetzen sind. Weitere Hürden bei der Programmierung werden in den folgenden Abschnitten beschrieben. 3:1 Operation 1 2:2 Operation 2 Operation 1 Operation 2 Abbildung 2.5: Eine Shader-Einheit der NVidia GeForce 6 kann pro Takt zwei unabhängige Operationen auf den Elementen eines Quadrupels ausführen. Dabei kann das Quadrupel entweder im Verhältnis 3:1 oder 2:2 aufgeteilt werden. Können bedingt durch Datenabhängigkeiten nicht gleichzeitig Operationen auf passenden Datentypen ausgeführt werden, so befinden sich die übrigen Berechnungseinheiten im Leerlauf. Quelle: [KF05]. 8 2.2 Programmiersprachen 2.1.4 Stream-Processing Aufgrund der besonderen Ausrichtung auf Grafikverarbeitung lassen sich viele Algorithmen aus anderen Bereichen der Informatik nur schwer an das beschriebene Programmiermodell anpassen. Berechnungen werden gestartet, indem der Grafik-Pipeline grafische Primitive übermittelt werden. Größere Datenmengen können nur in Texturen gespeichert und dann im Shader ausgelesen werden. Die Ergebnisse der Berechnung müssen in den Bereich des Framebuffers gelegt werden, den das Primitiv abdeckt. Die Ergebnisse aus dem Framebuffer sind aber erst in einem zweiten Renderingdurchlauf nutzbar, so dass die meisten Algorithmen nur durch Multipass-Verfahren2 realisiert werden können. Viele Algorithmen sind mit Hilfe des StreamProcessing-Modells leichter umzusetzen, welches kurz erläutert werden soll. Abschnitt 2.2.2 bietet anschließend einen Überblick über gängige GPU-Stream-Processing-Sprachen, die die Abbildung des allgemeinen Stream-Processings auf das GPU-Programmiermodell vornehmen. Beim Stream-Processing werden Datenabhängigkeiten und Kommunikationmuster explizit modelliert, indem Berechnungen durch so genannte Streams und Kernels ausgedrückt werden. Ein Stream ist eine geordnete Menge von Daten, wobei alle Elemente vom gleichen Datentyp sind. Ein Kernel ist ein Funktionsblock, dessen Ein- und Ausgabe Streams sind. Die Berechnung eines Elementes im Ausgabestream hängt dabei ausschließlich von der Eingabe ab und ist unabhängig von den Berechnungen anderer Ausgabeelemente. Ein Kernel besitzt also keinen inneren Zustand. Dank dieser Eigenschaften sind SIMD-Architekturen wie GPUs meist gut zum Stream-Processing geeigenet. Ein Kernel wird dabei als Fragment-Shader realisiert. Als Ein- und Ausgabeströme dienen Texturen. Eine Berechnung erfolgt, indem ein Rechteck in die Ausgabetextur gezeichnet wird, welches die Ergebnisse enthalten soll. Die Texturkoordinaten der Eckpunkte werden so gewählt, dass sie jedem Pixel die passenden Indizes zur Adressierung der Eingabeströme liefern. Da Texturen als Eingabeströme dienen, bieten die meisten Grafikkarten die Möglichkeit auch direkt in eine Textur zu rendern (Render-To-Texture). Dadurch wird der langsame Datentransfer zwischen Haupt- und Grafikspeicher vermieden, der sonst zur Verwendung des Framebuffers als Textur notwendig wäre. Häufig wird zusätzlich die Ping-Pong-Tecknik verwendet. Diese Technik arbeitet mit zwei Texturen. Im ersten Renderdurchgang enthält die erste Textur die Eingabedaten, während die zweite Textur für die Ausgabe genutzt wird. In jedem folgenden Renderingdurchgang werden die Rollen jeweils getauscht, da die Eingabedaten des vorherigen Durchgangs in der Regel nicht mehr benötigt werden. So entsteht ein effizienter, wenn auch umständlicher Weg zur Nutzung der GPU-Performance. Glücklicherweise werden diese Details größtenteils durch die Stream-Processing-Sprachen verborgen. Einen guten Überblick über weitere Methoden, die beim Stream-Processing auf GPUs zum Einsatz kommen, liefert beispielsweise [OLG07]. 2.2 Programmiersprachen Um die Möglichkeiten der GPUs auch nutzen zu können, müssen natürlich entsprechende Programmiersprachen vorhanden sein. Dieser Abschnitt geht auf die bekanntesten Hochsprachen und deren Unterschiede ein. Im Bereich des interaktiven 3D-Renderings werden meist Shading-Languages mit direkter Programmierung von Vertex- und Fragment-Shadern genutzt. 2 Multipass-Verfahren nutzen mehrere Renderingdurchläufe um ein bestimmtes Ergebnis zu berechnen. Dabei werden für einen Durchlauf jeweils die Teilergebnisse der bereits durchgeführten Renderings verwendet. 9 2 Grundlagen der GPU-Programmierung Im GPGPU-Bereich sind dagegen Stream-Processing-Sprachen gebräuchlich. Die zahlreich vorhandenen Assembler-Sprachen3 sind nicht mehr zeitgemäß und werden daher nicht näher betrachtet. Eine Einführung in die OpenGL-Shading-Language, die in dieser Arbeit Verwendung fand, wird separat in Abschnitt 2.3 gegeben. 2.2.1 Shading-Languages Ein Shading-Language-Programm besteht aus einem Vertex-Shader und einem zugehörigen Fragment-Shader. Es ist nicht möglich nur einen Shader zu verwenden und anstelle des zweiten Shaders auf Teile der Fixed-Function-Pipeline zurückzugreifen. Es existieren sowohl von der 3D-API abhängige, als auch API-übergreifende Sprachen. Die wichtigsten Vertreter sind hier HLSL, GLSL und Cg. Alle drei Sprachen werden bei der Übersetzung in Zwischensprachen überführt, die teilweise starken Restriktionen unterliegen. HLSL Microsoft entwickelte für DirectX die Sprache HLSL (High Level Shading Language). Sie ist nur mit Microsoft Windows und DirectX nutzbar und wurde in dieser Arbeit nicht verwendet. Da bei der Spieleproduktion jedoch weitestgehend DirectX eingesetzt wird, orientieren sich viele Grafikkartenhersteller an diesem Standard. HLSL baut auf speziellen Assemblersprachen für Vertex- und Fragment-Shader auf, die in verschiedenen Versionen existieren. Die Fähigkeiten dieser Assemblersprachen werden im so genannten Shader-Model beschrieben. Die der OpenGL-Shading-Language (GLSL) zugrundeliegenden Assemblersprachen besitzen kein solches Versionssystem. Daher wird auch im Zusammenhang mit GLSL auf das Shader-Model Bezug genommen. Ältere Versionen des Shader-Model variierten hauptsächlich in der Größe der verarbeitbaren Programme und der Anzahl und Art der Texturzugriffe. Mit dem Shader-Model 3, welches erstmals in DirectX 9.0c verfügbar wurde, enthielten Fragment-Prozessoren die nötigen Fähigkeiten zum Raytracing komplexer dreidimensionaler Objekte [KF05]: Erhöhte Instruktionsanzahl Der Quelltext eines Fragment-Programms darf im Shader-Model 3 maximal 65535 Maschinenbefehle erzeugen. Kürzere Programme, die bedingt durch Schleifen und Funktionsaufrufe mehr als 65535 Befehle ausführen, werden abgebrochen. Hierbei ist allerdings nicht klar, wie Befehle einer Hoch- oder Assemblersprache auf die Maschinenbefehle umgesetzt werden, da jeder GPU-Hersteller einen eigenen Instruktionssatz verwendet. 32-Bit Fließkommagenauigkeit Fragment-Shader können nur mit 32-Bit Fließkommazahlen arbeiten. Das Zahlendarstellung entspricht dabei der IEEE-754-Norm für einfache Genauigkeit: VZ 1 Bit 3 Exponent 8 Bit Mantisse 23 Bit Als Beispiele seien hier die Assembler-Sprachen GL_ARB_vertex_program, GL_ARB_fragment_program, GL_NV_vertex_program, GL_NV_fragment_program, GL_ATI_vertex_shader, GL_ATI_fragment_shader, GL_NV_register_combiners und GL_NV_texture_shader genannt, die jeweils in verschiedenen Versionen vorliegen. 10 2.2 Programmiersprachen Allerdings gibt es keine Darstellung für ungültige und unendliche Werte und es kommt hinzu, dass die Implementierung der Fließkommaoperationen nicht genau spezifiziert ist, um den GPU-Herstellern etwas Optimierungsspielraum zu lassen. Verschiedene GPUs können also für die gleiche Berechnung unterschiedliche Ergebnisse liefern. Verzweigungen Shader-Model 3 erlaubt bedingte Sprünge und Schleifen. Shading-LanguageCompiler haben leider immer noch große Probleme mit bedingten Array-Zuweisungen (siehe Abschnitt 7.1.1). Cg Cg steht für „C for graphics“ und wurde von NVidia entworfen, um GPU-Programme über mehrere Plattformen und APIs hinweg nutzbar zu machen. Cg kann nur über den Compiler des Cg-Toolkits benutzt werden. Die Übersetzung von Cg erfolgt profilabhängig in eine andere Shading-Language, die von der gerade verwendeten API unterstützt werden muss. Die Wahl des Profils bestimmt daher den Funktionsumfang von Cg. So existieren beispielsweise für die Shader-Models von DirectX verschiedene Profile. Um die Funktionalität des Shader-Model 3 im Cg-Toolkit 1.5 [NVi07] und unter OpenGL nutzen zu können, stehen zwei Profile zur Verfügung: vp40, fp40 Profile, welche Cg in die Assemblersprachen übersetzen, die durch die OpenGLErweiterungen NV_vertex_program2 und NV_fragment_program2 spezifiziert sind. Sie werden nur durch NVidia-Grafikkarten unterstützt. glslv, glslf Profile, die Cg nach GLSL übersetzen. Cg bietet durch die profilabhängige Übersetzung einen Vorteil für Computerspiele, da Effekte leichter für verschiedene Hardware umgesetzt werden können. Benötigt man jedoch die Funktionalität von Shader-Model 3, so ist die direkte Verwendung von GLSL vorzuziehen, da die Zwischenschicht, die der Cg-Compiler darstellt, eine weitere potentielle Fehlerquelle ist und zudem die Compilezeit erhöht. GLSL Aufgrund der oben genannten Gründe und bereits vorhandener Kenntnisse wurde für diese Arbeit die OpenGL-Shading-Language (GLSL) verwendet. Sie kann ausschließlich mit der OpenGL-API genutzt werden und ist seit OpenGL 2.0 fest im OpenGL-Standard integriert. GLSL wurde durch das Architectural Review Board (ARB), welches aus Mitarbeitern von Wirtschaft und Forschung besteht, entworfen und spezifiziert. Wie die meisten ARB-Entwicklungen kann auch GLSL 1.0 als OpenGL-Erweiterung genutzt werden. Dazu werden in OpenGL 1.5 die Erweiterungen ARB_Shader_Objects, GL_ARB_Vertex_Shader, GL_ARB_Fragment_Shader und GL_ARB_Shading_Language_100 benötigt. Eine Einführung in GLSL wird in Abschnitt 2.3 gegeben. 2.2.2 Stream-Processing-Languages Stream-Processing-Sprachen verwenden Streams, um Daten auszutauschen und Kernels um Berechnungen durchzuführen. Sie abstrahieren von der konkreten Struktur der Grafikpipeline und ermöglichen so eine deutlich vereinfachte Verwendung der GPU als Koprozessor. Es existieren zwei bekannte Frameworks, die dieses Konzept umsetzen: BrookGPU und Sh. 11 2 Grundlagen der GPU-Programmierung BrookGPU Brook ist eine Erweiterung des ANSI-C-Standards durch die Universität von Stanford. Die Erweiterungen bestehen aus Konstrukten zur Beschreibung von Kernels und Streams und aus einer API, in der häufig verwendete Algorithmen umgesetzt sind. Das BrookGPU-System besteht aus einem eigenen Compiler und einem Laufzeitsystem. Letzteres setzt die im Programm auftretenden Berechnungen auf die jeweilige GPU um. [BFH+ 04] Sh Sh ist eine in C++ eingebettete Sprache. Sie benötigt keinen eigenen Compiler, sondern ist als C++-Bibliothek implementiert. [Rap06] Stream-Processing-Sprachen wurden in dieser Arbeit aus Zeitgründen nicht berücksichtigt. Möglicherweise lässt sich das Raytracing algebraischer Flächen im Stream-Processing-Modell leichter und stabiler auf die GPU umsetzen, als dies beim direkten Einsatz von ShadingLanguages der Fall ist. 2.3 Einführung in die OpenGL-Shading-Language Die im Juni 2003 freigegebene Sprachbeschreibung der OpenGL-Shading-Language ist eine der wichtigsten Neuerungen von OpenGL seit Veröffentlichung der OpenGL-Version 1.0 im Jahre 1992. Sie erlaubt die Programmierung von Vertex- und Fragment-Shadern, wodurch die Standardfunktionalität der zugehörigen Stufen in der OpenGL-Grafikpipeline ersetzt wird. An dieser Stelle wird Bezug auf die Spezifikationen von OpenGL 2.0 [SA04] und der OpenGL-Shading-Language 1.10 [KBR04] genommen. Die OpenGL-Spezifikation enthält nur die zur Ausführung und Ansteuerung der Shader notwendigen Funktionen. GLSL selbst wird in einem separaten Dokument beschrieben. Die folgenden Abschnitte beschreiben beide Aspekte. Ein abschließendes Beispiel verdeutlicht die Nutzung von GLSL. 2.3.1 OpenGL-API für GLSL Der Quelltext von GLSL-Shadern wird in OpenGL in Shader-Objekten organisiert. Mehrere Shader-Objekte zusammen bilden ein Programm-Objekt. In einem Programm-Objekt muss mindestens ein Vertex-Shader-Objekt und ein Fragment-Shader-Objekt vorhanden sein. Der Code aller Shader-Objekte eines Typs bildet jeweils eine Einheit, so dass Funktionen, die in einem Shader-Objekt definiert sind, auch in den anderen Shader-Objekten verwendet werden können. Dies soll die Erstellung von Shader-Bibliotheken erleichtern. Leider wird diese Funktionalität von den getesteten Grafikkartentreibern nicht korrekt umgesetzt (siehe Abschnitt 7.1.1). Shader-Objekte Ein Shader-Objekt wird in der Anwendung über einen Index referenziert, welcher über die Funktion GLuint glCreateShader( GLenum type ); 12 2.3 Einführung in die OpenGL-Shading-Language angefordert werden kann. Der Wert von type ist dabei entweder GL_VERTEX_SHADER oder GL_FRAGMENT_SHADER. Einem Shader-Objekt wird mittels void glShaderSource( GLuint shader, GLsizei count, const GLchar **string, const GLint *length ); ein Shader-Quelltext zugewiesen. shader gibt die ID des Shaders und count die Anzahl der zuzuweisenden Quelltextteile an. string enthält diese Quelltextteile und length speichert die Längen der einzelnen Strings. Wurde dem Shader-Objekt bereits ein Quelltext zugewiesen, so wird dieser überschrieben. Ist ein Shader-Objekt mit GLSL-Quelltext versehen, so kann es mit void glCompileShader( GLuint shader ); kompiliert werden. Nicht mehr benötigte Shader-Objekte können und sollten mit void glDeleteShader( GLuint shader ); gelöscht werden, um den belegten Speicher wieder freizugeben. Programm-Objekte GLSL-Programm-Objekte werden in OpenGL durch GLuint glCreateProgram( void ); erzeugt. An ein solches Programm-Objekt können mehrere Shader-Objekte angehängt werden. Dafür steht die Funktion void glAttachShader( GLuint program, GLuint shader ); zur Verfügung. Ein Shader-Objekt kann einem Programm-Objekt bereits hinzugefügt werden, bevor es einen Quelltext besitzt. Sind alle nötigen Shader-Objekte kompiliert und angefügt, so müssen die einzelnen Teile das GLSL-Programms noch mit void glLinkProgram( GLuint program ); gebunden werden. Damit sind die Schritte zum Erstellen eines vollständigen GLSL-Programms unter OpenGL abgeschlossen. Nun kann die Funktionalität der Fixed-Function-Pipeline mit void glUseProgram( GLuint program ); durch das GLSL-Programm ersetzt werden. Der Aufruf glUseProgram( 0 ); schaltet wieder zur Standard-Funktionalität zurück. Wird ein GLSL-Programm nicht mehr benötigt, so müssen zuerst alle Shader-Objekte vom Programm-Objekt entfernt werden bevor dieses gelöscht werden kann. Dazu dienen die nachfolgenden Funktionen. void glDetachShader( GLuint program, GLuint shader ); void glDeleteProgram( GLuint program ); glDetachShader löscht hier nicht den Shader, sondern trennt nur Shader- und Programm- Objekte voneinander. 13 2 Grundlagen der GPU-Programmierung Fehlerbehandlung Beim Kompilieren und Binden des GLSL-Programms können natürlich Fehler auftreten. Ob diese Operationen erfolgreich waren, lässt sich mit den Funktionen void glGetShaderiv( GLuint shader, GLenum pname, GLint *params ); void glGetProgramiv( GLuint program, GLenum pname, GLint *params ); prüfen. pname gibt dabei an, welche Daten ausgelesen und in params gespeichert werden sollen. Für einen Shader kann GL_COMPILE_STATUS und für ein Programm GL_LINK_STATUS und GL_VALIDATE_STATUS abgefragt werden. Die Validierung mit void glValidateProgram( GLuint program ); ist ein zusätzlicher Schritt, welcher prüft, ob das GLSL-Programm auch tatsächlich auf der vorhandenen Hardware und mit dem aktuellen OpenGL-Status lauffähig ist. Enthält die Ergebnisvariable params den Wert GL_FALSE, so trat bei der betreffenden Aktion ein Fehler auf. Um die genaue Fehlerursache zu ermitteln, stehen zwei so genannte Info-Logs zur Verfügung. Diese können mit den Befehlen void glGetShaderInfoLog( GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog ); void glGetProgramInfoLog( GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog ); ausgelesen werden. Die notwendige Puffergröße für das Info-Log kann man zuvor als das Attribut GL_INFO_LOG_LENGTH mit glGetShaderiv beziehungsweise glGetProgramiv bestimmen. Shader-Variablen Ein Shader kann auf eine Vielzahl von Variablen zugreifen, die sich in drei Gruppen einteilen lassen. Die Variablen der verschiedenen Gruppen werden im Shader-Code unterschiedlich deklariert. Um von der Anwendung auf Shader-Variablen zugreifen zu können, muss das ShaderProgramm vollständig kompiliert und gebunden sein. uniform-Variablen Globale Variablen werden als uniform deklariert. Sie können in Vertex- und Fragment-Shadern ausgelesen, aber nicht beschrieben werden. Derartige Variablen können nur von der Anwendung verändert werden und repräsentieren meist Teile des aktuellen Systemzustands wie beispielsweise Zeitparameter einer Animation. Der Zugriff auf uniform-Variablen innerhalb der Anwendung erfolgt über einen Index. Um die Verbindung zwischen den textuellen Bezeichnern im Shader und dem Index im Programm herzustellen, wird die Funktion GLint glGetUniformLocation( GLuint program, const GLchar *name ); verwendet. Die Zuweisung eines Wertes an eine uniform-Variable erfolgt je nach Variablentyp über eine der folgenden Funktionen. Für eine genauere Erläuterung der einzelnen Varianten sei auf die OpenGL-Spezifikation [SA04] verwiesen. 14 2.3 Einführung in die OpenGL-Shading-Language void glUniform{1234}{if}( GLint location, T value ); void glUniform{1234}{if}v( GLint location, GLsizei count, T value ); void glUniformMatrix{234}{f}v( GLint location, GLsizei count, GLboolean transpose, const GLfloat *value ); uniform-Variablen sind über ein komplettes grafisches Primitiv konstant und können daher nur außerhalb eines glBegin/glEnd-Blockes verändert werden. attribute-Variablen Beim Übergeben von Vertices an die Grafikpipeline ist es meist wünschenswert auch zusätzliche Attribute wie Materialien, Normalen und Texturkoordinaten angeben zu können. Dies kann wie gewohnt über die Standardfunktionen von OpenGL erfolgen. Im Vertex-Shader sind die entsprechenden Variablen vordefiniert. Möchte man zusätzlich Attribute übergeben, die OpenGL nicht bekannt sind, so können diese im Vertex-Shader als attribute-Variablen deklariert werden, deren Index in der Anwendung mit GLint glGetAttributeLocation( GLuint program, const GLchar *name ); bestimmt werden kann. Bei der Übergabe eines Vertex kann der Attributwert je nach Variablentyp über eine der folgenden Funktionen an den Vertex gebunden werden. void glVertexAttrib{1234}{sfd}( GLuint index, T values ); void glVertexAttrib{123}{sfd}v( GLuint index, T values ); void glVertexAttrib4{bsifd ubusui}v( GLuint index, T values ); varying-Variablen Im Fragment-Shader sind anstelle von Attributen die so genannten varying-Variablen verfügbar. Sie entstehen bei der Interpolation der Ausgaben des Vertex-Shaders. Diese Art von Variablen kann daher nicht direkt von OpenGL aus angesteuert werden. 2.3.2 GLSL-Sprachelemente Die Sprachbeschreibung von GLSL [KBR04] ist sehr umfassend und kann nicht im Detail besprochen werden. Da GLSL sehr stark an C angelehnt ist, wird nur auf die wichtigsten Unterschiede eingegangen. Datentypen Die primitiven Datentypen beschränken sich in GLSL auf float, int und bool. GLSL-Fließkommazahlen entsprechen in ihrer Darstellung den IEEE-Fließkommazahlen einfacher Genauigkeit (32 Bit). Es ist jedoch nicht festgelegt, wie die Operationen auf den Fließkommazahlen genau durchzuführen sind. Lediglich die OpenGL-Richtlinien für Fließkommaberechnungen müssen erfüllt werden (siehe [SA04, S. 6]). Der int-Datentyp existiert nur als Programmierhilfe und muss nicht direkt in der Hardware umgesetzt werden. Die Zahlendarstellung sieht 16 Bit für den Betrag der Zahl und 1 Bit für das Vorzeichen vor. Die zugrunde liegende Hardware kann den int-Datentyp auf float abbilden und muss nur sicherstellen, dass die Operationen für den 16 Bit Wertebereich [−32.767, 32.767] korrekte Ergebnisse liefern. Der 15 2 Grundlagen der GPU-Programmierung Datentyp bool enthält nur die Elemente true und false. Diese können bei Bedarf in Ganzoder Fließkommazahlen mit den Werten 1 oder 0 konvertiert werden. Für jeden der primitiven Datentypen existieren Vektordatentypen verschiedener Dimension: vec2, vec3, vec4 für float-Vektoren; ivec2, ivec3, ivec4 für int-Vektoren; bvec2, bvec3, bvec4 für bool-Vektoren. Matrizen sind nur für Fließkommazahlen vorhanden: mat2, mat3, mat4. Texturzugriffe erfolgen über so genannte sampler-Variablen. GLSL definiert entsprechende Datentypen für ein- und mehrdimensionale Texturen, Cube- und Shadow-Maps: sampler1D, sampler2D, sampler3D, samplerCube, sampler1DShadow und sampler2DShadow. Benutzerdefinierte Datentypen können durch Verbunde (struct) realisiert werden, die auch geschachtelt werden können. Weiterhin können in GLSL auch eindimensionale Arrays verwendet werden. Mehrdimensionale Arrays können nur simuliert werden, indem Arrays von Verbunden gebildet werden, die wiederum Arrays enthalten. Allerdings muss die Größe eines Arrays bereits zur Übersetzungszeit feststehen. Es gibt zur Laufzeit keine Möglichkeit zusätzlichen Speicherplatz zu belegen. Qualifizierer Variablendeklarationen können einen oder mehrere Qualifizierer enthalten. Mit const deklarierte Variablen sind nach ihrer Initialisierung nicht mehr änderbar. Für globale Shader-Variablen sind die bereits in Abschnitt 2.3.1 erläuterten Qualifizierer uniform, attribute und varying erlaubt, wobei varying-Variablen keine Verbunde und attribute-Variablen weder Verbunde noch Arrays enthalten dürfen. Da es in GLSL keine Referenzen beziehunhsweise Zeiger gibt, erfolgen Zuweisungen und Parameterübergaben immer durch Wertekopien. Um auch Funktionen mit Rückgabeparametern zu realisieren, können Funktionsparameter mit out beziehungsweise inout deklariert werden. Das Standardverhalten ist gleichwertig zu einer Deklaration mit in. Rekursion Der Quelltext von GLSL-Programmen kann analog zur Programmiersprache C in Funktionen beziehungsweise Prozeduren gegliedert werden. Allerdings verbietet die GLSL-Spezifikation jede Form von Rekursion. Viele rekursive Algorithmen können auch durch Schleifen implementiert werden, wobei Zwischenergebnisse in einem Stack verwaltet werden. Die beschränkten Zugriffsmöglichkeiten auf Arrays (siehe Abschnitt 7.1.1) verhindern aber möglicherweise die erfolgreiche Umsetzung einiger rekursiver Algorithmen. Standardbibliothek und Operatoren In GLSL steht eine Vielzahl arithmetischer Operationen zur Verfügung. Neben den komponentenweise durchgeführten Operationen auf Vektoren und Matrizen, sind auch Skalarprodukt, Matrixprodukt, Multiplikation von Matrizen und Vektoren und vieles mehr definiert. In der Standardbibliothek sind unter anderem Trigonometrie-, Exponential- und Interpolationsfunktionen vorhanden. GLSL verfügt darüberhinaus über komplexe Zugriffsoperationen für Vektordatentypen. Die Einzelkomponenten eines Vektors können über den (.)-Operator gefolgt vom Komponentennamen angesprochen werden: 16 2.3 Einführung in die OpenGL-Shading-Language vec4 v.x; v.r; v.s; v; v.y; v.z; v.w; v.g; v.b; v.a; v.t; v.p; v.q; // Syntax für Raumkoordinaten // Syntax für Farbwert // Syntax für Texturkoordinaten Dabei kann auch auf mehrere Komponenten gleichzeitig und in beliebiger Reihenfolge zugegriffen werden. Die verschiedenen Bezeichnungsschemata dürfen jedoch nicht vermischt werden. vec4 v4 = vec4( 1.0, 2.0, 3.0, 4.0 ); vec2 v2 = vec2( 5.0, 6.0 ); v2 = v4.xz; v2 = v4.ww; v4.rgba = v4.wzyx + v2.xxxx v2 = v4.rx; // // // // v2 == ( 1.0, 3.0 ) v2 == ( 4.0, 4.0 ) v4 == ( 8.0, 7.0, 6.0, 5.0 ) Fehler (gemischtes Bezeichnungsschema) Vektoren können auch wie Arrays indiziert werden. Der gleichzeitige Zugriff auf mehrere Elemente ist damit allerdings nicht möglich. Vordefinierte Variablen GLSL-Programme können auf einen Großteil der aktuellen OpenGL-Zustandsvariablen zugreifen. Dazu zählen insbesondere Modelview- und Projektionsmatrix, sowie Parameter der Lichtquellen. Darüberhinaus verfügen Vertex- und Fragment-Shader jeweils über eigene Variablen, die zum Teil im nächsten Abschnitt beschrieben werden. Shadereintrittspunkte und -ausgaben Der Einstiegspunkt von Vertex- und Fragment-Shader ist jeweils die main-Routine. Diese hat weder Parameter noch Rückgabewerte. Die Kommunikation mit den benachbarten Teilen der Grafikpipeline erfolgt ausschließlich über globale Variablen. Als Eingabe für den Vertex-Shader dient insbesondere die Variable gl_Vertex, welche den zu bearbeitenden Vertex enthält. Der Vertex-Shader transformiert den Vertex, speichert ihn in gl_Position und übergibt ihn damit dem Rasterizer. Andere als varying deklarierte Variablen werden ebenfalls vom Rasterizer interpoliert und an den Fragment-Shader weitergereicht. Der Fragment-Shader benutzt die eingehenden Shader-Variablen, um den Farbwert des zugehörigen Pixels zu bestimmen. Dieser wird in gl_FragColor abgelegt. Zusätzlich zum Farbwert kann auch die Tiefe eines Pixels bestimmt werden und über gl_FragDepth zum Tiefentest weitergegeben werden. 2.3.3 GLSL-Beispiel Zum besseren Verständnis wird nun ein kurzes GLSL-Beispiel vorgestellt. Ziel ist es mit Hilfe eines GLSL-Programms aus einem ebenen Gitternetz eine farbige, wehende Fahne zu erzeugen. Abbildung 2.6 zeigt die Schritte von der Ebene bis zur fertigen Fahne. Das gezeigte Gitternetz wird zwischen den Punkten (0, 0, 0) und (3, 2, 0) aufgespannt. Um das Wehen der Fahne zu erzeugen, wird die z-Koordinate eines Gitterpunktes durch eine Sinusschwingung modelliert. Als Parameter für die Sinusschwingung dienen die x-Koordinate des Punktes sowie ein Zeitparameter timestamp. Eine Seite einer Fahne wird meist an einem 17 2 Grundlagen der GPU-Programmierung (a) (b) (c) Abbildung 2.6: Einzelschritte des GLSL-Beispiels: (a) regelmäßiges, planares Gitter; (b) Gitternetz der wehenden Fahne; (c) eingefärbte, wehende Fahne. Mast befestigt, wodurch sich die Fahne an dieser Seite kaum bewegen kann. Zur Modellierung dieses Verhaltens wird die Schwingung abhängig von der x-Koordinate gedämpft. Es ergibt sich folgende Berechungsvorschrift für die z-Koordinate eines Vertex: z = sin(x + timestamp) ∗ x (2.1) Um die Schwingung besser der Fahnengröße anzupassen, kann die Vorschrift bei der Implementierung im Vertex-Shader noch leicht modifiziert werden. Der Vertex-Shader hat außer der Transformation zusätzlich die Aufgabe, die Farbattibute, die jedem Vertex angehängt wurden, an den Rasterizer weiterzugeben. Der Fragment-Shader übernimmt das Färben der Fahne. Da der Rasterizer den benötigten Farbwert bereits über die Fläche interpoliert hat, muss der Farbwert lediglich der Ausgabevariable gl_FragColor zugewiesen werden. Die Shader müssen nun noch von einem OpenGL-Programm geladen und angesteuert werden. Die dieser Arbeit beiliegende DVD enthält den zum Rendering der Fahne notwendigen Quelltext. Der Effekt aus dem Beispiel hätte auch leicht ohne GLSL realisiert werden können. Die Verwendung von Shadern hat jedoch gegenüber dem konventionellen Vorgehen einen klaren Vorteil. Durch die Animation der Fahne verändern sich die Positionen der einzelnen Gitterpunkte in jedem Frame. Soll die Berechnung ohne Shader durchgeführt werden, so müssten die Vertices und Normalen der Fahne in jedem Bild abhängig vom Animationsparameter auf der CPU neu berechnet und zur Grafikkarte gesendet werden. Dies erhöht die CPU-Last und den Datenaustausch mit der Grafikkarte. Erfolgt die Berechnung der Animation auf der GPU, so ist das von der CPU erzeugte Gitternetz immer gleich und kann daher in einer OpenGLDisplay-Liste gespeichert werden. Dadurch wird der gesamte Code zum Rendern der Fahne im Grafikspeicher gehalten4 . Die CPU muss nur den Animationsparameter verändern und den Zeichenvorgang auslösen. Zu Testzwecken wurde eine CPU-basierte Variante der Fahnenanimation erstellt. Auf einem System bestehend aus einer AMD Athlon 2000+ CPU und einer NVidia GeForce 6600 GT wurde eine Szene mit 100 Fahnen in jeweils verschiedenen Animationsstadien gerendert. Die CPU-basierte Animation konnte mit 31 Bildern pro Sekunde dargestellt werden. Der Einsatz von Display-Listen und Shadern steigerte die Bildwiederholrate auf 131 Bilder pro Sekunde. 4 Die OpenGL-Spezifikation schreibt die Speicherung der Display-Listen im Grafikspeicher nicht vor. Dennoch unterstützen die meisten modernen Grafikkarten diese Beschleunigungstechnik. 18 2.4 Werkzeuge und Bibliotheken 2.4 Werkzeuge und Bibliotheken Um die GLSL-Programmierung zu erleichtern wurde eine Reihe von Programmen und Bibliotheken entwickelt. Einige davon wurden in dieser Arbeit als Hilfmittel genutzt und sollen daher kurz vorgestellt werden. ATI RenderMonkey RenderMonkey [ATI] ist ein Rapid-Prototyping-Werkzeug zur Shader-Entwicklung, welches besonders für den Einstieg in die Thematik geeignet ist. Es erledigt die Shader-Übersetzung und die Übergabe von Shader-Variablen. Der Nutzer kann Shader-Parameter über eine GUI einstellen und die Wirkung der erstellten Shader unmittelbar an vorgefertigten 3D-Modellen überprüfen. Sogar Multipass-Verfahren können mit RenderMonkey leicht umgesetzt werden. NVidia NVemulate Auf Systemen mit NVidia-Grafikkarten kann NVemulate [NVi] als Debugging-Werkzeug verwendet werden. Einerseits kann durch NVemulate das Verhalten verschiedener NVidiaGPUs emuliert werden und sogar auf einen Software-Renderer umgeschaltet werden. Andererseits können diverse Logfiles generiert werden, die die Shader-Nutzung einer Anwendung protokolieren. Unter den gesammelten Daten finden sich Shader-Quelltexte, der Assemblercode der Zwischensprachen und Fehlermeldungen, die beim Kompilieren und Binden der Shader entstehen. 3Dlabs GLSLvalidate Mit GLSLvalidate [3Dl] lassen sich GLSL-Shader auf Verstöße gegen die Spezifikation von GLSL prüfen. In einigen Fällen sind die generierten Fehlermeldungen aussagekräftiger als die der Grafikkartentreiber. GLee GLee [Woo] steht für GL Easy Extension und erleichtert den Zugriff auf die Funktionalität von aktuellen OpenGL-Versionen. Da sich die mitgelieferten OpenGL-Bibliotheken einiger Compiler (darunter auch Microsoft Visual Studio) seit Jahren nicht verändert haben, können beispielsweise die Funktionen zur Verwendung der OpenGL-Shading-Language nur umständlich über den OpenGL-Erweiterungsmechanismus erreicht werden. GLee besteht aus einer aktuellen OpenGL-Headerdatei und einer Bibliothek, die das Laden der benötigten Erweiterungen automatisch übernimmt, sofern diese von der vorhandenen Grafikhardware unterstützt werden. 2.5 Zusammenfassung Durch den Einsatz von Shadern können die Berechnungen in der Grafikpipeline flexibel gestaltet werden. Im betrachteten Modell können Vertex- und Fragment-Shader mit Hilfe von Hochsprachen programmiert werden. Auch wenn das ungewöhnliche Programmiermodell die Umsetzung einiger Algorithmen erschwert, können viele berechnungsintensive Anwendungen durch die Parallelverarbeitung auf aktuellen GPUs beschleunigt werden. 19 2 Grundlagen der GPU-Programmierung 20 3 Grundlagen des GPU-basierten Raytracings Zur Darstellung einer 3D-Szene sind eine Reihe verschiedener Verfahren bekannt. Die Idee des so genannten Raycasting-Algorithmus wurde 1968 von Appel [App68] entwickelt und 1980 durch Whitted zum rekursiven Raytracing [Whi80] (auch Whitted-Raytracing genannt) erweitert. Beim Raytracing können globale Beleuchtungsphänomene, also Wechselwirkungen aller Objekte und Lichtquellen der Szene, berücksichtigt werden, wodurch die Erzeugung photorealistischer Bilder möglich wird. Der damit verbundene hohe Berechnungsaufwand verhinderte lange Zeit den Einsatz des Raytracings in der Echtzeitvisualisierung. Programmierbare GPUs und Spezialhardware, wie die Ray-Processing-Unit der Universität des Saarlandes [WSS05], ermöglichten erstmals Bildwiederholraten, wie sie für interaktive Anwendungen benötigt werden. 3.1 Der Raytracing-Algorithmus Der ursprüngliche Raycasting-Algorithmus [App68] geht von einer virtuellen Kamera, bestehend aus Augpunkt und Bildebene, aus, durch die die Szene betrachtet wird. Vom Augpunkt werden durch jeden Pixel der Bildebene ein oder mehrere Strahlen, so genannte Primärstrahlen, in die Szene ausgesendet, um den Farbwert des Pixels zu bestimmen. Dabei wird das erste Szenenobjekt ermittelt, welches den Strahl schneidet. Anhand der Lichtquellen in der Szene kann für den Schnittpunkt ein lokaler Beleuchtungswert ermittelt werden, der die Pixelfarbe bestimmt. Hierfür nutzt man beispielsweise das Phong-Beleuchtungsmodell, wie es auch in OpenGL zur Anwendung kommt [SA04, S. 59ff]. Leider kann dieses einfache Verfahren keine globalen Beleuchtungseffekte realisieren. Das rekursive Raytracing [Whi80] ermöglicht hingegen Spiegelungen, Brechungen und Schatten. Dies geschieht wie in Abbildung 3.1 durch die Generierung so genannter Sekundärstrahlen und Schattenfühler. Die Sekundärstrahlen entstehen, wenn ein Strahl an einem Objektschnittpunkt reflektiert oder gebrochen wird. Ein Schattenfühler ist lediglich ein weiterer Strahl vom Objektschnittpunkt in Richtung einer Lichtquelle. Durch ihn kann bei der Beleuchtung eines Punktes bestimmt werden, ob ein weiteres Szenenobjekt die Lichtquelle verdeckt und der Punkt damit im Schatten liegt. Die Primär- und Sekundärstrahlen definieren einen Baum von Strahlen, der sich jeweils an Schnittpunkten mit Objekten verzweigt. In hinreichend großen Szenen kann dieser Baum sehr tief werden. Da tief liegende Knoten meist nur einen sehr geringen Beitrag zur Farbe des Pixels liefern, werden ab einer bestimmten Baumtiefe keine weiteren Sekundärstrahlen erzeugt. Die Farbe an einem Schnittpunkt zwischen Strahl und Objekt wird nun durch ein lokales Beleuchtungsmodell unter Berücksichtigung der Schattenfühler bestimmt und mit den rekursiv ermittelten Farbinformationen der zugehörigen Sekundärstrahlen verrechnet. 21 3 Grundlagen des GPU-basierten Raytracings reflektierter Strahl Flächennormale Primärstrahl Augpunkt Bildebene Schattenfühler gebrochener Strahl Szenenobjekt Abbildung 3.1: Generierung der Strahlen beim rekursiven Raytracing: Ein Primärstrahl verläuft vom Augpunkt durch einen Pixel der Bildebene in die Szene. Schneidet der Primärstrahl ein Objekt, so werden Schattenfühler, reflektierter und gebrochener Strahl weiterverfolgt. Das vorgestellte Verfahren nach Whitted liefert für viele Szenen gute Ergebnisse. Es scheitert aber, sobald Lichtbündelungen (Kaustiken) oder diffuse Lichtreflektionen die Beleuchtung der Szene maßgeblich beeinflussen. Um diese Beleuchtungsphänomene auch beim Raytracing zu berücksichtigen, wurden komplexere Algorithmen wie beispielsweise Path-Tracing [LWS93] und Photon-Mapping [Jen96] entwickelt, die jedoch auch einen erheblichen Anstieg der Renderingzeit mit sich bringen. Abbildung 3.2 verdeutlicht die Unterschiede zwischen Raycasting, dem Raytracing nach Whitted und Photon-Mapping. 3.1.1 Beschleunigung des Raytracings Bereits Whitted [Whi80] stellte in seinem Artikel über das rekursive Raytracing fest, dass die Berechnung der Schnittpunkte zwischen den Strahlen und den in der Szene befindlichen Objekten die rechenintensivste Aufgabe des Verfahrens darstellt. Der naive Ansatz testet jeden Strahl mit jedem grafischen Primitiv der Szene. Die meisten Beschleunigungsverfahren legen daher die Primitive in speziellen Datenstrukturen ab, mit denen sich leicht abfragen lässt, welche Primitive überhaupt vom Strahl getroffen werden können. Der Schnittpunkttest wird nur auf die verbliebenen Primitive angewandt. Gängige Beschleunigungsverfahren wie uniform grids, k-d trees und bounding volume hierarchies wurden bereits für die GPU umgesetzt. Ein Vergleich der Implementierungen findet sich in [TS05]. Die genannten Verfahren entfalten ihre Wirkung in Szenen mit vielen, meist einfachen Objekten, nicht jedoch beim Raytracing einzelner, komplexer Primitive wie algebraischer Flächen. Die zum Rendern der Fläche benötigte Zeit hängt daher maßgeblich von den verwendeten Schnittpunkttests ab. Sie werden in den Kapiteln 5 und 6 näher erläutert. 3.2 Anpassung des Algorithmus für die GPU Das Raytracing algebraischer Flächen ist eine sehr rechenintensive Aufgabe. Um eine echtzeittaugliche Bildwiederholrate von etwa 15 Bildern pro Sekunde [AMMH02, S. 1] erzielen zu können, wurde in dieser Arbeit auf die Generierung der Sekundärstrahlen und Schattenfühler 22 3.2 Anpassung des Algorithmus für die GPU (a) Raycasting (b) rekursives Raytracing (c) Photon-Mapping Abbildung 3.2: Vergleich zwischen Raycasting, rekursivem Raytracing nach Whitted und Photon-Mapping: (a) Raycasting kann verdeckte Flächen eliminieren und Lichtintensitäten nach einem lokalen Beleuchtungsmodell berechnen. (b) Beim rekursiven Raytracing werden Spiegelungen und Brechungen korrekt dargestellt, es fehlen Kaustiken sowie diffuse Reflektionen. Schatten erscheinen hart, da das Flächenlicht nur als Punkt betrachtet wird. (c) Die Szene wird durch Photon-Mapping korrekt global beleuchtet. Alle Szenen wurden mit SunFlow [Sun] gerendert. verzichtet. Die Darstellung der algebraischen Flächen erfolgt also durch die einfachste Form des Raytracings, dem Raycasting. Das Raycasting kann in konventionellen 3D-APIs wie OpenGL und DirectX dazu genutzt werden, komplexere Primitive als nur Polygone darzustellen. Dazu wird mit Hilfe der 3D-API die umgebende Box1 des Primitives gezeichnet. Beim Rastern der Box kann im FragmentShader für jeden Pixel der betreffende Teil des Primitives durch Raycasting bestimmt und beleuchtet werden. Statt einer umgebenden Box kann natürlich auch ein beliebiges anderes Polygon gezeichnet werden. Es dient lediglich dazu, das Raycasting auf einen Ausschnitt der Bildebene zu begrenzen. Abbildung 3.3 verdeutlicht noch einmal das grundlegende Vorgehen beim GPU-basierten Raycasting. 3.2.1 Aufgaben des Vertex-Shaders Zur Simulation des Raycastings in der umgebenden Box müssen zuerst die Primärstrahlen erzeugt werden. Beim Zeichnen der Box gelangen deren Eckpunkte v untransformiert in den Vertex-Shader und können dort mit Hilfe der Modelview-Matrix M an die gewünschte Position in der Szene transformiert werden. Da der Augpunkt im Koordinatenursprung o liegt, ergibt sich der Primärstrahl eines Eckpunktes als r1 (t) = o + t · (M v − o) (3.1) mit t ∈ [1, ∞]. Das Primitiv, welches durch die Box repräsentiert wird, müsste nun ebenfalls dieser Transformation unterzogen werden. Einfacher ist es aber die inverse Transformation auf den Strahl anzuwenden. Analog können zusätzliche Transformationen des Primitives innerhalb der Box in die Berechnung des Strahls einbezogen werden. Sind diese zusätzlichen Transformationen in der Matrix S zusammengefasst2 , so ergibt sich der Strahl, mit dem die 1 2 Es ist natürlich ausreichend nur die Vorderseiten der Box zu zeichnen. Die benötigten Matrizen müssen dem Shader gegebenenfalls als uniform-Parameter übergeben werden. 23 3 Grundlagen des GPU-basierten Raytracings Primärstrahl Augpunkt Szenenobjekt mit umgebender Box Bildebene Abbildung 3.3: GPU-basiertes Raycasting: Die Anwendung zeichnet die umgebende Box der darzustellenden Objekte. Beim Rendern der Box berechnen Shader, was der Betrachter beim Blick in die Box sehen würde. Schnittpunktberechnung durchgeführt wird, als r2 (t) = S −1 M −1 (o + t · (M v − o)) (3.2) Ursprung und Richtung der so erzeugten Primärstrahlen werden anschließend in varyingVariablen gespeichert und so dem Rasterizer übergeben, der sie für jedes Fragment interpoliert. 3.2.2 Aufgaben des Fragment-Shaders Der Fragment-Shader führt nun das eigentliche Raycasting durch. Er enthält den Code zur Berechung des Schnittpunktes phit und der Normale ~nphit , sowie zur Beleuchtung des ermittelten Schnittpunktes. Der Schnittpunkt von Strahl und Fläche wird im Allgemeinen nicht direkt auf der Vorderseite der Box liegen. Im Fragment-Shader sollte daher zusätzlich zur Pixelfarbe noch der Tiefenwert des Schnittpunktes ausgegeben werden. Auf diese Weise kann der Tiefenpuffertest auch weiterhin zur Elimination verdeckter Flächen genutzt werden, was die Darstellung mehrerer Primitive, deren umgebende Boxen sich überschneiden, stark vereinfacht. Die Realisierung des Raycastings über Fragment-Shader hat trotz der Geschwindigkeit moderner GPUs einen Nachteil. Obwohl die zu benachbarten Primärstrahlen gehörenden Schnittpunkte meist nahe beieinander liegen, kann diese Kohärenz aufgrund der unabhängigen Verarbeitung der Primärstrahlen nicht direkt zur Beschleunigung der Schnittpunktberechnung genutzt werden. Über ein Multipass-Verfahren könnte jedoch eine Beschleunigung erzielt werden. Im ersten Durchgang rendert man die Szene mit einer verringerten Auflösung. Der zweite Durchgang rendert in voller Auflösung und verwendet die Ergebnisse des ersten Renderings als Startnäherung für die Schnittpunktberechnung. Die Tauglichkeit dieses Ansatzes konnte aus Zeitgründen leider nicht mehr untersucht werden. 3.2.3 Koordinatensystem der Beleuchtungsberechnung Die Beleuchtungsberechnung findet erwartungsgemäß im Koordinatensystem des Augpunktes statt. Der in dieses System zurücktransformierte Schnittpunkt ergibt sich als M Sphit . Die 24 3.2 Anpassung des Algorithmus für die GPU Transformation der Normale kann jedoch nicht auf diese Weise vorgenomen werden. Enthalten die Matrizen M und S beispielsweise nicht-uniforme Skalierungen, so steht die transformierte Normale nicht mehr senkrecht zur Fläche. Wie in [Tur90] gezeigt wurde, bleibt diese Eigenschaft hingegen erhalten, wenn man die Matrix ((M S)−1 )T verwendet. 3.2.4 Alternative Ansätze Die Ergebnisse des vorgestellten Raycasting-Algorithmus lassen sich hervorragend mit konventionellem Polygon-Rendering kombinieren, da die genutzten Beleuchtungsinformationen weitestgehend gleich sind. Für mehr Realismus kann man zum Preis einer geringeren Bildwiederholrate auch rekursives Raytracing oder gar Photon-Mapping einsetzen. Komplette oder teilweise Portierungen dieser Algorithmen sind für die GPU bereits erfolgt [PBMH02, WMK04, PDC+ 03]. Bedingt durch die begrenzte Programmlänge von Shadern kann der Farbwert eines Pixels jedoch nicht mehr in einem Renderingdurchlauf bestimmt werden. Die Daten der generierten Sekundärstrahlen werden daher vom Fragment-Shader ausgegeben und im nächsten Durchlauf als Eingabe verwendet. Bei der Schnittpunktberechnung müssen zudem alle Objekte der Szene betrachtet werden, was den Einsatz von Beschleunigungsstrukturen sinnvoll macht. Man kann leicht erahnen, dass die hohe Komplexität der Algorithmen und die daraus resultierenden Multipass-Verfahren eine direkte Umsetzung in Shader-Programme recht umständlich machen. Möglicherweise eignet sich das Stream-Processing-Modell, wie es in [PBMH02] beschrieben wird, besser für derartige Anwendungen. 25 3 Grundlagen des GPU-basierten Raytracings 26 4 Raycasting algebraischer Flächen Implizite und insbesondere auch algebraische Flächen sind ein wichtiges Hilfsmittel in der wissenschaftlichen Modellierung und Visualisierung. Eine implizite Fläche ist definiert als die Nullstellenmenge einer Funktion F : R3 → R, das heißt die Fläche wird durch die Menge aller Punkte (x, y, z) ∈ R3 gebildet, die die Gleichung F (x, y, z) = 0 erfüllen1 . Durch F wird der Raum also in ein Gebiet mit F (x, y, z) < 0, ein Gebiet mit F (x, y, z) > 0 und ein Gebiet mit F (x, y, z) = 0 aufgeteilt, wobei jeweils zwei der Gebiete auch leer sein können. Implizite Flächen, die durch ein Polynom in x, y und z beschrieben sind, werden algebraische Flächen genannt. Sie können durch die Summe X ai,j,k xi y j z k (4.1) F (x, y, z) = i,j,k∈N beschrieben werden, wobei man die ai,j,k als Koeffizienten und die Produkte ai,j,k xi y j z k als Monome bezeichnet. Der Grad eines solchen Monoms ist die Summe seiner Potenzen. Der Grad einer algebraischen Fläche ist als das Maximum der Grade seiner Monome definiert, wobei nur Monome mit von Null verschiedenen Koeffizienten betrachtet werden. Abbildung 4.1 zeigt einige algebraische Flächen vom Grad 2. Zur Visualisierung algebraischer Flächen kann beispielsweise das im vorigen Kapitel erläuterte Raytracing- beziehungsweise Raycasting-Verfahren zum Einsatz kommen. Nachfolgend wird ein Überblick über die Besonderheiten beim Raycasting algebraischer Flächen gegeben. Die Erzeugung der Primärstrahlen und die Beleuchtungsberechnung unterscheiden sich nicht vom Raycasting anderer Primitive und werden daher nicht näher untersucht. 1 Prinzipiell lassen sich auch Flächen in höherdimensionalen Räumen definieren. Zur Visualisierung müssen sie jedoch in den R3 projiziert werden. (a) Kugel: x2 + y 2 + z 2 − 1 = 0 (b) Zylinder: x2 + z 2 − 1 = 0 (c) Doppelkegel: x2 − y 2 + z 2 = 0 Abbildung 4.1: Einige algebraische Flächen vom Grad 2, so genannte Quadriken. 27 4 Raycasting algebraischer Flächen 4.1 Schnittpunktberechnung Um die Schnittpunkte zwischen algebraischer Fläche und Raycasting-Strahl zu bestimmen kann man den Strahl r(t) = (x(t), y(t), z(t))T = o + t · d~ (4.2) in die Flächengleichung einsetzen: (4.3) F (x(t), y(t), z(t)) = f (t) = 0 Die Schnittpunktberechnung reduziert sich damit auf die Nullstellenbestimmung eines univariaten Polynoms f : R → R. Die Transformation in das univariate Polynom f (t) = n X bi ti . (4.4) i∈N muss gegebenenfalls explizit erfolgen, da einige Algorithmen zur Nullstellenbestimmung die Koeffizienten bi benötigen. Kapitel 5 und 6 beschäftigen sich ausführlich mit der Berechnung der Polynomkoeffizienten und der Nullstellen. 4.2 Clipping Algebraische Flächen, wie beispielsweise der in Abbildung 4.1b dargestellte Zylinder, dehnen sich häufig bis ins Unendliche aus. Bei der Visualisierung betrachtet man daher meist nur einen kleinen Ausschnitt der Fläche, der zum Beispiel von einer Kugel oder einem Würfel begrenzt ist. Der Raycasting-Strahl r(t) kann an dem Begrenzungskörper geclippt werden, so dass man ein Intervall [a, b] erhält, in dem sämtliche zu berücksichtigenden Schnittpunkte der Fläche mit dem Strahl liegen. Die Kenntnis des Intervalls [a, b] kann weiterhin dazu genutzt werden, die Rechengenauigkeit bei der Bestimmung der Schnittpunkte zu erhöhen, welche auf der GPU mit 32-Bit Fließkommazahlen erfolgt. Für eine fest vorgegebene algebraische Fläche wird die Genauigkeit aller Berechnungen maßgeblich von der Wahl des Strahlursprungs o und der Strahlrichtung d~ beeinflusst. In Tests hat sich die folgende Konstruktion eines optimierten Strahls ropt (topt ) = oopt + topt · d~opt bewährt: oopt = r a+b 2 und d~opt = 0.5 (4.5) d~ ~. |d| (4.6) Sie entstand aus der Beobachtung, dass im Intervall [−1, 1] in etwa genauso viele IEEE-754Fließkommazahlen liegen, wie außerhalb dieses Intervalls. Durch die Wahl des optimierten Strahlursprungs in der Mitte des „interessanten“ Intervalls befinden sich die zur Darstellung wichtigen Nullstellen in der Nähe von topt = 0 und können daher gegebenenfalls genauer als entferntere Nullstellen bestimmt werden. Die Bedingung |d~opt | = 0.5 verhindert außerdem bei vielen Flächen ein zu starkes Anwachsen der Polynomkoeffizienten und den damit verbundenen Genauigkeitsverlust, der zu einer Verschiebung der Nullstellen führen kann. Natürlich handelt es sich hierbei nur um eine Heuristik. Es lassen sich leicht Flächenformeln konstruieren, bei denen diese Vorgehensweise fehlschlägt. 28 4.3 Flächennormale 4.3 Flächennormale Zur Bestimmung des Normalenvektors ~n der algebraischen Fläche kann die als Gradient bezeichnete Funktion ∂F ∂F ∂F grad(F ) = , , (4.7) ∂x ∂y ∂z genutzt werden. Der Vektor grad(F )(x, y, z) steht senkrecht zur Tangentialebene von F im Punkt (x, y, z) und zeigt immer in Richtung des größten Anstiegs der Funktionswerte von F , also in das Gebiet mit F (x, y, z) > 0 [Kön04, S. 52ff]. Der Gradient kann auch numerisch berechnet werden, indem man die partiellen Ableitungen durch Differenzenquotienten ersetzt: ∂F F (x + h, y, z) − F (x, y, z) (x) ≈ ∂x h ∂F F (x, y + h, z) − F (x, y, z) (y) ≈ ∂y h F (x, y, z + h) − F (x, y, z) ∂F (z) ≈ ∂z h (4.8) (4.9) (4.10) Mit Hilfe des Gradienten lässt sich auch bestimmen, ob der Betrachter von einem Gebiet mit F (x, y, z) < 0 oder F (x, y, z) > 0 auf die Fläche schaut, so dass für beide Fälle verschiedene Oberflächenmaterialien gewählt werden können. Dazu betrachtet man den Cosinus des Winkels zwischen dem Normalenvektor ~n und der Richtung d~ des Raycasting-Strahls, die der Blickrichtung des Betrachters entspricht (siehe Abbildung 4.2). Es reicht nicht aus, wenn nur das Vorzeichen von F am Augpunkt ermittelt wird, da der Raycasting-Strahl die Fläche aufgrund des Clippings mehrmals durchdringen kann, bis er auf einen Punkt der Fläche innerhalb des Clipping-Intervalls trifft. Häufig wird das Gebiet mit F (x, y, z) < 0 als das Innere und das Gebiet mit F (x, y, z) > 0 als das Äußere der Fläche bezeichnet. Für eine Kugel x2 + y 2 + z 2 − r2 = 0 ist dies intuitiv klar, jedoch können die Rollen der Gebiete leicht durch Multiplikation der Gleichung mit −1 vertauscht werden. Weiterhin gibt es so genannte nicht-orientierbare Flächen, die im mathematischen Sinn nur eine Seite besitzen. Beispiele hierfür sind das Möbius-Band und die Kleinsche Flasche (Abbildung 4.3). ~n Augpunkt α F (x, y, z) < 0 Augpunkt d~ F (x, y, z) > 0 F (x, y, z) > 0 ~n α d~ F (x, y, z) < 0 (a) Der Betrachter schaut vom einem Gebiet mit (b) Der Betrachter schaut vom einem Gebiet F (x, y, z) < 0 auf die Fläche. Der Winkel α ist klei- mit F (x, y, z) > 0 auf die Fläche. Der Win~· ~ n kel α ist größer als 90◦ und dementsprechend ner als 90◦ und dementsprechend cos(α) = |dd||~ ~ n| > 0. ~· ~ n cos(α) = |dd||~ ~ n| < 0. Abbildung 4.2: Die Bestimmung des Gebietes, durch das die Fläche betrachtet wird, kann anhand des Winkels zwischen dem Normalenvektor und der Richtung des Raycasting-Strahls erfolgen. 29 4 Raycasting algebraischer Flächen (a) Möbius-Band (b) Kleinsche Flasche mit eingebettetem Möbius-Band Abbildung 4.3: Beispiele nicht-orientierbarer Flächen. Jeder Punkt des Möbius-Bandes (a) kann erreicht werden, ohne die Fläche zu durchdringen oder den Rand zu überqueren. Die Fläche besitzt also nur eine Seite. Die in (b) dargestellte Kleinsche Flasche enthält ein MöbiusBand, so dass Innen- und Außenseite nicht unterschieden werden können. Quelle: [Pol03]. 30 5 Berechnung der Polynomkoeffizienten Das Einsetzen des Raycasting-Strahls (x(t), y(t), z(t))T = o+td~ in die Gleichung F (x, y, z) = 0 einer algebraischen Fläche vom Grad n reduziert die Berechnung formal auf die Nullstellenbestimmung eines univariaten Polynoms f (t) vom Grad n, da jeweils nur lineare Polynome durch lineare Polynome ersetzt werden. Obwohl das Polynom f (t) vielfältig dargestellt werden kann, verwenden die meisten Verfahren zur Nullstellenberechnung die Darstellung in der bereits vorgestellten Monombasis n X ai ti . (5.1) f (t) = i=0 Als Ausgangspunkt der Berechnung der Koeffizienten ai dient natürlich das Polynom F (x, y, z), welches direkt aus einer Benutzereingabe stammt. Vom Benutzer kann nicht erwartet werden, dass er die Flächenformel in einer Basisdarstellung formuliert, da diese meist deutlich mehr Terme enthält und weniger aussagekräftig ist als die folgende, rekursive Darstellung der Polynome: Die einfachsten Polynome werden durch reelle Konstanten und durch die Variablen x, y und z gebildet. Ist P ein Polynom, so auch −P und P i mit i ∈ N. Sind P1 und P2 Polynome, so auch P1 + P2 , und P1 · P2 . Die beschriebene Notation soll anhand der Huntschen Fläche motiviert werden. Sie lässt sich als 4(x2 + y 2 + z 2 − 13)3 + 27(3x2 + y 2 − 4z 2 − 12)2 = 0 (5.2) beschreiben. In der Monombasis entsteht daraus die unhandliche Gleichung 4x6 + 12x4 y 2 + 12x4 z 2 + 12x2 y 4 + 24x2 y 2 z 2 + 12x2 z 4 + 4y 6 + 12y 4 z 2 + 12y 2 z 4 + 4z 6 + 87x4 − 150x2 y 2 − 960x2 z 2 − 129y 4 − 528y 2 z 2 + 276z 4 + 84x2 + 1380y 2 + 4620z 2 − 4900 = 0. (5.3) Die nachfolgend beschriebenen Verfahren sind daher so formuliert, dass sie auch mit rekursiv aufgebauten Polynomen umgehen können. 5.1 Termumformungen auf der CPU Die Funktionen der einzelnen Strahlkoordinaten ox + td~x x(t) y(t) = o + td~ = oy + td~y z(t) oz + td~z (5.4) werden für die Variablen x, y und z in F (x, y, z) eingesetzt. Auf der CPU kann dieses Einsetzen nur für den generischen Strahl aus Gleichung (5.4) vorgenommen werden, da die genauen Werte von o und d~ erst beim eigentlichen Raycasting im Fragment-Shader bekannt sind. Die 31 5 Berechnung der Polynomkoeffizienten Berechnungen müssen also symbolisch durchgeführt werden. Das Polynom f (t) besteht nun aus Termen, die mit Hilfe von Polynomaddition, Polynommultiplikation und Polynompotenzierung zur Monomdarstellung vereinfacht werden können. Aus der Monomdarstellung lassen sich dann die Formeln für die einzelnen Koeffizienten ablesen. Dies soll am Beispiel einer Kugel erläutert werden. Eine Kugel mit Radius 1 und Mittelpunkt im Koordinatenursprung ist durch die Gleichung 0 = x2 + y 2 + z 2 − 1 (5.5) gegeben. Setzt man die Strahlkoordinaten ein, so erhält man 0 = (ox + td~x )2 + (oy + td~y )2 + (oz + td~z )2 − 1 = (o2 + 2ox td~x + t2 d~ 2 ) + (o2 + 2oy td~y + t2 d~ 2 ) + (o2 + 2oz td~z + t2 d~ 2 ) − 1 = x 2 (ox x y y z z 2 + o2y + o2z − 1) + (2ox d~x + 2oy d~y + 2oz d~z )t + (d~x2 + d~y2 + d~z2 )t (5.6) (5.7) (5.8) und kann die Koeffizienten leicht ablesen. a0 = (o2x + o2y + o2z − 1) a1 = (2ox d~x + 2oy d~y + 2oz d~z ) (5.10) a2 = (d~x2 + d~y2 + d~z2 ) (5.11) (5.9) Die durch Termumformungen ermittelten Berechnungsvorschriften für die Koeffizienten müssen nun als Codefragmente in den Fragment-Shader eingefügt werden. Anschließend können dort die Koeffizienten durch das Einsetzen konkreter Strahlen berechnet werden. Leider hat dieses CPU-basierte Verfahren den Nachteil, dass die Berechnungsvorschriften auch bei einfachen Polynomen ohne geeignete Optimierungsstrategien sehr lang werden können. Derartige Optimierungen wurden in dieser Arbeit zugunsten des folgenden Verfahrens nicht näher untersucht. 5.2 Termumformungen auf der GPU Führt man die im vorigen Abschnitt beschriebenen Umformungen im Fragment-Shader durch, so sind Ursprung und Richtung des Strahls bekannt und man kann mit konkreten Werten rechnen. Funktionen zur Polynomaddition, -multiplikation und -potenzierung, die am Ende dieses Abschnitts kurz erläutert werden, müssen dazu einmalig in der Shading-Language programmiert werden. Der zur darzustellenden Fläche gehörende Umformungscode kann dann als Folge dieser Polynomoperationen realisiert werden. Man beginnt mit den linearen Polynomen für x, y und z, sowie mit konstanten Polynomen für alle in der Flächenformel enthaltenen Konstanten und setzt diese zum Polynom f (t) zusammen. Diese Vorgehensweise soll erneut am Beispiel der Kugel verdeutlicht werden. Als Strahlursprung soll o = (1, 1, 1) dienen, die Strahlrichtung wird auf d~ = (2, 3, 4) festgelegt. Daraus resultieren die linearen Polynome x = 1 + 2t, 32 y = 1 + 3t und z = 1 + 4t, (5.12) 5.2 Termumformungen auf der GPU die nun in die Kugelgleichung eingesetzt werden. 0 = x2 + y 2 + z 2 − 1 2 (5.13) 2 2 (5.14) = (1 + 2t) + (1 + 3t) + (1 + 4t) − 1 2 2 2 = (1 + 4t + 4t ) + (1 + 6t + 9t ) + (1 + 8t + 16t ) − 1 2 2 = (2 + 10t + 13t ) + (1 + 8t + 16t ) − 1 2 = (3 + 18t + 29t ) − 1 = 2 + 18t + 29t2 (5.15) (5.16) (5.17) (5.18) Die Koeffizienten sind also a0 = 2, a1 = 18 und a2 = 29. Da keinerlei symbolische Berechnungen durchgeführt werden müssen, lässt sich das geschilderte Verfahren deutlich leichter umsetzen als die Termumformungen auf der CPU. Das Polynom F (x, y, z) muss jedoch in einer Darstellung vorliegen, aus der sich der Umformungscode leicht ableiten lässt. Syntaxbäume eignen sich dafür besonders gut. 5.2.1 Polynomoperationen Um das beschriebene Verfahren anwenden zu können, benötigt man Operationen zur Addition, Multiplikation und Potenzierung von Polynomen. Seien dazu die beiden Polynome X X pn (x) = ai xi und qm (x) = bj xj (5.19) i∈N j∈N vom Grad n beziehungsweise m gegeben, wobei die Koeffizienten ai mit i > n beziehungsweise bj mit j > m zur einfacheren Definition der Operationen gleich Null gesetzt werden. Die Addition dieser Polynome ist dann gegeben durch max(n,m) pn (x) + qm (x) = X (ai + bi )xi . (5.20) i=0 Die Multiplikation kann durch eine Faltung der Koeffizientenarrays realisiert werden: pn (x) · qm (x) = n+m X xi i=0 Die Potenz pn (x)k für k ∈ N wird üblicherweise mit k 2 (pn (x) 2 ) pn (x)k = pn (x) · pn (x)k−1 1 i X aj bi−j (5.21) j=0 Hilfe der Vorschrift für gerade k 6= 0 für ungerade k für k = 0 (5.22) berechnet, welche lediglich O(log k) Iterationen anstelle der O(k) Iterationen des trivialen Algorithmus ( pn (x) · pn (x)k−1 für k > 0 k pn (x) = (5.23) 1 für k = 0 33 5 Berechnung der Polynomkoeffizienten benötigt. Aufgrund ihrer Einfachheit kann die letztgenannte Berechnungsvorschrift für kleine k und Polynome von niedrigem Grad aber in der Praxis dennoch schneller sein. 5.3 Polynominterpolation Anhand von (n + 1) Wertepaaren (xi , fi ) mit xi , fi ∈ R, i ∈ {0, . . . , n} und paarweise verschiedenen xi lässt sich ein Polynom n X ak xk (5.24) pn (x) = k=0 vom Grad n bestimmen, welches die Wertepaare, auch Interpolationsstellen genannt, exakt interpoliert. Es gilt also pn (xi ) = n X ak xki = fi k=0 ∀i ∈ {0, . . . , n} (5.25) Das Interpolationspolynom pn (x) ist eindeutig, das heißt es existiert kein von pn (x) verschiedenes Polynom vom gleichen Grad, welches ebenfalls durch die gegebenen Wertepaare verläuft. pn (x) ist genau dann Lösung der gestellten Interpolationsaufgabe, wenn dessen Koeffizientenvektor (a0 , . . . , an ) Lösung des linearen Gleichungssystems 1 x0 x20 · · · xn0 a0 f0 .. .. .. .. .. .. = .. (5.26) . . . . . . . 1 xn x2n · · · xnn an fn | {z } Vn ist. Die Matrix Vn ist eine so genannte Vandermonde-Matrix, deren Determinante sich über die Formel n−1 n Y Y det(Vn ) = (xj − xi ) (5.27) i=0 j=i+1 berechnen lässt. Da angenommen wurde, dass die xi paarweise verschieden sind, ist diese Determinante niemals Null und die Matrix Vn regulär. Das Gleichungssystem ist somit eindeutig lösbar1 und das Interpolationspolynom durch die Koeffizienten ai eindeutig bestimmt (vgl. [EMNW05, S. 351f]). Derartige Vandermonde-Systeme sind häufig schlecht konditioniert, so dass kaum ein numerisches Verfahren die Koeffizienten akkurat bestimmen kann [PTVF96, S. 90ff, S. 120]. Die Verwendung dieser Methode beim Raycasting konnte die schlechte Konditionierung bestätigen (siehe Abschnitt 8.1.1). Nichtsdestotrotz werden nun Algorithmen erläutert, die die Berechnung der Koeffizienten bei der Transformation von F (x, y, z) nach f (t) mit Hilfe eines Interpolationpolynoms durchführen. Nimmt man an, dass der Grad der algebraischen Fläche bekannt ist, so kann man entlang des Strahls (n + 1) verschiedene Stützstellen ti mit ti ∈ R, i ∈ {0, . . . , n} wählen und damit (n + 1) Punkte (x(ti ), y(ti ), z(ti )) auf dem Strahl berechnen. Diese Punkte können in 1 Sind alle fi gleich Null, so wäre auch (a0 , . . . , an ) = (0, . . . , 0) Lösung des Gleichungssystems. Allerdings wäre das resultierende Interpolationspolynom nicht mehr vom Grad n. 34 5.3 Polynominterpolation f (t) pn (t) pn (t) (6,5) (6,5) (7,5) (6,5) (5,4) (5,4) (4,2.99) (4,2.9) (3,2) (4,2.99) (3,2) (2,1) (2,1) t (a) (2,1) t (b) (8,2.5) t (c) Abbildung 5.1: Sensibiltät der Polynominterpolation gegenüber Störungen: (a) zeigt den Verlauf des gesuchten Polynoms f (t) und für die Interpolation ungünstige Stützstellen, die fast auf einer Geraden liegen. Nimmt man nun wie in (b) an, dass die Interpolation durch Rechenfehler bei der Auswertung von f (t) an der Stützstelle t = 4 mit dem Wertepaar (4, 2.99) statt mit (4, 2.9) durchgeführt wird, so unterscheidet sich das resultierende Polynom deutlich von f (t). In (c) wurden bessere Stützstellen gewählt, die nicht zu einer Geraden degenerieren können. Das fehlerhaft berechnete Wertepaar (4, 2.99) hat kaum Einfluss auf das Ergebnis. die Flächenformel eingesetzt werden und man erhält (n + 1) Werte fi = F (x(ti ), y(ti ), z(ti )). Die Wertepaare (ti , fi ) dienen als Grundlage für die Interpolation. Da das Interpolationspolynom pn (t) das einzige Polynom ist, welches die berechneten Wertepaare interpoliert, muss es, analytisch betrachtet, mit dem gesuchten Polynom f (t) übereinstimmen. Durch ungünstige Wahl der Stützstellen und durch die begrenzte Genauigkeit von Fließkommaoperationen können die berechneten Koeffizienten jedoch von denen des gesuchten Polynoms abweichen und damit wichtige Eigenschaften wie beispielsweise die Lage und Existenz von Nullstellen verändern. Abbildung 5.1 verdeutlicht diese Problematik. In der Numerik werden zur Polynominterpolation in der Regel die so genannten Tschebyscheff-Knoten, die Nullstellen der Tschebyscheff-Polynome, verwendet, da diese den Interpolationsfehler gering halten. Die hier durchgeführte Transformation bestimmt jedoch die unbekannten Koeffizienten eines Polynoms durch Abtastung und anschließende Interpolation. Der theoretische Interpolationsfehler ist daher unabhängig von der Wahl der Stützstellen gleich Null. Lediglich die Rundungsfehler der Fließkommaberechnungen beeinflussen das Ergebnis. Auch wenn bereits einige Nachteile der Interpolationsmethode aufgezeigt wurden, besitzt sie dennoch den Vorteil der leichteren Implementierung. Die Funktionen, die das Interpolationspolynom berechnen, sind unabhängig von der konkreten Formel von F (x, y, z), da sie nur mit den Interpolationsstellen arbeiten. Setzt man voraus, dass auch der Gradient numerisch berechnet wird, unterscheiden sich die zu verschiedenen algebraischen Flächen gehörenden Fragment-Shader nur in der Funktion, welche die Wertepaare (ti , fi ) anhand der Flächenformel berechnet. Bei geeigneter Syntaxprüfung kann diese Formel direkt von einer Benutzereingabe übernommen werden. 5.3.1 Lagrange-Interpolation Es wurde gezeigt, dass das Interpolationspolynom vom Grad n durch (n + 1) Interpolationsstellen eindeutig bestimmt ist. Als erstes wird das Lagrange-Interpolationsverfahren zur 35 5 Berechnung der Polynomkoeffizienten Bestimmung dieses Polynoms erläutert. Die Lagrange-Interpolationformel ist durch pn (x) = n X j=0 n Y x − xk fj Lj (x) mit Lj (x) = xj − xk (5.28) k=0 k6=j gegeben. Es ist offensichtlich vom Grad n und interpoliert wie gewünscht die (xi , fi ): pn (xi ) = n X j=0 = = n X j=0 j6=i n X j=0 j6=i = n X j=0 j6=i n Y xi − xk fj xj − xk fj k=0 k6=j n Y k=0 k6=j (5.29) n Y xi − xk xi − xk + fi xj − xk xi − xk (5.30) k=0 k6=i fj · (xi − xi ) · n n Y Y xi − xk + fi 1 xj − xk k=0 k6=j k6=i n Y xi − xk + fi = fi fj · 0 · xj − xk (5.31) k=0 k6=i (5.32) k=0 k6=j k6=i Die Formel ist in der beschriebenen Form nicht nur zur Berechnung von Funktionswerten geeignet. Durch symbolisches Ausmultiplizieren der Produkte und anschließendes Aufsummieren kann das Polynom in die Monomdarstellung überführt werden, so dass die Koeffizienten ablesbar sind. Die Gesamtlaufzeit des Verfahrens beträgt O(n3 ). Sie kann aber durch die Umformung n Y (x − xk ) n n n X Y X fj x − xk k=0 · fj (5.33) = n Y xj − xk (x − xj ) j=0 j=0 k=0 (xj − xk ) k6=j k=0 k6=j Q leicht auf O(n2 ) reduziert werden. Das Produkt nk=0 (x−xk ) lässt sich in O(n2 ) auswerten. Es ist unabhängig von j und muss daher nur einmal berechnet werden. Für jedes j führt man nun in O(n) eine Polynomdivision mit (x − xj ) durch. Alle weiteren von j abhängigen Aufgaben benötigen ebenfalls O(n) Schritte, so dass sich ein Gesamtaufwand von O(n2 ) + n · O(n) = O(n2 ) ergibt. 5.3.2 Newton-Interpolation Weit verbreitet sind auch die Interpolationsformeln von Newton. Sie beruhen auf der Darstellung eines Polynoms in der Newton-Basis pn (x) = n X j=0 36 bj j−1 Y k=0 (x − xk ). (5.34) 5.3 Polynominterpolation f [x0 ] = f0 f [x0 , x1 ] = f [x1 ]−f [x0 ] x1 −x0 f [x1 ] = f1 f [x0 , x1 , x2 ] = f [x1 , x2 ] = f [x1 ,x2 ]−f [x0 ,x1 ] x2 −x0 f [x2 ]−f [x1 ] x2 −x1 f [x2 ] = f2 Abbildung 5.2: Rechenschema zur Bestimmung der dividierten Differenzen für drei Stützstellen. Alle Einträge außer die f [xi ] können aus den zwei benachbarten Tabelleneinträgen der vorigen Spalte in O(1) berechnet werden. Die unterstrichenen Einträge werden für die Newton-Interpolationsformel benötigt. Bei der Hinzunahme weiterer Stützstellen entsteht nur eine neue Zeile. Die bereits berechneten Werte können weiterverwendet werden. Unter der Interpolationsbedingung pn (xi ) = fi entsteht das lineare Gleichungssystem f0 = b0 (5.35) f1 = b0 + b1 (x1 − x0 ) (5.36) (5.37) f2 = b0 + b1 (x2 − x0 ) + b2 (x2 − x0 )(x2 − x1 ) .. . fn = b0 + b1 (x2 − x0 ) + b2 (x2 − x0 )(x2 − x1 ) + · · · + bn n−1 Y k=0 (xn − xk ) (5.38) zur Berechnung der bj . Die Koeffizientenmatrix des Gleichungsystems ist eine obere Dreiecksmatrix, wodurch sich die bj sukzessive berechnen lassen. Dazu werden die so genannten dividierten Differenzen f [xi , . . . , xi+j ] verwendet, die rekursiv definiert sind als f [xi ] = fi , i = 0, . . . , n (5.39) f [xi+1 , . . . , xi+j ] − f [xi , . . . , xi+j−1 ] f [xi , . . . , xi+j ] = , i = 0, . . . , n − 1, j = 1, . . . , n − i. xi+j − xi (5.40) Die Koeffizienten der Newton-Basis sind dann durch bj = f [x0 , . . . , xj ] gegeben und damit auch das Newtonsche Interpolationspolynom. pn (x) = n X j=0 f [x0 , . . . , xj ] j−1 Y (x − xk ) (5.41) k=0 Abbildung 5.2 veranschaulicht noch einmal das Rechenschema zur Bestimmung der benötigten dividierten Differenzen. Für eine ausführliche Herleitung des Zusammenhangs zwischen den bj und den dividierten Differenzen sei an dieser Stelle auf [Sto02, S. 48ff] verwiesen. Die Berechnung der Koeffizienten der Monombasis ist bei der Newton-Interpolation leicht zu realisieren. Zuerst werden die (n + 1) dividierten Differenzen f [x0 , . . . , xj ] berechnet, wofür O(n2 ) Rechenschritte benötigt werden. Wie in Abbildung 5.3 deutlich wird, kann die Berechnung in einem Array der Länge n + 1 durchgeführt werden. Nun folgt die Summation 37 5 Berechnung der Polynomkoeffizienten Initialisierung f [x0 ] f [x1 ] f [x2 ] f [x0 ] f [x0 x1 ] f [x1 x2 ] f [x0 ] f [x0 x1 ] f [x0 x1 x2 ] 1. Iteration 2. Iteration .. . Abbildung 5.3: Berechnung der (n + 1) dividierten Differenzen auf einem Array der Länge (n + 1): Nach der Initialisierung des Arrays mit den Stützwerten fi wird das Array in jeder Iteration von rechts nach links durchlaufen und man berechnet aus dem aktuellen und dem linken Nachbarwert die neue dividierte Differenz der aktuellen Arrayzelle. So wird sichergestellt, dass kein später noch benötigter Zwischenwert überschrieben wird. Nach jeder Iteration enthält das Array eine weitere der benötigten dividierten Differenzen (unterstrichen dargestellt). entsprechend Gleichung (5.41). Dafür benötigt man zwei weitere Arrays P und N der Länge (n + 1), die jeweils die Koeffizienten von Polynomen in der Monombasis repräsentieren. Qj−1 In Iteration j speichert P das Zwischenergebnis für pn (x) und N die Newton-Basis k=0 (x − xk ). Sie muss nicht in jeder Iteration neu berechnet werden, sondern kann in O(n) um ein weiteres Glied (x − xk ) erweitert werden. Die Einträge des Arrays N werden nun mit f [x0 , . . . , j] multipliziert auf P aufsummiert. Nach n solchen Iterationen enthält P die gesuchten Koeffizienten. Der Gesamtaufwand des Verfahrens beträgt daher O(n2 ). Obwohl die asymptotische Laufzeit mit der der Lagrange-Formeln übereinstimmt, ist die konkrete Laufzeit der Berechnung nach Newton geringer. 5.3.3 Weitere Interpolationsverfahren Außer den Interpolationsformeln von Lagrange und Netwon existieren noch weitere Verfahren, die jedoch im Rahmen dieser Arbeit nicht untersucht werden konnten. Numerisch günstige Eigenschaften werden beispielsweise der Tschebyscheff-Interpolation zugeschrieben [Boy02, Gie02]. Dabei werden die Koeffizienten des Interpolationspolynoms in der Tschebyscheff-Basis berechnet und anschließend in die Monombasis transformiert. Genau diese Transformation, die in ähnlicher Form auch schon bei den Verfahren von Lagrange und Newton durchgeführt wurde, kann, bedingt durch die schlechte Konditionierung der Vandermonde-Matrix, große Rundungsfehler erzeugen. Es wäre daher sinnvoll die Polynomnullstellen direkt in der Tschebyscheff-Basis zu berechnen, wie es in [GG83] beschrieben wird. Die meisten der im folgenden Kapitel dargelegten Algorithmen zur Nullstellenbestimmung arbeiten unabhängig von der Darstellung des Polynoms und können daher nahezu unverändert übernommen werden. 5.4 Zusammenfassung Es wurde dargelegt, wie aus der algebraischen Fläche F (x, y, z) = 0 durch Einsetzen des Raycasting-Strahls r(t) ein univariates Polynom f (t) entsteht und wie dessen Koeffizienten be- 38 5.4 Zusammenfassung rechnet werden können. Auf der CPU können mittels Termumformungen direkte Berechnungsvorschriften für jeden einzelnen Koeffizienten bestimmt werden. Da die entstehenden Terme meist sehr lang sind, empfiehlt sich die Verlagerung der Termumformungen auf die GPU, wo die Berechnungen nicht auf Variablen beruhen, sondern mit konkreten Werten durchgeführt werden können. Eine weitere, scheinbar vielversprechende Methode ist die Polynominterpolation. Sie kann die Koeffizienten anhand einiger Stützstellen und -werte ermitteln und benötigt daher nur wenig Wissen über den Aufbau der Flächenformel. Leider ist dieses Verfahren sehr empfindlich gegenüber Rundungsfehlern. 39 5 Berechnung der Polynomkoeffizienten 40 6 Berechnung der Nullstellen univariater Polynome Im vorigen Abschnitt wurde dargelegt, wie ein multivariates Polynom F (x, y, z) durch Einsetzen einer Strahlgleichung (x(t), y(t), z(t))T = o + td~ in das univariate Polynom f (t) transformiert werden kann. Es verbleibt die Berechung der Nullstellen von f (t), welche die Schnittpunkte der algebraischen Fläche F (x, y, z) = 0 mit dem Raycastingstrahl darstellen. Der erste Abschnitt dieses Kapitels beschäftigt sich mit allgemeinen Eigenschaften der Nullstellen von Polynomen sowie mit häufig benötigten Standardalgorithmen. Anschließend werden die Verfahren zur Berechnung von Polynomnullstellen detailliert erläutert. 6.1 Vorbemerkungen 6.1.1 Anzahl und Vielfachheit von Nullstellen Um die maximale Anzahl an Nullstellen, die ein Polynom vom Grad n besitzen kann, zu ermitteln, kann der Fundamentalsatz der Algebra herangezogen werden. Er besagt, dass jedes Polynom n X ai xi (6.1) pn (x) = i=0 vom Grad n ≥ 1 mit komplexen Koeffizienten ai eine komplexe Nullstelle besitzt [Chi00, S. 269ff]. Ist beispielsweise ξ1 ∈ C diese Nullstelle, so läßt sich der Linearfaktor (x − ξ1 ) mit Hilfe der Pseudodivision (siehe Abschnitt 6.1.2) von pn (x) abspalten [Chi00, S. 241f]. pn (x) = n X i=0 ai xi = (x − ξ1 ) n−1 X i=0 bi xi = (x − ξ1 )pn−1 (x) (6.2) Der Vorgang kann nun analog für pn−1 (x) fortgesetzt werden, so dass sich nach n Abspaltungen die so genannte Linearfaktorzerlegung von pn (x) ergibt. pn (x) = n X i=0 ai xi = an (x − ξ1 ) · · · (x − ξn ) (6.3) Jedes Polynom vom Grad n mit komplexen Koeffizienten hat also genau n komplexe Nullstellen. Am Beispiel von x3 − x2 + x − 1 = (x − 1)(x − i)(x + i) mit i2 = −1 (6.4) wird klar, dass Polynome mit reellen Koeffizienten ebenfalls komplexe Nullstellen besitzen können. Das Beispiel verdeutlicht eine weitere wichtige Eigenschaft der Polynome mit reellen ai . Ist ξ = a + bi mit a, b ∈ R eine Lösung von pn (x) = 0, so ist es auch deren konjugiert 41 6 Berechnung der Nullstellen univariater Polynome Komplexe ξ = a + bi = a − bi: pn (ξ) = n X i ai ξ = i=0 n X (6.5) ai ξ i = pn (ξ) = 0. i=0 Mehrfache Nullstellen Enthält die Linearfaktorzerlegung von pn (x) einen Linearfaktor (x − ξ) genau m–mal mit m > 0, so wird ξ als Nullstelle mit Vielfachheit m bezeichnet. Für m = 1 nennt man ξ eine einfache, für m > 1 eine mehrfache Nullstelle. Wie in [Mig92, S. 102] gezeigt wurde, ist ξ nun nicht nur Nullstelle von pn (x), sondern auch Nullstelle der ersten m − 1 Ableitungen. Es gilt also pn (ξ) = p′n (ξ) = . . . = p(m−1) (ξ) = 0 und p(m) (6.6) n n (ξ) 6= 0. In der Praxis und insbesondere bei technischen Anwendungen kommt es aufgrund von Rundungsfehlern in den Eingabedaten nur sehr selten vor, dass das Polynom sowie mehrere Ableitungen an der gleichen Stelle zu Null werden. Ist p′n (ξ) = 0 und |pn (ξ)| < ε, so wird aber meist angenommen, dass sich an der Stelle ξ eine mehrfache Nullstelle befindet. 6.1.2 Das Horner-Schema In [EMNW05, S. 92ff] wird das Horner-Schema zur schnellen und rundungfehlergünstigen Berechnung der Funktions- und Ableitungswerte eines Polynoms, sowie zum Abdividieren reeller und komplexer Nullstellen vorgestellt. Das Polynom pn (x) = an xn + an−1 xn−1 + · · · + a1 x + a0 (6.7) kann offensichtlich auch in der Form pn (x) = (· · · (( an x + an−1 ) x + an−2 ) x + · · · + a1 ) x + a0 |{z} | | | | bn {z } bn−1 =bn x+an−1 {z bn−2 =bn−1 x+an−2 {z b1 =b2 x+a1 {z } } b0 =b1 x+a0 (6.8) } mit den Zwischenergebnissen bn , . . . , b0 geschrieben werden. Durch diese Umformung benötigt man bei der Auswertung von pn (x) an einer Stelle ξ deutlich weniger Multiplikationen. Die Berechnung der bi für ein konkretes ξ kann mit Hilfe einer Tabelle geschehen, die als einfaches Horner-Schema bezeichnet wird: an an−1 +0 +bn ξ = bn = bn−1 an−2 ... a1 a0 +bn−1 ξ . . . +b2 ξ +b1 ξ = bn−2 ... = b1 (6.9) = b0 Es lässt sich zeigen, dass die bi , i = 1, . . . , n, in der dritten Zeile des einfachen Horner- 42 6.1 Vorbemerkungen Schemas gleichzeitig die Koeffizienten des Polynoms pn−1 (x) darstellen, dass durch Abdividieren der Nullstelle ξ von pn (x), also der Polynomdivision von pn (x) mit (x − ξ), entsteht. b0 entspricht dabei dem Divisionsrest. Sind alle Koeffizienten von pn (x) reell, so entstehen beim Abdividieren einer komplexen Nullstelle ξ auch komplexe Koeffizienten in pn−1 (x). Dies kann vermieden werden, indem die konjugiert komplexe Nullstelle ξ gleichzeitig abdividiert wird. Das Produkt (x − ξ)(x − ξ) = x2 − (ξ + ξ)x + ξ · ξ = x2 − px − q (6.10) mit reellen p und q bildet einen quadratischen Faktor von pn (x), so dass das Polynom pn−2 (x), welches aus der Polynomdivision von pn (x) mit x2 − px − q hervorgeht, ebenfalls nur reelle Koeffizienten besitzt. Das Nacheinanderausführen der Division durch (x − ξ) und (x − ξ) lässt sich, wie in [EMNW05, S. 95f] beschrieben, im doppelreihigen Horner-Schema zusammenfassen, welches ausschließlich reelle Arithmetik benötigt: an an−1 an−2 ... +0 +0 +cn q . . . +c4 q +c3 q +c2 q +0 +cn p = cn = cn−1 a2 a1 +cn−1 p . . . +c3 p +c2 p = cn−2 ... = c2 = c1 a0 (6.11) +0 = c0 Die ci , i = 2, . . . , n, in der vierten Zeile des doppelreihigen Horner-Schemas entsprechen den gesuchten Koeffizienten von pn−2 (x). c1 und c0 sind die Koeffizienten des Divisionsrestes c1 x + c0 und entsprechend gleich Null, wenn ξ Nullstelle von pn (x) ist. Die vorgestellten Divisionsverfahren mit linearem beziehungsweise quadratischem Divisor verwenden keinerlei Divisionen reeller oder komplexer Zahlen und werden daher als Pseudodivision oder auch als synthetische Division bezeichnet. Eine weitere Anwendung des Horner-Schemas ist die Berechnung von Ableitungswerten. Als Ausgangspunkt dient die aus der Pseudodivision von pn (x) mit (x − ξ) resultierende Schreibweise von pn (x): pn (x) = (x − ξ)pn−1 (x) + b0 (6.12) Durch Bildung der Ableitung entsteht p′n (x) = pn−1 (x) + (x − ξ)p′n−1 (x), (6.13) so dass man mit x = ξ eine alternative Vorschrift zur Berechnung von p′n (x) an der Stelle ξ erhält, die durch zweimalige Anwendung des Horner-Schemas realisiert werden kann: p′n (ξ) = pn−1 (ξ) + (ξ − ξ)p′n−1 (ξ) = pn−1 (ξ) (6.14) Analog lassen sich Berechnungsvorschriften für höhere Ableitungen herleiten. Es ergibt sich p(k) n (ξ) = k!pn−k (ξ), (6.15) wobei pn−k (x) durch Pseudodivision von pn−(k−1) (x) mit (x − ξ) bestimmt wird. 43 6 Berechnung der Nullstellen univariater Polynome 6.1.3 Polynomdivision Das einfache Horner-Schema ist zur euklidischen Division mit beliebigem Divisor ungeeignet. Die Lösung der Gleichung pn (x) = s(x)qm (x) + r(x) (6.16) bei gegebenem pn (x) und qm (x) mit m ≤ n, das heißt die Bestimmung des Quotienten s(x) und des Divisionsrestes r(x), kann jedoch leicht mit Hilfe des folgenden Algorithmus bestimmt werden: Zu Beginn des Algorithmus wird pn (x) als aktueller Rest r(x) angenommen. Der Grad von r(x) sei mit l bezeichnet. Nun teilt man den höchsten Koeffizienten von r(x) durch den höchsten Koeffizienten von qm (x). Der entstehende Quotient ist der Koeffizient von xl−m in s(x). Er wird mit qm (x) multipliziert und von r(x) subtrahiert, so dass ein neuer Rest r(x) entsteht, mit dem analog weitergerechnet wird bis l kleiner als m ist. 6.2 Lösungsformeln Einfache Polynomgleichungen bis zum Grad vier können durch geschlossene Formeln gelöst werden, welche lediglich endlich viele Rechenschritte benötigen, um bei exakter Arithmetik ein exaktes Ergebnis zu liefern. Dabei werden ausschließlich die vier Grundrechenarten und Wurzeloperationen verwendet. Bereits 1600 v. Chr. waren die Babylonier in der Lage einfache quadratische Gleichungen auf diese Art zu lösen. Die vollständige Lösungsformel wird auf den indischen Mathematiker und Astronomen Aryabatthiya (5. Jahrhundert) zurückgeführt. Zu Beginn des 16. Jahrhunderts leistete Scipione del Ferro den wesentlichen Beitrag für die Lösung kubischer Gleichungen. Er zeigte, wie Gleichungen der Form x3 + ax + b = 0 aufgelöst werden können. Niccolò Tartaglia erweiterte die Berechnungsvorschrift auf alle Gleichungen vom Grad drei und übersandte seine Entdeckung dem italienische Mathematiker Gerolamo Cardano, den er zur Geheimhaltung verpflichtete. Als Cardanos Schüler, Lodovico Ferrari, einige Jahre später die Lösungsformel für biquadratische Gleichungen entwickelte, beschloss Cardano 1545 beide Methoden in seinem Werk Ars magna de Regulis Algebraicis zu veröffentlichen. Tartaglia brach daraufhin sämtlichen Kontakt zu Cardano ab. Quelle: [Mat78, S. 288, S. 367f, S. 544f] In den darauffolgenden Jahren stellten sich viele Mathematiker die Frage nach der Existenz ähnlicher Lösungsformeln für Gleichungen höheren Grades. Wie Niels Henrik Abel 1824 in [Abe26] bewies, kann es solche Formeln jedoch nicht geben. Im Folgenden werden die Lösungsformeln für lineare, quadratische, kubische und biquadratische Gleichungen beschrieben. Die in der betreffenden Literatur häufig verwendete Bezeichnung der Polynomkoeffizienten mit den Buchstaben a bis d wird hier übernommen. In allen Berechnungen wird davon ausgegangen, dass sowohl der höchste als auch der niedrigste Koeffizient von Null verschieden sind. 6.2.1 Lineare Gleichungen Die Auflösung der linearen Polynomgleichung ax+b = 0 sei hier nur der Vollständigkeit wegen erwähnt und geschieht trivialerweise nach b x1 = − . a 44 (6.17) 6.2 Lösungsformeln 6.2.2 Quadratische Gleichungen Die Lösungen der quadratrischen Polynomgleichung ax2 + bx + c = 0 können durch die Formel q b 2 − ac −b ± 2 (6.18) x1,2 = 2a oder auch durch x1,2 = −b ± 2c q b 2 2 (6.19) − ac berechnet werden [PTVF96, S. 183f]. Beide Lösungsformeln haben ihre Berechtigung. Jedoch entstehen numerische Instabilitäten, sobald a, c oder beide Koeffizienten √ klein sind. Der Term √ 2 b − 4ac reduziert sich dann annähernd auf b, was den Term −b ± b2 − 4ac für eine der beiden Nullstellen nahezu auslöscht. Die betreffende Nullstelle kann auf diesem Wege nicht mehr genau bestimmt werden. Daher wird in [PTVF96, S. 183f] vorgeschlagen beide Methoden wie folgt zu kombinieren. Man setzt p q = −b − sgn(b) b2 − 4ac (6.20) und berechnet x1 mit (6.18) und x2 mit (6.19): x1 = q 2a x2 = 2c q (6.21) Gleichung (6.21) nutzt somit die Vorteile beider Verfahren aus, um eine in den meisten Fällen stabile Lösungsformel für quadratische Gleichungen zu synthetisieren. 6.2.3 Kubische Gleichungen Die Lösung kubischer Gleichungen wird in [MMWW99, S. 12] wie folgt vorgenommen. Die normierte kubische Gleichung x3 + ax2 + bx + c = 0 wird durch die Substitution x = y − a3 in 2 3 die reduzierte Form y 3 + py + q = 0 überführt. Dabei ist p = 3b−a und q = 2a − ab 3 27 3 + c. Nun p 3 q 2 kann man aus der Diskriminante D = 3 + 2 der reduzierten Form drei Fälle ableiten: D>0 eine reelle, zwei zueinander konjugiert komplexe Nullstellen D=0 drei reelle Nullstellen, davon mindestens zwei gleiche Lösungen D<0 drei paarweise verschiedene reelle Nullstellen Die nach Cardano benannten Formeln liefern die Lösungen für alle drei Fälle. Dazu setzt man r √ p 1 3 u = − q + D und v = − . (6.22) 2 3u 45 6 Berechnung der Nullstellen univariater Polynome und berechnet y1 = u + v 1 y2 = − (u + v) + 2 1 y3 = − (u + v) − 2 (6.23) √ 1 (u − v) 3i 2 √ 1 (u − v) 3i. 2 (6.24) (6.25) Aus den yk der reduzierten Form erhält man leicht die Lösungen der Normalform: xk = yk − a 3 (6.26) Die obigen Formeln haben einen gewissen Nachteil für den Fall D < 0. Obwohl alle √ drei Nullstellen reell sind, muss man dennoch mit komplexen Zwischenwerten rechnen, da D nicht reell ist. Für diesen Fall existiert jedoch eine alternative Vorschrift, welche erstmals 1615 in De Emendatione Aequationum von François Viete erschien. Man berechnet r q p 3 und ϕ = arccos − . (6.27) r= − 3 2r Unter Verwendung von D < 0 lässt sich durch Umstellen der Gleichung für die Diskriminante 3 q verifizieren, dass − p3 im Intervall [0, ∞) und − 2r im Intervall [−1, 1] liegt. Damit sind r und ϕ stets definiert und reell. Für r wird jeweils die positive der beiden möglichen Wurzeln verwendet. Die yk erhält man nun aus ϕ √ y1 = 2 3 r cos (6.28) 3 √ ϕ + 2π (6.29) y2 = 2 3 r cos 3 √ ϕ + 4π . (6.30) y3 = 2 3 r cos 3 6.2.4 Biquadratische Gleichungen Wie in [MMWW99, S. 13] beschrieben wird, kann ausgehend von der normalisierten Form x4 + ax3 + bx2 + cx + d = 0 die reduzierte Form y 4 + py 2 + qy + r = 0 durch die Substitution x = y − a4 gewonnen werden, wobei 3 p = b − a2 , 8 q =c− ab a3 + 2 8 und r = d − ac a2 b 3a4 + − . 4 16 256 (6.31) Aus der reduzierten Form leitet sich die so genannte kubische Resolvante ab: z 3 + 2pz 2 + (p2 − 4r)z − q 2 = 0 46 (6.32) 6.3 Lokal konvergente Iterationsverfahren Sind z1 , z2 , z3 die mit Hilfe der Cardanischen Formeln ermittelten Lösungen der kubischen Resolvante, so berechnet man w1 als eine Lösung von w2 = z1 , (6.33) 2 w2 als eine Lösung von w = z2 und setzt q w3 = − . w1 w2 (6.34) (6.35) Die wk setzen sich nun wie folgt zu den Lösungen der reduzierten Form zusammen: y1 = (+w1 + w2 + w3 )/2 (6.36) y2 = (+w1 − w2 − w3 )/2 (6.37) y4 = (−w1 − w2 + w3 )/2 (6.39) (6.38) y3 = (−w1 + w2 − w3 )/2 Für die biquadratische Gleichung können die Nullstellen nun mit xk = yk − a4 bestimmt werden. Aus den Lösungen der kubischen Resolvante lässt sich hierbei auch der Anteil reeller beziehungsweise komplexer Lösungen an der Lösungsmenge der biquadratischen Gleichung ablesen: kubische Resolvente alle Nullstellen reell und positiv alle Nullstellen reell, eine positiv, zwei negativ eine Nullstellen reell, zwei komplex biquadratische Gleichung → → → vier reelle Nullstellen vier komplexe Nullstellen zwei reelle, zwei komplexe Nullstellen 6.3 Lokal konvergente Iterationsverfahren Zur numerischen Berechnung von Nullstellen einer Funktion ist eine Vielzahl von iterativen Verfahren bekannt, deren Konvergenz nur dann garantiert werden kann, wenn sich die Startwerte der Iteration bereits nahe genug an einer Nullstelle befinden beziehungsweise gewisse Zusatzkriterien erfüllen. Man bezeichnet dies als lokale Konvergenz. Eine lokal konvergente Methode ist für sich gesehen selten zur Berechnung aller Nullstellen einer Funktion geeignet. In den meisten Fällen benutzt man zuvor ein Verfahren, welches eine Nullstelle so gut annähert oder isoliert, dass die Bedingungen für lokale Konvergenz erfüllt sind. Mit den ermittelten Startwerten wird dann auf eine lokal konvergente Methode umgeschalten, die im Allgemeinen eine höhere Konvergenzgeschwindigkeit aufweist als die zur Vorberechnung der Startwerte genutzten Algorithmen. 6.3.1 Newton-Iteration Die Newton-Iteration ist ein wichtiges und bekanntes Verfahren zur numerischen Lösung von nichtlinearen Gleichungen und Gleichungssystemen. Für eine stetig differenzierbare Funktion f : R → R und eine bereits bekannte Näherung xk einer Nullstelle konstruiert das NewtonVerfahren im Punkt (xk , f (xk )) eine Tangente an den Graphen von f und verwendet die Nullstelle dieser Tangente als neue Näherung xk+1 . Aus der Gleichung der Tangente y = f ′ (xk )(x − xk ) + f (xk ) (6.40) 47 6 Berechnung der Nullstellen univariater Polynome folgt mit y = 0 und x = xk+1 die Iterationsvorschrift des Newton-Verfahrens: xk+1 = xk − f (xk ) f ′ (xk ) (6.41) Es kann gezeigt werden, dass die Newton-Iteration für hinreichend nahe an einer einfachen Nullstelle gelegene Startwerte x0 quadratisch konvergiert. In der Nähe einer mehrfachen Nullstelle konvergiert das Verfahren nur linear. Eine Herleitung der Konvergenzbereiche und -ordnungen findet sich beispielsweise in [EMNW05, S. 51ff]. Leider konnte die Newton-Iteration in dieser Arbeit nicht verwendet werden, da keines der untersuchten global konvergenten Verfahren geeignete Startwerte für die Iteration liefert und die Konvergenz somit nicht garantiert werden kann. 6.3.2 Einschlussverfahren Als Einschlussverfahren bezeichnet man Algorithmen, welche eine Folge von geschachtelten Intervallen erzeugen, die gegen eine Nullstelle konvergieren. Ausgangspunkt dieser Verfahren bildet der so genannte Zwischenwertsatz von Bolzano. Er besagt, dass eine reelle Funktion f : R → R, die auf einem abgeschlossenen Intervall [a, b] stetig ist, jeden Funktionswert zwischen f (a) und f (b) annimmt [Heu00, S. 223]. Haben f (a) und f (b) verschiedene Vorzeichen, das heißt es gilt f (a)f (b) < 0, so muss es in (a, b) zwangsläufig eine Nullstelle der Funktion f geben. Es lässt sich leicht herleiten, dass das Intervall sogar eine ungerade Anzahl von Nullstellen enthalten muss. Ausgehend von dem Einschlussintervall [a, b] werden die Startwerte x1 = a, f1 = f (a), x2 = b und f2 = f (b) gesetzt, so dass die Eigenschaft f1 f2 < 0 erfüllt ist. Die Einschlussverfahren berechnen nun ein x3 , welches zwischen x1 und x2 liegt, und teilen damit das Einschlussintervall. Befindet sich die gesuchte Nullstelle nicht bei x3 , so muss eines der beiden Teilintervalle den Vorzeichenwechsel enthalten. Gilt f2 f3 < 0 mit f3 = f (x3 ), so liegt die Nullstelle zwischen x2 und x3 und die Intervallgrenzen und Funktionswerte werden wie folgt umbenannt: x1 := x2 f1 := f2 (6.42) x2 := x3 f2 := f3 (6.43) Ist hingegen f2 f3 > 0, so befindet sich die Nullstelle zwischen x1 und x3 und es wird x2 := x3 f2 := f3 (6.44) gesetzt. Da alle Intervalle dieser Folge per Konstruktion eine Nullstelle enthalten, ist die Konvergenz von Einschlussverfahren garantiert. Die einzelnen Algorithmen unterscheiden sich vorrangig in der Wahl eines geeigneten x3 und damit auch in der Konvergenzgeschwindigkeit. Die Größe |x2 −x1 | des Einschlussintervalls gibt gleichzeitig den maximalen absoluten Fehler an. Er erlaubt eine Aussage über die Anzahl der korrekten Kommastellen des Ergebnisses. 1 Eine bessere Abschätzung kann jedoch über den relativen Fehler | x2x−x | gemacht werden, da 2 dieser die insgesamt gültigen Stellen berücksichtigt. Als Abbruchkriterium verwendet man also wahlweise x2 − x1 < ε. (6.45) |x2 − x1 | < ε oder x2 48 6.3 Lokal konvergente Iterationsverfahren f (x) x 5 4 3 2 1 Iterationen Abbildung 6.1: Beispielhafte Ausführung des Bisektionsverfahrens. Die Größe des Einschlussintervalls halbiert sich mit jeder Iteration. Bisektion Das Bisektionsverfahren ist das einfachste und bekannteste Einschlussverfahren. Es wählt x3 = x1 + x2 2 (6.46) und teilt damit das Einschlussintervall in der Mitte (Abbildung 6.1). Obwohl das Bisektionsverfahren nur linear gegen die Nullstelle konvergiert, hat es doch den Vorteil der geringsten Zusatzkosten pro Iteration. Übersteigt der Aufwand, der bei Verfahren mit hoher Konvergenzordnung in die Berechnung der ck investiert wird, die Kosten zur Auswertung von f , so lohnt sich möglicherweise die Verwendung des Bisektionsverfahrens. Regula-Falsi und verwandte Verfahren Die Regula-Falsi verwendet den Schnittpunkt der x-Achse mit der Geraden, die die Punkte (x1 , f1 ) und (x2 , f2 ) verbindet, als Näherung für die Nullstelle von f (Abbildung 6.2). Aus der Geradengleichung f1 − f2 (x − x1 ) (6.47) y = f1 + x1 − x2 lässt sich mit y = 0 die Berechnungsvorschrift für x3 herleiten: x3 = x = x1 − f1 (x1 − x2 ) f1 − f2 (6.48) Analytisch betrachtet liegt die neue Näherung x3 zwischen x1 und x2 , da man aus der Bedin1 gung f1 f2 < 0 auch 0 < f1f−f < 1 folgern kann. Testimplementierungen auf der GPU ergaben 2 aber, dass x3 bedingt durch die geringe Rechengenauigkeit durchaus das Einschlussintervall verlassen kann. Insbesondere bei schlecht separierten Nullstellen, wie sie in der Nähe von Singularitäten algebraischer Flächen auftreten, bewirkt dies häufig die Konvergenz zu einer anderen Nullstelle außerhalb des Startintervalls. Stellt man fest, dass x3 nicht zwischen x1 und x2 oder zu nahe an den Intervallgrenzen liegt, so könnte man stattdessen einen Bisekti- 49 6 Berechnung der Nullstellen univariater Polynome f (x) x 3 2 1 Iterationen Abbildung 6.2: Beispielhafte Ausführung der Regula-Falsi. Durch die Randpunkte des Graphen im aktuellen Einschlussintervall wird eine Gerade gelegt. Das Einschlussintervall wird am Schnittpunkt dieser Geraden mit der x-Achse geteilt. onsschritt durchführen. Der dadurch entstehende Zusatzaufwand verlangsamt die Konvergenz des Verfahrens jedoch deutlich. In [EMNW05, S. 74ff] werden zur Steigerung der Konvergenzgeschwindigkeit zahlreiche Variationen der Regula-Falsi vorgeschlagen. Das Pegasus-, das Illinois- und das Anderson-BjörkVerfahren wurden in der OpenGL-Shading-Language implementiert und ebenfalls getestet. Sie zeigten jedoch ein ähnlich instabiles Verhalten bei schlecht separierten Nullstellen und eine für Echtzeitansprüche ungenügende Konvergenzgeschwindigkeit, die sich wahrscheinlich auf die in Abschnitt 2.1.3 beschriebene Behandlung von Programmverzweigungen und Schleifen zurückführen lässt. Aus den genannten Gründen wurden diese Algorithmen nicht näher untersucht. 6.4 Nullstellenisolation Ziel der in diesem Abschnitt vorgestellten Algorithmen ist die Bestimmung von Intervallen, in denen ein Polynom pn (x) mindestens eine reelle Nullstelle besitzt. Einige Verfahren sind selbstständig in der Lage die Intervalle weiter zu verkleinern, bis die Nullstellen vollständig separiert und hinreichend gut angenähert sind, während andere die Intervalle so wählen, dass lokal konvergente Iterationsverfahren darauf angewandt werden können. 6.4.1 Das D-Chain-Verfahren Zwischen zwei benachbarten Nullstellen der ersten Ableitung eines Polynoms pn (x) verhält sich dieses streng monoton, das heißt es gilt entweder p′n (x) < 0 oder p′n (x) > 0. Kennt man also zwei benachbarte Nullstellen a und b von p′n (x) und gilt zusätzlich pn (a)pn (b) < 0, so hat pn (x) nach dem Zwischenwertsatz (siehe Abschnitt 6.3.2) und aufgrund der Monotonie in (a, b) genau eine reelle (gegebenenfalls mehrfache) Nullstelle. [a, b] erfüllt gleichzeitig die Voraussetzungen zur Anwendung eines Einschlussverfahrens, so dass die weitere Eingrenzung der Nullstelle kein Problem darstellt. Die Berechnung der Nullstellen von pn (x) lässt sich also auf die Berechnung der Nullstellen der ersten Ableitung p′n (x) 50 6.4 Nullstellenisolation zurückführen. Da p′n (x) wiederum ein Polynom ist, kann der Algorithmus rekursiv angewandt (k) werden1 . Der Grad der k-ten Ableitung pn (x) ist dabei (n − k), so dass nach k = n − 1 Ableitungen ein Polynom vom Grad eins entsteht, dessen Nullstelle sich leicht bestimmen lässt. Es wäre natürlich auch möglich, Polynome vom Grad zwei, drei oder vier als Basisfälle anzusehen. Allerdings sind die mit Hilfe der Lösungsformeln (Abschnitt 6.2) berechneten Nullstellen häufig unpräzise und daher für eine weitere Verwendung in diesem rekursiven Algorithmus ungeeignet. (k) In der bisher beschriebenen Form kann das Verfahren keine Nullstellen von pn bestimmen, (k+1) die vor der kleinsten beziehungsweise nach der größten Nullstelle der Ableitung pn (x) (k+1) auftreten. Sind die Nullstellen von pn (x) der Größe nach als ζ0 , . . . , ζm mit m ≤ n − k − 2 bezeichnet, so ist der Algorithmus für die Intervalle [ζi , ζi+1 ] klar. Die Randintervalle [−∞, ζ0 ] und [ζm , +∞] müssen jedoch gesondert behandelt werden, um feste Intervallgrenzen für die Einschlussverfahren zu ermitteln. Da beim Raycasting die algebraische Fläche in der Regel geclippt wird und daher nur Schnittpunkte in einem gewissen Bereich des Strahls von Interesse sind, kann man auch die Nullstellensuche von Vornherein auf ein Intervall [l, u] eingrenzen, wobei l die untere und u die obere Clippinggrenze darstellt. Ist nun ζl die kleinste Nullstelle von (k+1) pn (x) mit ζl > l so kann man [l, ζl ] als unteres Randintervall verwenden. Alle Nullstellen unterhalb von l bleiben somit unberücksichtigt. Analog lässt sich ein maximales ζu mit ζu < u und ein entsprechendes oberes Randintervall [ζu , u] finden. 6.4.2 Der Algorithmus von Sturm Mit Hilfe des Sturmschen Theorems lässt sich die Anzahl voneinander verschiedener reeller Nullstellen eines Polynoms pn (x) in einem Intervall [a, b] bestimmen. Dazu wird eine Folge von Polynomen f0 , f1 , . . . , fn fallenden Grades mit f0 = pn (x), f1 = p′n (x), und fi = −fi−2 mod fi−1 , i = 2, . . . , n (6.49) verwendet, die eine so genannte Sturmsche Kette bildet. Die Anzahl Vorzeichenwechsel V (x) in der Kette für eine bestimmte Stelle x lässt sich bestimmen, indem man zunächst alle Werte mit fk (x) = 0 aus der Kette streicht und anschließend abzählt, wie oft aufeinander folgende Terme verschiedene Vorzeichen haben. Das Theorem von Sturm besagt nun, dass die Anzahl verschiedener reeller Nullstellen r(a, b) von pn (x) in [a, b] durch r(a, b) = V (a) − V (b) (6.50) gegeben ist. Eine detaillierte Herleitung des Theorems von Sturm ist in [Sto02, S. 377ff], aber auch in [Mig92, S. 193ff] zu finden. Die vorgestellte Abschätzung der reellen Nullstellen in einem Intervall [a, b] erlaubt in einfacher Weise die iterative Berechnung der kleinsten reellen Nullstelle dieses Intervalls. Enthält [a, b] mindestens eine Nullstelle, das heißt es gilt r(a, b) > 0, so startet man die Iteration mit a0 = a und b0 = b und konstruiert anschließend eine Folge von Intervallen, die gegeben ist 1 Der rekursive Algorithmus berechnet die Nullstellen eines Polynoms, indem die Nullstellen aller Ableitungen des Polynoms berechnet werden. Die Bezeichnung D-Chain resultiert aus dieser „Kette von Ableitungen“ (engl. chain of derivatives). 51 6 Berechnung der Nullstellen univariater Polynome durch [ak+1 , bk+1 ] = ( [ak , ck ] r(ak , ck ) > 0 [ck , bk ] sonst mit ck = ak + bk . 2 (6.51) Das Intervall [ak , bk ] wird also in der Mitte geteilt und für das vordere Teilintervall [ak , ck ] die Anzahl an Nullstellen bestimmt. Befinden sich in [ak , ck ] noch Nullstellen, so wird die Iteration damit fortgesetzt. Andernfalls wird das Intervall [ck , bk ] untersucht. Es gibt zwei Möglichkeiten die Iteration zu beenden. Die erste Möglichkeit verkleinert das Intervall solange bis es nur noch eine Nullstelle enthält (r(ak , bk ) = 1) und die Intervallgröße |bk − ak | eine gewisse Größe ε unterschreitet. Um eine schnellere Konvergenz zu erzielen, sollte man jedoch zusätzlich das Abbruchkriterium f (ak )f (bk ) < 0 verwenden und gegebenfalls ein Einschlussverfahren wie beispielsweise das Bisektionsverfahren anschließen. Beim Raycasting hat der Algorithmus von Sturm einen Vorteil, der sich besonders bei Flächen höheren Grades bemerkbar macht. Viele Verfahren müssen im schlechtesten Fall alle n Nullstellen des Polynoms pn (x) berechnen, bis feststeht, welches die kleinste Nullstelle im Intervall [a, b] ist. Der soeben beschriebene Algorithmus berechnet unabhängig vom Grad des Polynoms nur eine einzige Nullstelle. Dennoch kann er leicht auf die Berechnung aller Nullstellen in [a, b] erweitert werden, indem man jeweils die kleinste Nullstelle ξ berechnet und dann mit dem Restintervall [ξ, b] genauso verfährt. 6.4.3 Wertebereichanalyse Können von einer Funktion f : R → R sowohl eine untere Schranke F als auch eine obere Schranke F innerhalb eines Intervalls [a, b] berechnet oder zumindest gut genug abgeschätzt werden, so lässt sich ein Bisektionsalgorithmus konstruieren, der die kleinste Nullstelle in diesem Intervall bestimmt. Gilt F ≤ 0 ≤ F auf [a, b], dann besteht die Möglichkeit, dass f in [a, b] eine Nullstelle besitzt. Man teilt dann [a, b] in der Mitte und untersucht die beiden entstea+b henden Intervalle [a, a+b 2 ] und [ 2 , b] rekursiv mit dem gleichen Algorithmus bis das Intervall hinreichend verkleinert wurde und man annehmen kann, dass man eine Nullstelle gefunden hat. Gilt F ≤ 0 ≤ F jedoch nicht, so verläuft der Graph von f in [a, b] gänzlich oberhalb oder unterhalb der Achse und man kann sicher sein, dass in diesem Zweig der Bisektion keine Nullstelle zu finden ist. Da die OpenGL-Shading-Language nicht mit den hierfür benötigten rekursiven Funktionsaufrufen umgehen kann, muss der beschriebene Algorithmus leicht abgewandelt werden. Sei [a, b] das Startintervall, in dem nach einer Nullstelle gesucht werden soll, [c, d] das gerade betrachtete Teilintervall und e = c+d 2 die Mitte des Intervalls [c, d]. Nach der Teilung von [c, d] berechnet man zuerst die obere Schranke F [c,e] und die untere Schranke F [c,e] von f in [c, e]. Gilt nun F [c,e] ≤ 0 ≤ F [c,e] , so wird [c, e] weiter verfeinert. Andernfalls wird nicht [e, d] sondern [e, b] näher untersucht. Beim rekursiven Algorithmus musste für jede Rekursionsstufe jeweils die aktuelle obere und untere Intervallgrenze auf den Stack gelegt werden. Dies ist nun nicht mehr nötig, wodurch sich ein iterativer, in der OpenGL-Shading-Language implementierbarer Algorithmus ergibt. Intervallarithmetik In jeder Iteration des Algorithmus müssen F und F , also der Wertebereich von f in einem Intervall, berechnet werden. Dazu kann man die so genannte Intervallarithmetik verwenden, wie sie in [Kea96] beschrieben wird. Sie definiert Arithmetik nicht auf reellen Zahlen, sondern 52 6.4 Nullstellenisolation auf reellen Intervallen. Sind x = [x, x] und y = [y, y] solche Intervalle, dann sind die drei algebraischen Operationen +, − und × für die idealisierte Intervallarithmetik gegeben durch x ⊙ y = {x ⊙ y : x ∈ x ∧ y ∈ y} für ⊙ ∈ {+, −, ×}. (6.52) Für die praktische Realisierung ist aber die folgende Definition nützlicher: x + y = [x + y, x + y] (6.53) x − y = [x − y, x − y] (6.54) x × y = [min{xy, xy, xy, xy}, max{xy, xy, xy, xy}] (6.55) Operationen zwischen einer reellen Zahl x und einem Intervall lassen sich leicht hinzufügen, wenn man für x das Intervall x = [x, x] verwendet. Das Ergebnis einer Operation ⊙ ∈ {+, −, ×} auf reellen Zahlen x ∈ x und y ∈ y liegt nun garantiert im Intervall x⊙y. Diese fundamentale Eigenschaft macht man sich beispielsweise in der Numerik für die Abschätzung von Rundungsfehlern zunutze. An dieser Stelle ist aber die Abschätzung des Wertebereichs eines Polynoms f auf [a, b] von größerer Bedeutung. Dazu kann man f auf [a, b] anwenden und dabei alle Berechnungen bei der Auswertung von f auf Intervallen durchführen. Für jedes x ∈ [a, b] liegt f (x) in f ([a, b]). Soll zum Beispiel für f (x) = x(x − 1) der Wertebereich auf [0, 1] abgeschätzt werden, so gilt f ([0, 1]) ⊆ [0, 1]([0, 1] − 1) = [0, 1][−1, 0] = [−1, 0]. (6.56) Anhand des exakten Wertebereichs von [−0.25, 0] wird bereits klar, dass diese Methode zu einer Überschätzung neigt. Um die Überschätzung zu verringern, kann zur Bestimmung des Wertebereichs von Polynomen beispielsweise die rekursive Taylor-Methode verwendet werden, die im nächsten Abschnitt erläutert wird. Zuvor soll jedoch die praktische Realisierung der Intervalloperationen beschrieben werden. Computerarithmetik ist keineswegs exakt, sondern unterliegt aufgrund der begrenzten Stellenanzahl Rundungsfehlern. Um sicherzustellen, dass das Ergebnis einer Intervalloperation auch sämtliche möglichen Werte einschließt, muss bei der Berechnung der unteren Intervallgrenze des Ergebnisses abgerundet und entsprechend bei der oberen Grenze aufgerundet werden. Diese Art der Rundung ist in jedem IEEE-754-kompatiblen Fließkommaprozessor verfügbar. Leider ist die Rundungsart in der OpenGL-Shading-Language weder beeinflussbar noch bekannt, so dass eine korrekte Implementierung der Intervallarithmetik nicht ohne weiteres möglich ist. Daher wurden die Intervalloperationen – in der Hoffnung, dass derartige Rundungsfehler beim Raycasting algebraischer Flächen nur einen geringen Einfluss haben – in der Shading-Language ohne spezielle Rundung implementiert. Rekursive Taylor-Methode In [SSS+ 06] wird die so genannte rekursive Taylor-Methode zum Raycasting algebraischer Flächen eingesetzt. Andere, ebenfalls in [SSS+ 06] getestete Methoden, wie zum Beispiel affine Arithmetik, benötigten im Mittel mehr Iterationen und längere Renderingzeiten als die rekursive Taylor-Methode. Dies lässt auf eine genauere Bestimmung der Wertebereiche schließen. Daher soll hier lediglich die rekursive Taylor-Methode erläutert werden. Um nun also den Wertebereich von f (x) auf [x, x] zu bestimmen, wird f am Mittelpunkt 53 6 Berechnung der Nullstellen univariater Polynome x0 des Intervalls [x, x] mit Hilfe der Taylorschen Formel expandiert: f (x) = f (x0 ) + hf ′ (x0 ) + 12 h2 f ′′ (x0 + ϑh) (6.57) Dabei gilt x ∈ [x, x], x0 = x+x 2 , x−x 0 < ϑ < 1 und h = x − x0 ∈ [− x−x 2 , 2 ]= x−x 2 [−1, 1]. (6.58) Nimmt man an, dass der Wertebereich [G, G] der zweiten Ableitung f ′′ (x) im Intervall [x, x] bekannt ist, so lässt sich der Wertebereich [F , F ] von f in [x, x] als [F , F ] = f (x0 ) + x−x ′ 2 [−1, 1]f (x0 ) ausdrücken. Mit der Substitution x1 = einfachen: x−x 2 + 1 2 x−x 2 [−1, 1] 2 [G, G] kann man die Gleichung zudem noch etwas ver- [F , F ] = f (x0 ) + x1 f ′ (x0 )[−1, 1] + 12 x21 [−1, 1]2 [G, G] = f (x0 ) + (6.59) x1 f ′ (x0 )[−1, 1] + 1 2 2 x1 [0, 1][G, G] (6.60) (6.61) Da die zweite Ableitung f ′′ (x) wieder ein Polynom ist, kann [G, G] auf die gleiche Weise berechnet werden. Die sukzessive Differentiation eines Polynoms f vom Grad n endet in einer konstanten Funktion f (n) (x) = c. Der Wertebereich dieser Funktion im Intervall [x, x] ist offensichtlich [c, c]. 6.5 Global konvergente Iterationsverfahren Bei der Nullstellenberechnung von Polynomen sind in der Praxis besonders solche Verfahren von Interesse, die für eine große Anzahl von Polynomen pn (x) und ohne Kenntnis von Einschlussintervallen oder Startwerten zur einer Nullstelle konvergieren. Die so ermittelte Nullstelle kann abdividiert werden und der Algorithmus erneut auf das verbleibende Polynom angewendet werden. Konvergiert ein derartiges Verfahren bei einem Polynom mit reellen Koeffizienten zu einer komplexen Nullstelle ξ, dann kann, wie in Abschnitt 6.1.2 beschrieben, der quadratische Faktor ξ · ξ abgespalten werden, so dass wieder ein Polynom mit ausschließlich reellen Koeffizienten entsteht. Das Abdividieren einer ungenau bestimmten Nullstelle verändert meist die Lage der verbleibenden Nullstellen, so dass Algorithmen zu bevorzugen sind, welche tendenziell die betragsmäßig kleinste Nullstelle liefern, die sich in vielen Fällen am genauesten bestimmen lässt. Da beim Raycasting nur die reellen Nullstellen benötigt werden, kann die Berechnung der komplexen Nullstellen und die damit verbundene komplexe Arithmetik außerdem einen erheblichen Zusatzaufwand zur Folge haben. Nachfolgend werden die Muller- und die Laguerre-Methode beschrieben. Darüberhinaus existieren noch andere global konvergente Iterationsverfahren wie zum Beispiel das Verfahren von Jenkins und Traub [JT70] und der QR-Algorithmus [Wat82], die aber aufgrund ihrer Komplexität für eine GPU-basierte Implementierung ungeeignet erscheinen und daher nicht im Detail untersucht wurden. 54 6.5 Global konvergente Iterationsverfahren 6.5.1 Die Muller-Methode Die Methode von Muller [Mul56] interpoliert pn (x) an drei bekannten Näherungen xi , xi−1 und xi−2 mit Hilfe einer Parabel ϕ(x). Eine der beiden Lösungen der Parabelgleichung ϕ(x) = 0 wird als neue Näherung xi+1 verwendet. Die Iteration arbeitet mit komplexen Zahlen, da die Nullstellen der Parabel komplex sein können. Aus der Lagrangeschen Interpolationsformel für die Punkte (xi−2 , pn (xi−2 )), (xi−1 , pn (xi−1 )) und (xi , pn (xi )) sowie den Substitutionen q= xi − xi−1 xi−1 − xx−2 und y = x − xi xi − xi−1 (6.62) lässt sich die Parabel ϕ(x) = ϕ(xi + (xi − xi−1 )y) = 1 (Ay 2 + By + C) 1+q (6.63) mit A = qpn (xi ) − q(1 + q)pn (xi−1 ) + q 2 pn (xi−2 ) (6.64) C = (1 + q)pn (xi ) (6.66) 2 2 B = (2q + 1)pn (xi ) − (1 + q) pn (xi−1 ) + q pn (xi−2 ) (6.65) i herleiten (siehe dazu [EMNW05, S. 102ff]). Die durch y = xix−x −xi−1 herbeigeführte Verschiebung der interpolierenden Parabel in die Nähe des Koordinatenursprungs dient hierbei der Erhöhung der numerischen Stabilität für von Null entfernte Nullstellen. Nun lässt sich unter 1 die Lösungsformel für quadratische Gleichungen anwenden Vernachlässigung des Faktors 1+q und man erhält −2C √ y1/2 = . (6.67) B ± B 2 − 4AC Durch die Rücksubstitution x = xi + (xi − xi−1 )y ergibt sich der neue Näherungswert xi+1 = xi + (xi − xi−1 ) −2C √ . B ± B 2 − 4AC (6.68) Das Vorzeichen im Nenner des Bruches wird so gewählt, dass xi+1 und xi möglichst nahe √ 2 beieinander liegen. Im Fall B ± B − 4AC = 0 benutzt man y1/2 = 1. Muller schlägt in [Mul56] außerdem die Verwendung der Startwerte x0 = −1, x1 = 1 und x2 = 0 (6.69) vor. Die Funktionswerte an diesen Stellen werden allerdings nicht mit pn (x) = an xn + · · · + a0 berechnet, sondern a0 − a1 + a2 an x0 (6.70) a0 + a1 + a2 an x1 (6.71) und a0 an x2 (6.72) verwendet. Als Interpolationspolynom entsteht dann ϕ(x) = a2 x2 + a1 x + a0 , (6.73) 55 6 Berechnung der Nullstellen univariater Polynome wodurch pn (x) in der Umgebung von x = 0 gut approximiert wird. Das Verfahren liefert daher meist die betragskleinste Nullstelle von pn (x), was sich beim Abdividieren der Nullstelle positiv auswirkt. Für das Muller-Verfahren konnte nur lokale Konvergenz nachgewiesen werden. Die Konvergenzordnung beträgt für einfache Nullstellen p ≈ 1.84 und für mehrfache Nullstellen p ≈ 1.23. Durch eine weitere Modifikation erreichte Muller zu dem angegebenen Startprozess dennoch eine Konvergenz in allen getesteten Fällen. Nach jeder Iteration wird dazu die Bedingung pn (xi+1 ) (6.74) pn (xi ) ≤ 10 geprüft. Ist der Wert > 10, so wird q halbiert, xi+1 neu berechnet und die Bedingung erneut kontrolliert. 6.5.2 Die Laguerre-Methode In [PTVF96, S. 372ff] wird die so genannte Laguerre-Methode zur numerischen Bestimmung der Lösungen algebraischer Gleichungen vorgeschlagen. Geht man von der Linearfaktorzerlegung pn (x) = (x − ξ1 )(x − ξ2 ) · · · (x − ξn ) (6.75) aus, so kann man leicht induktiv nachweisen, dass die Ableitungen p′n (x) und p′′n (x) durch 1 1 1 ′ + + ··· + (6.76) pn (x) = pn (x) x − ξ1 x − ξ2 x − ξn 2 1 1 1 ′′ + + ··· + pn (x) = pn (x) x − ξ1 x − ξ2 x − ξn 1 1 1 − pn (x) + + ··· + (6.77) (x − ξ1 )2 (x − ξ2 )2 (x − ξn )2 gegeben sind. Betrachtet man ln |pn (n)| = ln |x − ξ1 | + ln |x − ξ2 | + · · · + ln |x − ξn |, (6.78) so stellt man fest, dass sich die zugehörigen Ableitungen in einfacher Weise aus pn (x) und dessen Ableitungen ergeben: d ln |pn (n)| 1 1 1 p′ (x) = + + ··· + = n dx x − ξ1 x − ξ2 x − ξn pn (x) 2 d ln |pn (n)| 1 1 1 H(x) = − = + + ··· + dx2 (x − ξ1 )2 (x − ξ2 )2 (x − ξn )2 ′ p′′ (x) pn (x) 2 p′′n (x) − = G(x)2 − n = pn (x) pn (x) pn (x) G(x) = (6.79) (6.80) (6.81) Nun wird in der Laguerre-Methode die Annahme gemacht, dass sich die Nullstelle ξ1 in einer gewissen Entfernung a von der aktuellen Näherung xi befindet und alle anderen Nullstellen 56 6.6 Zusammenfassung sich in einer Entfernung b befinden, das heißt es gilt xi − ξ1 = a und xi − ξk = b für k = 2, 3, . . . , n. (6.82) Damit lassen sich die Gleichungen (6.79) und (6.80) zu 1 n−1 + a b n−1 1 H(xi ) = 2 + 2 a b G(xi ) = (6.83) (6.84) umformen, wodurch sich die folgenden Berechnungsvorschriften für die Distanz a und die neue Näherung xi+1 ergeben: a= n p G(xi ) ± (n − 1)(nH(xi ) − G(xi )2 ) xi+1 = xi − a (6.85) (6.86) Das Vorzeichen im Nenner von a wird analog zur Muller-Methode so gewählt, dass a möglichst klein wird. Negative Terme in der Wurzel erzeugen komplexe Näherungswerte, so dass das Verfahren auch in der Lage ist, die komplexen Nullstellen zu finden. Nimmt man als Startwert x0 = 0, so tendiert die Laguerre-Methode in der Regel dazu, gegen die betragsmäßig kleinste Nullstelle zu konvergieren. Obwohl die Konvergenz für Polynome mit ausschließlich reellen Nullstellen nachgewiesen wurde, ist über die Konvergenz bei Polynomen, die auch komplexe Nullstellen besitzen, wenig bekannt. Es konnte aber gezeigt werden, dass die Laguerre-Methode kubisch gegen einfache Nullstellen solcher Polynome konvergiert, wenn der Startwert nahe genug an der Nullstelle liegt. In der Praxis hat sich herausgestellt, dass das vorgestellte Verfahren fast immer konvergiert und die Näherungswerte nur in Ausnahmefällen einen Zyklus bilden, der aber leicht durchbrochen werden kann, indem gelegentlich nicht xi − a, sondern xi − δa als neue Näherung verwendet wird, wobei man δ in jedem solchen Schritt zufällig im Intervall (0, 1) wählt. 6.6 Zusammenfassung Die Bestimmung der reellen Nullstellen von Polynomen ist ein notwendiger Schritt beim Raycasting algebraischer Flächen. Diese Nullstellen können mit Hilfe des D-Chain-Verfahrens oder des Algorithmus von Sturm isoliert werden, so dass sie anschließend durch lokal konvergente Verfahren, wie Bisektion oder Regula-Falsi, bis zur gewünschten Genauigkeit angenähert werden können. Die Anwendung lokal konvergenter Verfahen ist bei der Wertebereichsanalyse nicht ohne weiteres möglich, da das Verfahren nicht in der Lage ist die genaue Anzahl von Nullstellen innerhalb eines Intervalls zu ermitteln. Global konvergente Verfahren, wie die Mulleroder Laguerre-Methode, benötigen hingegen keine besonderen Startwerte, sondern konvergieren stets zu einer (gegebenenfalls komplexen) Nullstelle des Polynoms. Diese Nullstelle kann abdividiert und der Algorithmus auf das entstehende Polynom erneut angewendet werden. Für Polynome bis zum Grad vier existieren darüberhinaus geschlossene Lösungsformeln. 57 6 Berechnung der Nullstellen univariater Polynome 58 7 Implementierung Die in den vorhergehenden Kapiteln beschriebenen Verfahren zum Raycasting algebraischer Flächen wurden zusammen mit einer grafischen Benutzeroberfläche und einem Compiler, der aus der Formel einer algebraischen Fläche den benötigten GLSL-Code erzeugt, in einem Prorgamm namens RealSurf umgesetzt. Die folgenden Abschnitte beschäftigen sich mit der Programmarchitektur und den genannten Teilaspekten der Implementierung. 7.1 Umsetzung des Raycasting-Algorithmus in GLSL Eine der Hauptaufgaben dieser Arbeit ist die Umsetzung verschiedener Algorithmen zur Berechnung der Schnittpunkte einer algebraischen Fläche mit einem Strahl in der OpenGL-Shading-Language. Dabei ist eine Vielzahl spezieller Eigenschaften des GPU-Programmiermodells und der Shader-Programmierung zu beachten. Hinzu kommen spezifische Beschränkungen der Grafikhardware und -treiber, die nicht aus der Spezifikation der OpenGL-Shading-Language hervorgehen. Diese Beschränkungen und die daraus resultierenden Hilfskonstruktionen, die die Implementierung einiger Algorithmen erst ermöglichten, werden nun näher betrachtet. 7.1.1 Verwendete Grafikhardware und Grafiktreiber Die Lauffähigkeit der in dieser Arbeit erstellen Shader-Programme hängt maßgeblich von der verwendeten Grafikhardware und dem Grafikkartentreiber ab. Die Hardware muss die Funktionalitäten des Shader Model 3 unterstützen und der Treiber, der den GLSL-Compiler enthält, muss in der Lage sein, die GLSL-Programme korrekt auf den Befehlssatz der Hardware umzusetzen. Die gesamte Shader-Entwicklung wurde auf NVidia-GPUs der Serien GeForce 6 und GeForce 7 durchgeführt. Beide Serien verfügen, soweit bekannt, über gleichwertige Fähigkeiten beim Umgang mit Shadern. Als Grafiktreiber für beide Serien diente der NVidia-Forceware-Treiber in der Version 93.71. Tests der entwickelten Programme konnten weiterhin auf einer NVidia Quadro FX 4000 und einer ATI Radeon X1600 durchgeführt werden. Leider konnte die ATI-Grafikkarte die Raytracing-Shader nicht verarbeiten. Der ATI-Treiber Catalyst 7.1 schaltete mit der wenig hilfreichen Meldung „Unsupported language feature used“ auf den Software-Renderer um. Die genaue Ursache für dieses Verhalten konnte nicht ermittelt werden. In der Spezifikation der OpenGL-Shading-Language werden keine Einschränkungen bezüglich Größe oder Ausführungszeit der Programme vorgenommen. Programmverzweigungen, Schleifen und Array-Zugriffe können beliebig eingesetzt werden. Leider existieren in der Praxis bei den genannten Punkten sehr wohl Einschränkungen. Hinzu kommen verschiedene Probleme mit dem Shading-Language-Compiler des verwendeten Grafikkartentreibers Forceware 93.71: 59 7 Implementierung Array-Zugriffe Der Zugriff auf Arrayelemente kann nur durch Konstanten oder durch Variablen, deren Werte zur Übersetzungszeit bekannt sind, erfolgen. Der Compiler akzeptiert die meisten Zählschleifen, die auf Arrays zugreifen. Werden Schleifen geschachtelt oder sind die Berechnungen der Array-Indizes zu komplex, so ist der Compiler nicht immer in der Lage, die Array-Indizes im Voraus zu berechnen und bricht den Übersetzungsvorgang ab. Die Verwendung von breakBefehlen zum vorzeitigen Verlassen einer Schleife wirkt sich auf die selbe Weise aus. Schleifen Schleifen, deren Schleifenbedingung von einer uniform-Variable abhängt, werden nach 255 Iterationen abgebrochen. Durch Schachtelung von Schleifen kann diese Beschränkung aber abgeschwächt werden. Bedingte Rücksprünge Der bedingte Rücksprung aus Funktionen ist nicht möglich. Pro Funktion kann es also nur eine return-Anweisung geben. Dieses Verhalten verursacht lediglich einen Performanceverlust. Statt dem bedingten Rücksprung wird eine Ergebnisvariable verwendet, der abhängig von der Bedingung das Ergebnis zugewiesen wird. Die Funktion gibt immer diese Ergebnisvariable zurück. Interne Compilerfehler Eine sehr große Hürde bei der Implementierung komplexer Algorithmen in GLSL ist ein Abbruch des Übersetzungvorgang mit der Fehlermeldung „internal compiler error“, der ohne erkennbaren Grund bei größeren Programmen auftritt. Hinter einem solchen Fehler verbirgt sich meist ein einfacher Syntaxfehler in Form eines vergessenen Semikolons oder auch ein Schreibfehler in einem Variablennamen. Da bei internen Compilerfehlern meist keine Zeilennummern ausgegeben werden, ist die Fehlersuche jedoch sehr zeitaufwendig und die eigentliche Fehlerursache in der Praxis nur sehr schwer zu ermitteln. Modularisierung des Quelltextes OpenGL erlaubt eine Aufteilung des GLSL-Quelltextes in einzelne Module, die unabhängig voneinander kompiliert und erst beim Aufruf von glLinkProgram zusammengefügt werden. Die Shader-Programme zum Raycasting verschiedener algebraischer Flächen unterscheiden sich nur wenig. Zur Performacesteigerung könnten die flächenunabhängigen Shader-Komponenten daher einmalig in einem Vorverarbeitungsschritt übersetzt werden. Diese Funktionalität wird jedoch vom NVidia-GLSL-Compiler nicht unterstützt, so dass der gesamte Quelltext des zum Raycasting verwendeten Shader-Programms zu einer Zeichenkette zusammengefügt werden muss, die dann dem Compiler übergeben wird. 7.1.2 Modifikation der Raycasting-Algorithmen für GLSL Aufgrund der vorgestellten Einschränkungen bei der GPU-Programmierung müssen einige der in Kapitel 5 und 6 erläuterten Verfahren modifiziert werden. Besondere Auswirkungen auf die Implementierung haben Array-Zugriffe innerhalb von Schleifen. Alle Array-Indizes müssen dem GLSL-Compiler zur Übersetzungszeit bekannt sein. Sie dürfen daher nicht vom Raycasting-Strahl oder anderen Variablen abhängig sein, die erst während des Renderingvorgangs zur Verfügung stehen. Die notwendigen Modifikationen der einzelnen Algorithmen sind sich sehr ähnlich und werden nachfolgend am Beispiel des D-Chain-Verfahrens und des Abdividierens von Nullstellen beschrieben. 60 7.1 Umsetzung des Raycasting-Algorithmus in GLSL Das D-Chain-Verfahren (k) Beim D-Chain-Verfahren (vgl. Abschnitt 6.4.1) werden die Nullstellen von pn (x) mit Hilfe (k) (k+1) (k) der Nullstellen der Ableitung (pn (x))′ = pn (x) berechnet. Jedes pn (x) besitzt maximal (k+1) n − k Nullstellen. Sind alle Nullstellen von pn (x) innerhalb des Clipping-Intervalls (l, u) bereits berechnet und zusammen mit l und u sortiert in einem Array gespeichert1 , so bilden (k) jeweils zwei benachbarte Einträge ein Intervall, welches maximal eine Nullstelle von pn (x) (k+1) enthält. Nun kann es jedoch vorkommen, dass pn (x) nur m < n − k reelle Nullstellen innerhalb von [l, u] besitzt und sich damit die Anzahl der zu untersuchenden Intervalle verringert. (k+1) (x), welches Die verwendeten Array-Indizes verändern sich also in Abhängigkeit von pn wiederum aus dem Raycasting-Strahl und der Formel der algebraischen Fläche gebildet wird und somit nicht zur Übersetzungszeit bekannt ist. (k) Nachfolgend wird zur Speicherung der Nullstellen für jedes pn (x) ein Array r(k) der Länge (k) (k) n − k + 2 verwendet. r0 enthält stets die untere Clipping-Grenze l und rn−k+1 stets die obere Clipping-Grenze u. Zusätzlich wird eine Markierung ◦ eingeführt, die ein Array-Element von r(k) als gültige obere Grenze eines Intervalls definiert, in dem nach einer Nullstelle von (k−1) (k) pn (x) gesucht werden soll. Nicht markierte Elemente ri , i = 1, . . . , n − k werden während (k) der Iteration über das Array jeweils mit dem Vorgänger ri−1 belegt, so dass ein sortiertes Array entsteht, welches nur die Werte von markierten Elementen, sowie von l und u enthält. Für ein Polynom sechsten Grades mit zwei reellen Nullstellen ξ1 und ξ2 in (l, u) würde der unmodifizierte Algorithmus lediglich eine Liste mit den Werten l, ξ1 , ξ2 und u verwenden, während r(0) beispielsweise die folgende Struktur aufweisen könnte (die gestrichelt umrandeten Felder sind immer gleich und werden nicht berechnet): r(0) = l l ξ1 ◦ ξ1 ξ1 ξ2 ◦ ξ2 u ◦ Gelangt der modifizierte Algorithmus nun bei der Iteration über r(k) an ein nicht markiertes (k) (k−1) Array-Element ri mit i = 1, . . . , n − k, so wird dieses einfach übersprungen. ri wird (k−1) (k) dabei auf ri−1 gesetzt und nicht markiert. Ist ri hingegen markiert, so wird das Intervall (k) (k) (k−1) (ri−1 , ri ) auf eine Nullstelle von pn (k−1) ri (x) untersucht. Wird eine Nullstelle ξ gefunden, dann (k−1) ri setzt man = ξ und markiert als gültig. Andernfalls verfährt man wie im Falle (k) des unmarkierten ri . Abbildung 7.1 zeigt das Verfahren anhand des Polynoms pn (x) = x4 − 11x3 + 44x2 − 74x + 40. Durch dieses recht umständliche Vorgehen kann die Adressierung der r(k) für alle Polynome pn (x) auf die gleiche Weise erfolgen und somit in GLSL und unter den beschriebenen Einschränkungen durch Grafiktreiber und -hardware realisiert werden. Abdividieren von Nullstellen Die Muller- und die Laguerre-Methode berechnen jeweils eine Nullstelle des reellen Polynoms pn (x), welche sowohl reell als auch komplex sein kann. Im Falle einer komplexen Nullstelle ξ wird nicht der Linearfaktor (x − ξ), sondern der quadratische Faktor (x − ξ)(x − ξ) mit Hilfe des Horner-Schemas (siehe Abschnitt 6.1.2) abdividiert, wodurch wieder ein Polynom 1 Andere brauchbare Datenstrukturen, wie beispielsweise verkettete Listen, können nicht in GLSL realisiert werden, da es keine Zeigertypen gibt. 61 7 Implementierung Nullstellen von p′′′ n (x) : r(3) = Nullstellen von p′′n (x) : Nullstellen von p′n (x) r(2) = r(1) : Nullstellen von pn (x) : r(0) = = l l l l 2.75 ◦ 2.27 ◦ 3.23 ◦ 1.0 1.0 1.71 ◦ 1.0 ◦ u ◦ 1.71 1.71 u ◦ u ◦ 4.0 ◦ u ◦ Abbildung 7.1: Inhalt der r (k) bei der Berechnung der Nullstellen des Polynoms pn(x) = x4 − 11x3 + 44x2 − 74x + 40 mit dem D-Chain-Verfahren. Die Arrays sind so angeordnet, dass die beiden Array-Elemente von r(k+1) , die über einem Element von r(k) stehen, das zu untersuchende Einschlussintervall dieses Elementes definieren. Der Algorithmus (1) (1) soll am Beispiel der Berechnung von r(1) beschrieben werden. Zuerst wird r0 = l und r4 = u gesetzt. Das erste zu untersuchende Intervall (l, 2.27) enthält die Nullstelle 1.71 von p′n (x). (1) Daher wird r1 = 1.71 gesetzt und mit ◦ markiert. Die folgenden Intervalle (2.27, 3.23) und (3.23, u) enthalten keine Nullstellen von p′n (x). Die zuletzt gültige obere Intervallgrenze 1.71 (1) (1) (1) wird in r2 und r3 kopiert, aber nicht mit ◦ versehen. r4 = u ist immer eine gültige obere Intervallgrenze und erhält daher ein ◦. Bei der Berechnung der Nullstellen von pn (x) werden nun alle Intervalle übersprungen, deren obere Intervallgrenze nicht mit ◦ markiert ist. Die mit ◦ versehenen Elemente von r(0) sind, abgesehen von u, die reellen Nullstellen von pn (x) innerhalb des Clipping-Intervalls (l, u). Alle in der Abbildung enthaltenen Zwischenwerte wurden auf zwei Nachkommastellen gerundet. mit ausschließlich reellen Koeffizienten entsteht. Anschließend wird die nächste Nullstelle des verbleibenden Polynoms ermittelt. Ob der Grad dieses Polynom um eins oder um zwei kleiner ist als der Grad von pn (x), hängt also von der gefundenen Nullstelle ab, die natürlich nicht zur Übersetzungszeit bekannt ist. Die Modifikation des Verfahrens besteht nun darin, eine Zählschleife zu verwenden, welche annimmt, dass eine reelle Nullstelle gefunden und abdividiert wird. Dadurch reduziert sich der Polynomgrad in jeder Iteration um eins. Tritt nun eine komplexe Nullstelle auf, so wird der quadratische Faktor abdividiert und in der nächsten Iteration der Zählschleife keine neue Nullstelle berechnet. Dies kann leicht durch eine Variable skip_next realisiert werden, die jeweils am Anfang des Schleifenrumpfes geprüft wird: ... bool skip_next = false; for( int i = n; i >= 1; i-- ) { if( skip_next ) { // Nullstellensuche für eine Iteration aussetzen skip_next = false; } else { // Nullstellensuche durchführen root = calculate_root( p ); 62 7.2 Erzeugung der Shader aus den Formeln algebraischer Flächen if( is_real( root ) ) { // linearen Faktor abdividieren p = deflate1( p, root ); } else { // quadratischen Faktor abdividieren p = deflate2( p, root ); skip_next = true; } } } ... 7.2 Erzeugung der Shader aus den Formeln algebraischer Flächen Der Fragment-Shader zum Raycasting algebraischer Flächen besteht aus zwei Teilen: einem flächenunabhängigen Teil mit den Funktionen zur Berechnung der Polynomkoeffizienten und der Nullstellen von Polynomen und einem flächenabhängigen Teil, welcher die Funktionen zur Berechnung der Polynomkoeffizienten ansteuert. Der letztgenannte Teil muss für jede algebraische Fläche neu geschrieben beziehungsweise generiert werden. Zu diesem Zweck wurde ein Compiler entwickelt, der in der Lage ist die Flächenformel in einen Syntaxbaum zu überführen und daraus wiederum den benötigten Shader-Code zu erzeugen. Hierfür wurden die unter der GNU General Public License veröffentlichten Werkzeuge Flex [Frea] und Bison [Freb] verwendet, die anhand der grammatikalischen Beschreibung einer Sprache so genannte Scanner beziehungsweise Parser für diese Sprache generieren können. Der Scanner zerlegt einen Eingabetext in logisch zusammengehörige Einheiten, auch Token genannt, die durch reguläre Ausdrücke beschrieben werden. Mit Hilfe dieser Token führt der Parser die Syntaxanalyse der Eingabe durch, so dass daraus beispielsweise ein Syntaxbaum erzeugt werden kann. Auf eine detaillierte Darstellung der Funktionsweise dieser Werkzeuge wird in dieser Arbeit verzichtet. Eine gute Referenz hierfür liefern [LMB95] und [Her95]. Die dem Parser zugrunde gelegte Grammatik (siehe Anhang B) ermöglicht es, Polynome in den drei Variablen x, y und z zu verarbeiten. Hinzu kommen beliebige Variablen, die als reellwertige Flächenparameter dienen und ein interaktives Verändern der Fläche erlauben sollen. Der Parser erzeugt aus der Eingabe einen Syntaxbaum, wie er in Abbildung 7.2 zu sehen ist. Ist der Syntaxbaum einmal erstellt, so kann der GLSL-Code zur Berechnung der Polynomkoeffizienten leicht durch Traversieren des Baumes erstellt werden. Für die in Abschnitt 5.2 beschriebenen Termumformungen auf der GPU würde beispielsweise der folgende Code erzeugt: // Basispolynome mit den Daten des polynomial x = create_poly_1( o.x, polynomial y = create_poly_1( o.y, polynomial z = create_poly_1( o.z, Raycasting-Strahls o+t*d füllen d.x ); d.y ); d.z ); 63 7 Implementierung + ^ + ^ ∗ ^ x − 3 x y z 2 2 1 Abbildung 7.2: Syntaxbaum zur Eingabe x3 ∗ (x − 1) + y 2 + z 2 . Die Klammerung der Teilausdrücke ist implizit in der Baumstruktur enthalten. // p durch Termumformungen berechnen polynomial p = add( add( mult( power( x, 3, 1 ), sub( x, create_poly_0( 1.0 ), 1 ), 4 ), power( y, 2, 1 ), 4 ), power( z, 2, 1 ), 4 ); Der dritte Parameter der Funktionen add, sub, mult und power bestimmt jeweils den Grad des resultierenden Polynoms, welcher als Konstante zur Übersetzungszeit des Shaders vorliegen muss2 . Zur Berechnung der Polynomkoeffizienten durch Interpolation (Abschnitt 5.3) wird lediglich eine Funktion zur Auswertung der algebraischen Fläche F in einem Punkt benötigt: float F( float x, float y, float z ) { return x * x * x * ( x - 1 ) + y * y + z * z; } Der Code zur numerischen Berechnung des Gradienten von F mit Hilfe von Differenzenquotienten besitzt eine ähnlich einfache Struktur. Zur Verbesserung der numerischen Stabilität des Raycasting-Verfahrens ist es allerdings ∂F ∂F sinnvoll die Formeln der partiellen Ableitungen ∂F ∂x , ∂y und ∂z mit Hilfe symbolischer Berechnungen aus F herzuleiten. Der Baum wird dazu beginnend mit der Wurzel entsprechend den Regeln der Differentialrechnung umstrukturiert. In einem Vorverarbeitungsschritt werden für jeden Knoten die in seinem Unterbaum vorkommenden Variablen bestimmt. Dadurch kann man leicht ermitteln, ob sich ein (gegebenenfalls sehr komplexer) Unterbaum durch die 2 Die Funktionen sind so implementiert, dass der Grad der Eingabepolynome nicht benötigt wird. 64 7.3 Grafische Benutzeroberfläche Differentiation auf Null reduziert. Die Differentiation von − + 3 x +y∗z−1 = b 1 ˆ x ∗ 3 y z nach y würde beispielsweise folgendermaßen ablaufen (die mit me müssen jeweils noch differenziert werden): ∂ − ∂y ∂ ∗ ∂y 1 ^ ⇒ ^ ∗ 3 gekennzeichneten Unterbäu- ∂ + ∂y + x ∂ ∂y y x ⇒ ∗ 3 y y ⇒ z z z z Aus den Syntaxbäumen der partiellen Ableitungen kann nun der Code zur Berechnung des Gradienten erstellt werden, der zusammen mit dem Code zur Berechnung der Polynomkoeffizienten den flächenabhängigen Teil des Fragment-Shaders bildet. Dieser wird durch den flächenunabhängigen Teil zu einem vollständigen Fragment-Shader für das Raycasting algebraischer Flächen ergänzt. 7.3 Grafische Benutzeroberfläche Die Bedienoberfläche von RealSurf (Abbildung 7.3) wurde mit Hilfe der Qt-Bibliothek entwickelt. Sie erlaubt die Eingabe von Flächenformeln und Flächenparametern, sowie die Einstellung zahlreicher Darstellungsoptionen wie beispielsweise Materialien und Beleuchtung. Die jeweils aktuelle Ansicht einer Fläche kann zudem abgespeichert und wieder geladen werden. Die für das Raycasting relevanten Aufgaben der Benutzeroberfläche sind in Abbildung 7.4 als Aktivitätsdiagramm zusammengefasst. Sie werden im Anschluss erläutert. 7.3.1 Überführung einer Flächenformel in ein gerendertes Bild Gibt der Nutzer eine Flächenformel in die Eingabezeile ein oder wird eine gespeicherte Fläche geladen, so wird die Formel an den in Abschnitt 7.2 beschriebenen Compiler weitergeleitet. Dieser generiert den GLSL-Raycasting-Code und liefert außerdem alle in der Formel enthaltenen Flächenparameter, das heißt alle Variablen außer x, y und z. Nach dem Übersetzen des Shader-Codes durch den GLSL-Compiler wird im OpenGL-Widget mit diesem Shader eine Box gezeichnet, in der das Raycasting durchgeführt wird. 65 7 Implementierung Abbildung 7.3: Startansicht der grafischen Benutzeroberfläche von RealSurf. Das Hauptfenster gliedert sich in ein OpenGL-Widget zum Betrachten der Fläche (links), eine Zeile zur Eingabe einer Flächenformel (unten) und einen Bereich zur Steuerung der Flächenparameter, verschiedener Anzeigeoptionen und zum Laden und Speichern von Flächen (rechts). Programmstart auf Eingabe warten neue Flächenformel eingegeben Fläche gedreht, skaliert oder verschoben Flächen- oder Darstellungs- parameter verändert Fehlermeldung anzeigen Formel parsen Parameter an Shader-Programm übergeben Transformation an Shader-Programm übergeben Raycasting-Shader ausführen Eingabe fehlerhaft Eingabe fehlerfrei GLSL-Quelltext des Raycasting-Shaders generieren Abbildung 7.4: Vereinfachte Darstellung der für das Raycasting relevanten Aufgaben in RealSurf. 66 7.3 Grafische Benutzeroberfläche 7.3.2 Transformationen und Darstellungsparameter RealSurf erlaubt das interaktive Drehen, Skalieren und Verschieben der Fläche, also die interaktive Veränderung einer Transformationsmatrix. Diese Transformationsmatrix wird dem Raycasting-Shader als uniform-Variable übergeben. Ähnlich verhält es sich mit den Materialeigenschaften der Fläche, sowie mit den Parametern der Lichtquellen. Hierfür bietet die OpenGL-Shading-Language bereits vordefinierte uniform-Variablen an [KBR04, S. 45ff], die im wesentlichen den Parametern des Beleuchtungsmodells nach Phong entsprechen, welches von OpenGL verwendet wird [SA04, S. 59ff]. Daher benutzt auch RealSurf dieses Beleuchtungsmodell. 7.3.3 Flächenparameter Durch die Veränderung von Flächenparametern, wie beispielsweise dem inneren und äußeren Radius eines Torus, kann auch die Form einer Fläche interaktiv geändert werden. Solche Parameter werden vom Flächenformel-Compiler als uniform-Variable in den Shader integriert, um dort in die Berechnung der Polynomkoeffizienten einzufließen. Die Parameter können in RealSurf bequem über Schieberegler eingestellt werden. Abbildung 7.5 zeigt die Animation zwischen verschiedenen Flächen vierten Grades. Mit Hilfe von Flächenparametern lassen sich auch leicht Morphings zwischen beliebigen algebraischen Flächen erzeugen. Dazu wird einfach eine Interpolation zwischen den Flächen vorgenommen. Für die Variable t und die Punkte (−1, A), (0, B) und (1, C) ergibt sich beispielsweise mit der Interpolationsformel von Lagrange A t(t − 1) (t + 1)t − B(t + 1)(t − 1) + C . 2 2 (7.1) Setzt man für A, B und C jeweils Formeln algebraischer Flächen ein, so entsteht bei der Variation von t zwischen −1 und 1 eine Animation, die alle drei Flächen enthält und teilweise sehr interessante Zwischenergebnisse hervorbringt. Das Rendering einer solchen zusammengesetzten Formel ist natürlich aufwendiger als das Rendering einer der Einzelflächen. (a) µ2 = 1 3 (b) µ2 = 2 3 (c) µ2 = 1 (d) µ2 = √ 2 (e) µ2 = 3 Abbildung 7.5: Animation zwischen verschiedenen Flächen der Kummer-Familie durch Veränderung des Flächenparameters µ. Kummer-Flächen genügen √ der algebrai√ 2 )(x2 + y 2 + z 2 − µ2 w 2 )2 − (3µ2 − 1)(w − z − 2x)(w − z + 2x)(1 + z + schen Gleichung (3 − µ √ √ 2y)(w + z − 2y) = 0 vierten Grades [Wei99]. w wurde in dieser Animation auf 1 festgelegt. 67 7 Implementierung 68 8 Ergebnisse Beim Raycasting algebraischer Flächen werden Algorithmen zur Berechnung der Koeffizienten des Polynoms f (t), welches durch Einsetzen des Raycasting-Strahls in die Flächengleichung F (x, y, z) = 0 entsteht, und zur Berechnung der Nullstellen von f (t) benötigt. Die theoretischen Grundlagen, sowie die Besonderheiten der Implementierung in der OpenGL-ShadingLanguage wurden bereits erläutert. Hinsichtlich der Laufzeiten der Berechnungen und der Qualität der erzielten Ergebnisse gibt es teilweise erhebliche Unterschiede zwischen den verschiedenen Algorithmen. Zur kompakteren Darstellung der Resultate werden die Verfahren in den folgenden Abschnitten gegebenenfalls durch ihre Anfangsbuchstaben abgekürzt: tugpu (Termumformungen auf der GPU), li (Lagrange-Interpolation), ni (Newton-Interpolation), dcb (D-Chain-Verfahren mit Bisektion), dcrf (D-Chain-Verfahren mit Regula-Falsi), sk (Algorithmus von Sturm), wba (Wertebereichsanalyse), m (Muller-Iteration), l (Laguerre-Iteration) und lf (Lösungsformeln). 8.1 Optischer Vergleich der implementierten Verfahren Ein wichtiges Bewertungskriterium für Algorithmen, die beim Raycasting eingesetzt werden, ist die Qualität der erzeugten Renderings. Ein Verfahren mit hoher Laufzeit und guter Bildqualität ist beispielsweise deutlich besser geeignet als ein schnelles Verfahren, welches häufig Bildfehler verursacht. Besonders hervorzuheben ist die in Abschnitt 4.2 vorgestellte Heuristik zur Optimierung des Raycasting-Strahls. Sie stabilisiert mit geringem Aufwand alle nachfolgenden Berechnungen und erhöht die Bildqualität enorm (Abbildung 8.1). Die Bewertung der einzelnen Verfahren wird unter Verwendung dieser Optimierungsheuristik durchgeführt. 8.1.1 Berechnung der Polynomkoeffizienten Während die Berechnung der Polynomkoeffizienten durch Termumformungen auf der GPU stets gute bis sehr gute Ergebnisse liefert, hängt die Korrektheit der mit den Interpolationsverfahren berechneten Koeffizienten sehr stark von den gewählten Stützstellen ti entlang (a) Ohne Optimierung (b) Mit Optimierung Abbildung 8.1: Verbesserung der Stabilität der verwendeten numerischen Verfahren durch Optimierung des Raycasting-Strahls am Beispiel der Kleinschen Flasche. 69 8 Ergebnisse des Raycasting-Strahls ab. Spiegeln die aus den Stützstellen berechneten Punkte (ti , f (ti )) die charakteristischen Eigenschaften von f nur schlecht wieder, so können selbst kleine Rundungsfehler eine wesentliche Veränderung der Nullstellenlage zur Folge haben (siehe Abbildung 5.1). Da die charakteristischen Eigenschaften jedoch nicht im Voraus bekannt sind, wurden die Stützstellen äquidistant im Clipping-Intervall [a, b] gewählt. Das Auftreten von Bildfehlern ist somit stark flächenabhängig und besonders in der Nähe von Singularitäten zu beobachten. Des Weiteren lassen sich bei einigen Flächen Bildfehler durch Skalieren der Fläche provozieren. Dieses unerwünschte Verhalten ist exemplarisch in Abbildung 8.2 dargestellt. Dennoch liefern auch die Interpolationsverfahren bei den meisten der getesteten Flächen befriedigende bis gute Resultate. Die Polynomkoeffizienten sollten also bevorzugt durch Termumformungen auf der GPU berechnet werden. Aufgrund der stark flächenabhängigen Renderingqualität sind die Interpolationsverfahren nur bedingt geeignet. Natürlich lassen sich leicht Beispiele konstruieren, bei denen alle getesteten Verfahren allein aufgrund der endlichen Zahlendarstellung fehlschlagen. So entstehen beispielsweise beim Raycasting der Fläche x2 = 0 nur Polynome mit doppelten Nullstellen, die durch die Verwendung von Fließkommazahlen häufig zu Polynomen ohne Nullstelle degenerieren. 8.1.2 Berechnung der Polynomnullstellen Die in Kapitel 6 vorgenommene Klassifizierung der Algorithmen zur Bestimmung von Polynomnullstellen wird auch für die Bewertung dieser Verfahren übernommen, da Algorithmen derselben Kategorie häufig ähnliche Vor- beziehungsweise Nachteile aufweisen. Der Vergleich wird anhand verschiedener Beispielrenderings illustriert, da es nur schwer möglich ist, eine algebraische Fläche zu finden, an der sich die typischen Eigenschaften aller Verfahren gut erläutern lassen. Lösungsformeln Die Lösungsformeln für Polynome vom Grad eins und zwei sind aufgrund ihres äußerst geringen Berechnungsaufwands sehr robust und liefern exzellente Ergebnisse. Durch Divisionen und Wurzelberechnungen entstehenden Rundungsfehler verändern die Ergebnisse nur unwesentlich und fallen daher bei der Visualisierung algebraischer Flächen nicht ins Gewicht. Für Polynome der Form x3 + ax2 + bx + c schlägt die Lösungsformel (vgl. Abschnitt 6.2.3) fehl, sobald a der dominierende Koeffizient ist. Die genaue Ursache konnte aufgrund fehlender Debugging-Mechanismen bei der GPU-Programmierung nicht ermittelt werden. Eine mögliche Ursache der Instabilität könnte die im Algorithmus enthaltene hohe Potenz a6 sein. Die Koeffizienten b und c tauchen maximal in dritter Potenz auf. Leider setzt die Formel zur Lösung biquadratischer Gleichungen auf der Lösungsformel kubischer Gleichungen auf, so dass sich die Fehler entsprechend fortpflanzen. Ein beispielhafter Qualitätsvergleich der einzelnen Lösungsformeln ist in Abbildung 8.3 zu finden. Nullstellenisolation Die Verfahren zur Nullstellenisolation bestimmen Intervalle mit jeweils einer Nullstelle, welche anschließend mit Hilfe lokal konvergenter Verfahren, wie der Bisektion, weiter verkleinert werden, bis die Nullstelle genau genug approximiert ist. Bildfehler können entstehen, wenn 70 8.1 Optischer Vergleich der implementierten Verfahren die berechneten Intervalle beispielsweise nicht alle zur Berechnung der Pixelfarbe benötigten Nullstellen enthalten oder wenn ein Intervall mehrere Nullstellen enthält. Das D-Chain-Verfahren erwies sich in Kombination mit der Bisektion in den durchgeführten Tests als äußerst stabil. Die getesteten Flächen, die mit dem D-Chain-Verfahren nicht korrekt dargestellt werden, können auch mit keinem der anderen Algorithmen fehlerfrei gerendert werden. Das Scheitern aller Algorithmen lässt darauf schließen, dass bereits die Polynomkoeffizienten nicht exakt berechnet werden. Zusätzlich zur Bisektion wurde hier auch die RegulaFalsi untersucht. Sie zeigt in der GLSL-Implementierung sehr schlechte Konvergenzeigenschaften und führt teilweise sogar zu einem Abbruch des Shader-Programms. Renderings, die mit dem Algorithmus von Sturm erzeugt werden, enthalten oftmals Bildfehler entlang bestimmter Kurven. Diese verändern sich je nach Blickrichtung und Position der Fläche und fallen nicht nur mit Singularitäten zusammen. Tests ergaben, dass diese Darstellungsfehler genau dann auftreten, wenn das zu untersuchende Polynom f (t) und dessen Ableitung f ′ (t) ähnliche Linearfaktoren enthalten. Dadurch entsteht bei den im Algorithmus von Sturm verwendeten Polynomdivisionen ein Divisionsrest, dessen höchster Koeffizient nahe bei Null liegt. Dieser Divisionsrest wird in der nächsten Polynomdivision wiederum als Divisor eingesetzt und trägt somit entscheidend zur Anhäufung von Rundungsfehlern bei. Wie bereits in Abschnitt 6.4.3 beschrieben wurde, kann die zur Wertebereichsanalyse eingesetzte Intervallarithmetik aufgrund fehlender Einstellungsmöglichkeiten der Rundungsart nicht korrekt in der OpenGL-Shading-Language implementiert werden. Die ohne korrekte Rundung implementierte Wertebereichsanalyse mit Hilfe der rekursiven Taylor-Methode (vgl. Abschnitt 6.4.3) liefert erstaunlich gute Ergebnisse, die mit denen des D-Chain-Verfahrens vergleichbar sind. In äußerst seltenen Fällen treten einzelne Fehlerpixel auch in glatten Flächenbereichen auf. Typische Ergebnisse der einzelnen Verfahren zur Nullstellenisolation sind in Abbildung 8.4 zusammengefasst. Global konvergente Iterationsverfahren Die untersuchten global konvergenten Iterationsverfahren berechnen jeweils eine Nullstelle des Polynoms, die anschließend abdividiert wird, wodurch ein Restpolynom niedrigeren Grades entsteht, auf das das Verfahren erneut angewendet wird. Die Iterationsverfahren konvergieren zuverlässig zu einer Nullstelle des jeweils betrachteten Polynoms, jedoch werden durch das wiederholte Abdividieren die Koeffizienten des Restpolynoms so stark gestört, dass die zuletzt berechneten Nullstellen stark von denen des Ursprungspolynoms abweichen. Da die Verfahren komplexe Arithmetik verwenden, ist es teilweise schwierig reelle und komplexe Nullstellen voneinander zu unterscheiden. In der Regel besitzt jede berechnete Nullstelle einen komplexen Anteil, so dass ein Schwellwertkriterium für die Auswahl der reellen Nullstellen eingeführt werden muss. Leider führt ein fest gewählter Schwellwert insbesondere bei Polynomen vom Grad größer sechs häufig zu Fehlentscheidungen. Abbildung 8.5 verdeutlicht die Probleme bei der Verwendung der global konvergenten Iterationsverfahren. Die Darstellungsqualität ist für Flächen bis zum Grad sechs noch sehr gut, nimmt dann aber rapide ab. Der optische Vergleich der Verfahren legt nahe, zur Nullstellenberechnung bevorzugt das D-Chain-Verfahren mit Bisektion oder die Wertebereichsanalyse mit Hilfe der rekursiven Taylor-Methode zu verwenden. Für Flächen vom Grad eins und zwei sind darüberhinaus die Lösungsformeln empfehlenswert. 71 8 Ergebnisse Bildfehler in der Umgebung von Singularitäten Bildfehler bei skalierten Flächen Termumformungen auf der GPU NewtonInterpolation LagrangeInterpolation Abbildung 8.2: Exemplarischer Qualitätsvergleich der Algorithmen zur Berechnung der Polynomkoeffizienten. Mit Hilfe der Termumformungen auf der GPU können die Koeffizienten in den meisten Fällen sehr genau berechnet werden. Die Interpolationsverfahren scheitern hingegen in der Umgebung singulärer Punkte und Kurven, wie sie die Kleinsche Flasche aufweist. Durch eine Skalierung werden die maximal zwei Schnittpunkte, die der abgerundete Würfel x6 + y 6 + z 6 − 1 = 0 mit dem Raycasting-Strahl haben kann, so weit zusammengeschoben, daß sich nur noch wenige der Interpolationsstellen in deren Umgebung befinden. Die charakterischen Eigenschaften der Polynoms f (t) werden damit nur ungenügend erfasst. (a) Grad 1: Ebene (b) Grad 2: Kegel (c) Grad 3: Cayley-Fläche (d) Grad 4: Steiner-Fläche Abbildung 8.3: Beispielhafter Qualitätsvergleich der Lösungsformeln für Polynome vom Grad eins bis vier. Flächen vom Grad eins und zwei werden fehlerfrei gerendert. Die Lösungsformel für Polynome der Form x3 + ax2 + bx + c schlägt fehl, sobald der Koeffizient a eine dominante Rolle einnimmt. Da die Lösungsformel für Polynome vom Grad vier die Lösungsformel kubischer Polynome verwendet, treten auch hier Darstellungsfehler auf. 72 8.1 Optischer Vergleich der implementierten Verfahren (a) D-Chain-Verfahren mit Bisektion (b) D-Chain-Verfahren mit Regula-Falsi (c) Algorithmus Sturm von (d) Wertebereichsanalyse mit rekursiver Taylor-Methode Abbildung 8.4: Vergleich der Darstellungsqualität der Verfahren zur Nullstellenisolation anhand der Boyschen Fläche. Das D-Chain-Verfahren liefert in Kombination mit der Bisektion hervorragende Ergebnisse. Lediglich in unmittelbarer Nähe von Singularitäten kann es zu einzelnen Pixelfehlern kommen. Die GLSL-Implementierung der RegulaFalsi konvergiert häufig zur falschen Nullstelle (gelbe Bereiche) oder überschreitet die maximal zulässige Ausführungsdauer (Block-Artefakte). Beim Algorithmus von Sturm entstehen Bildfehler entlang bestimmter Kurven. Die Ergebnisse der Wertebereichsanalyse sind mit denen des D-Chain-Verfahrens mit Bisektion vergleichbar. (a) gewünschtes Ergebnis (b) Muller-Iteration (c) Laguerre-Iteration Abbildung 8.5: Darstellungsfehler der global konvergenten Iterationsverfahren bei der Bartschen Fläche vom Grad 10. Da die von beiden Verfahren berechneten Nullstellen in der Regel einen komplexen Anteil besitzen, ist die schwellwertabhängige Auswahl der Nullstellen, die als Schnittpuntke verwendet werden, schwierig und fehleranfällig. Zudem verfälscht das wiederholte Abdividieren von Nullstellen die verbleibenden Nullstellen. Die unterschiedlichen Ergebnisse der Muller- und der Laguerre-Methode entstehen durch die unterschiedliche Reihenfolge, in der die Verfahren die Nullstellen berechnen. 73 8 Ergebnisse GeForce 6600 GT GeForce 6800 GT Quadro FX 4000 GeForce 7600 GS Beschleunigungsfaktor 1.6 1.4 1.2 1 0.8 0.6 0.4 0.2 0 dcb dcrf sk wba m l lf Algorithmen zur Berechnung der Polynomnullstellen Abbildung 8.6: Skalierbarkeit der Raycasting-Algorithmen anhand von Testrenderings der Steiner-Fläche. Die Messungen der Bildwiederholraten wurden bezüglich der GeForce 6600 GT normiert, um ausgehend von dieser Grafikkarte einen Beschleunigungsfaktor zu ermitteln. Obwohl die Bescheunigungsfaktoren einer Grafikkarte für verschiedene Algorithmen in etwa gleich sein sollten, gibt es teilweise große Unterschiede. Die Faktoren für die Quadro FX 4000 schwanken beispielsweise zwischen 0.97 (l) und 1.5 (dcrf). 8.2 Skalierbarkeit der Raycasting-Algorithmen Die GPU einer modernen Grafikkarte besteht aus mehreren Berechnungseinheiten zur Ausführung der Fragment-Shader. Da die Berechnungseinheiten der GPU die einzelnen Raycasting-Strahlen unanhängig voneinander verarbeiten und dabei keinerlei Daten untereinander austauschen können, ist zu erwarten, dass alle Algorithmen etwa in gleichem Maße durch den Einsatz einer schnelleren Grafikkarte mit höherer Taktfrequenz oder einer größeren Anzahl Berechnungseinheiten beschleunigt werden. Abbildung 8.6 zeigt, dass die Beschleunigung teilweise sehr unterschiedlich ausfällt. Möglicherweise werden beim Übersetzen der ShaderProgramme für verschiedene Grafikkarten jeweils unterschiedliche Optimierungsalgorithmen eingesetzt, so dass die Schwankungen in den Beschleunigungsfaktoren auf die Unterschiede im erzeugten Maschinencode zurückzuführen sind. 8.3 Laufzeitvergleich der implementierten Verfahren In interaktiven Anwendungen ist die Laufzeit der eingesetzten Algorithmen von besonderem Interesse. Werden etwa 15 Bilder pro Sekunde erreicht, so nimmt das menschliche Auge eine Bewegung als fließend war [AMMH02, S. 1]. Zur Bewertung der untersuchten Algorithmen wurden Laufzeitmessungen mit verschiedenen algebraischen Flächen vom Grad eins bis zwölf durchgeführt (vgl. Anhang A.1). Dazu wurde auf einer NVidia Geforce 6600 GT pro Fläche und Algorithmus die Anzahl Bilder pro Sekunde (fps) bei einer Auflösung von 512 × 512 74 8.3 Laufzeitvergleich der implementierten Verfahren Flächen Name plane hyperboloid cayley clebsch steiner kummer stagnaro boy barth6 septic octic barth10 roundedcube Grad 1 2 3 3 4 4 5 6 6 7 8 10 12 Messergebnisse in fps tugpu li ni 136.00 45.63 20.00 10.11 10.18 7.70 5.60 4.33 3.68 5.86 1.74 0.62 1.90 127.00 40.00 19.35 9.93 8.79 6.69 5.77 3.82 2.70 2.87 1.12 – – 130.00 39.19 19.69 9.69 9.84 5.96 7.39 5.37 3.55 4.50 1.53 0.47 0.66 Tabelle 8.1: Laufzeitvergleich der Verfahren zur Berechnung der Polynomkoeffizienten. Der jeweils schnellste Algorithmus ist grün, der zweit schnellste gelb hinterlegt. Pixeln bestimmt.1 Leider war es nicht möglich die Messungen für die einzelnen Teilaufgaben des Raycastings separat durchzuführen, da der verwendete NVidia-Forceware-Treiber die GLSL-Shader, die nur die Berechnung der Polynomkoeffizienten enthielten, aus unbekannten Gründen als fehlerhaft zurückwies. Daher wird im Folgenden jeweils die Gesamtzeit zur Berechnung eines Bildes betrachtet. 8.3.1 Berechnung der Polynomkoeffizienten Zum Laufzeitvergleich der verschiedenen Algorithmen zur Berechnung der Polynomkoeffizienten wurde bei allen Tests für die Berechnung der Polynomnullstellen das D-Chain-Verfahren mit Bisektion fest eingestellt. Tabelle 8.1 enthält die Ergebnisse der Messungen. Die Termumformungen auf der GPU erreichen bei fast allen getesteten Flächen die höchste Bildwiederholrate. Interessant sind die Messergebnisse der Flächen boy und insbesondere stagnaro, deren Formeln im Verhältnis zu den anderen Testflächen sehr komplex sind. Während bei den Interpolationsverfahren nur die Berechnung der Interpolationspunkte von der Flächenformel abhängt, werden bei den Termumformungen auf der GPU komplexe Operationen wie Polynomaddition und -multiplikation anhand dieser Formel durchgeführt. Diese wirken sich in solchen Fällen aufgrund ihrer linearen beziehungsweise quadratischen Laufzeit besonders auf die Gesamtlaufzeit des Raycastings aus. Deshalb ist es ratsam, die dem Programm übergebene Formel so weit wie möglich zu vereinfachen. 8.3.2 Berechnung der Polynomnullstellen Die Polynomnullstellen können bei den getesteten Flächen vom Grad eins bis vier am schnellsten mit den Lösungsformeln berechnet werden (Tabelle 8.2). Die Lösungsformel für lineare 1 Die Messung erfolgte durch wiederholtes Rendern der Szene in einem Zeitraum t von mindestens fünf Sekunden. Die Anzahl gerenderter Bilder in diesem Zeitraum wurde anschließend durch t geteilt. 75 8 Ergebnisse Flächen Name plane hyperboloid cayley clebsch steiner kummer stagnaro boy barth6 septic octic barth10 roundedcube Grad dcb dcrf 1 2 3 3 4 4 5 6 6 7 8 10 12 136.00 45.63 20.00 10.11 10.18 7.70 5.60 4.33 3.68 5.86 1.74 0.62 1.90 127.50 5.82 2.01 0.98 0.96 0.74 0.64 0.48 0.34 0.65 0.23 0.11 0.24 Messergebnisse in fps sk wba m 135.00 58.50 40.50 21.66 11.07 13.68 8.60 6.00 12.40 2.63 2.61 1.20 – 19.19 12.90 8.60 4.33 4.17 3.63 2.80 4.14 2.58 2.29 1.37 0.54 0.84 129.50 115.50 14.50 14.66 8.06 7.70 3.27 2.67 3.29 2.05 1.99 0.35 0.57 l lf 133.00 115.00 16.61 14.49 4.09 8.73 3.00 2.34 2.67 2.04 1.40 0.74 0.58 137.50 118.50 70.50 35.71 43.00 31.50 – – – – – – – Tabelle 8.2: Laufzeitvergleich der Verfahren zur Berechnung der Polynomnullstellen. Der jeweils schnellste Algorithmus ist grün, der zweit schnellste gelb hinterlegt. Die Lösungsformeln (lf) nehmen eine gewisse Sonderstellung ein, da sie nur für die Grade eins bis vier anwendbar sind, und wurden deshalb nicht eingefärbt. Gleichungen dient bei allen anderen Algorithmen außer der Wertebereichsanalyse als Basisfall, wodurch sich auch die ähnlichen Messergebnisse beim Raycasting der Ebene erklären lassen. Die Lösungsformel für quadratische Gleichungen kommt bei der Muller- und der LaguerreIteration zum Einsatz, so dass diese Algorithmen für die Fläche vom Grad zwei noch eine verhältnismäßig hohe Bildwiederholrate erzielen können. Zur Beschleunigung des D-ChainVerfahrens, der Muller- und der Laguerre-Iteration könnten auch die Lösungsformeln vom Grad größer eins beziehungsweise zwei als Basisfall verwendet werden. Dies führt jedoch in der derzeitigen Implementierung zu ähnlichen Bildfehlern wie in Abbildung 8.3 und wurde daher nicht näher untersucht. Für alle getesteten Flächen schneidet auch der Algorithmus von Sturm sehr gut ab. Im Gegensatz zur Wertebereichsanalyse, die in den Tests eine relativ hohe Laufzeit aufweist, kann damit bestimmt werden, ob in einem Intervall genau eine Nullstelle und damit genau ein Vorzeichenwechsel liegt. Daher kann frühzeitig auf ein schnelles, lokal konvergentes Verfahren umgeschaltet werden. Zudem berechnet der Algorithmus von Sturm, wie auch die Wertebereichsanalyse, unabhängig vom Flächengrad nur die zum Raycasting benötigte Nullstelle. Das D-Chain-Verfahren mit Bisektion ist in Tabelle 8.2 häufig als zweit schnellster und teilweise auch als schnellster Algorithmus markiert. Die GLSL-Implementierung der RegulaFalsi konvergiert hingegen so schlecht, dass schon für Flächen vom Grad drei keine interaktiven Bildwiederholraten erzielt werden. 8.4 Abschließende Bewertung der implementierten Verfahren Die Algorithmen zum Raycasting algebraischer Flächen wurden eingehend bezüglich Bildqualität und Laufzeit untersucht. Die Termumformungen auf der GPU und das D-Chain-Verfahren 76 8.5 Zusammenfassung mit Bisektion konnten in beiden Kategorien überzeugen. Darstellungsqualität und Laufzeit sind bei allen Testflächen im oberen Bereich angesiedelt. Daher wird in RealSurf standardmäßig die Kombination dieser Algorithmen zum Raycasting der Flächen genutzt. Alle anderen Verfahren außer der Wertebereichsanalyse verursachen flächenabhängig Bildfehler und sind daher in ihrer bisherigen Implementierung zum Raycasting algebraischer Flächen nur bedingt geeignet. Trotz der guten Darstellungsqualität der Wertebereichsanalyse ist die Laufzeit im Vergleich zum D-Chain-Verfahren mit Bisektion eher schlecht. Obwohl man für eine echtzeitfähige Darstellung im Allgemeinen eine Bildwiederholrate von mindestens 15 Bildern pro Sekunde fordert, ist zum interaktiven Betrachten der Flächen eine Bildwiederholrate von etwa 8 Bildern pro Sekunde, wie sie vom D-Chain-Verfahren mit Bisektion auf der getesteten Hardware beispielsweise noch für den abgerundeten Würfel x6 + y 6 + z 6 − 1 = 0 vom Grad 6 erreicht wird, dennoch akzeptabel. In den Tabellen 8.1 und 8.2 wurden Testrenderings von Flächen bis zum Grad zwölf betrachtet. Aufgrund der beschränkten Ressourcen von GPUs bezüglich Codegröße, Ausführungszeit und Registeranzahl können auf den getesteten Grafikkarten nur wenige Flächen vom Grad größer als zehn gerendert werden. Die Flächen vom Typ xk + y k + z k − 1 = 0 konnten bis k = 13 erfolgreich dargestellt werden. 8.5 Zusammenfassung In dieser Arbeit wurde die Verwendbarkeit moderner Grafikhardware zum Raycasting algebraischer Flächen untersucht. Der Einsatz von Spezialhardware erfordert häufig Anpassungen an den benötigten Algorithmen. Diese Anpassungen, die auf Einschränkungen durch Hardware, Programmiermodell, Programmiersprache und fehlerbehaftete Grafikkartentreiber zurückzuführen sind, binden die entwickelten Programme sehr stark an die der Implementierung zugrunde gelegten Hardware. Die Implementierungsphase dieser Arbeit erwies sich als sehr zeitaufwendig und kompliziert, da einige Einschränkungen zu diesem Zeitpunkt nicht bekannt waren und erst während der Programmierung der Algorithmen entdeckt wurden. Hinzu kommt die mit 32 Bit für dieses Einsatzgebiet geringe Genauigkeit von Fließkommazahlen. Dennoch wurden verschiedene Algorithmen für die Berechnung der Schnittpunkte zwischen algebraischer Fläche und Raycasting-Strahl erfolgreich in der OpenGl-Shading-Language umgesetzt. Die meisten der untersuchten Algorithmen sind zum Raycasting algebraischer Flächen nur bedingt geeignet. Die erste Teilaufgabe, der Berechnung der Koeffizienten des Polynoms f (t), welches durch Einsetzen des Raycasting-Strahls in die Formeln F (x, y, z) der algebraischen Fläche entsteht, kann zuverlässig und effizient durch Termumformungen auf der GPU erledigt werden. Bei der anschließenden Berechnung der Nullstellen des Polynoms f (t) wird durch das D-Chain-Verfahren mit Bisektion sowohl eine gute Bildqualität als auch ein schnelles Rendering ermöglicht. Aufbauend auf der Leistungsfähigkeit programmierbarer GPUs ist es in dieser Arbeit gelungen, eine Anwendung zur interaktiven Visualisierung algebraischer Flächen zu realisieren. Die erreichten Bildwiederholraten sind allerdings stark flächen- und blickrichtungsabhängig. Bei einer Auflösung von 512 × 512 Bildpunkten können auf einer GeForce 6600 GT algebraische Flächen teilweise bis zum Grad vier in Echtzeit gerendert werden. Mit zunehmender Flächenkomplexität erhöht sich natürlich die Berechnungsdauer. Die Grenze der verfügbaren Ressourcen ist auf NVidia-Grafikkarten der Serien GeForce 6 und 7 bei Flächen vom Grad 13 erreicht. 77 8 Ergebnisse 8.6 Ausblick Der beschriebene Raycasting-Algorithmus kann auch auf der der Implementierung zugrunde gelegten Hardware noch verbessert werden. Die derzeitige Implementierung untersucht pro Pixel genau einen Raycasting-Strahl. Dadurch entstehen Treppeneffekte an den Rändern der Flächen. In einem Multipass-Verfahren könnten die Randpixel bestimmt und in einem weiteren Durchgang verfeinert werden. Weiterhin könnte die Beschränkung der Flächenkomplexität durch Multipass-Verfahren abgeschwächt werden. Dazu ist es nötig, den Raycasting-Algorithmus so aufzuteilen, dass die Zwischenergebnisse geeignet im Framebuffer abgelegt werden können. In dieser Arbeit wird eine Beschleunigung des Raycasting-Algorithmus durch direkten Einsatz von Fragment-Shadern erzielt. Wie bereits in Kapitel 2 beschrieben wurde, existieren spezielle Frameworks zur Umsetzung datenparalleler Prozesse auf die GPU. Sie verbergen die Details der GPU-Programmierung und ermöglichen dadurch eine effiziente Softwareentwicklung. Möglicherweise kann die durchaus komplizierte Implementierung der oben genannten Multipass-Algorithmen durch den Einsatz solcher Frameworks umgangen werden. Auch eine Erweiterung des Raycasting-Algorithmus auf ein rekursives Raytracing zur Realisierung von Spiegelungen, Brechungen und Schatten ist denkbar. 78 A Renderings und Flächenbeschreibungen Zur Abrundung und Vervollständigung der Thematik enthalten die folgenden Abschnitte dieses Anhangs mit RealSurf erstellte Renderings verschiedener algebraischer Flächen. Dazu zählen die im Ergebnisteil verwendeten Testflächen, sowie eine Sammlung weiterer bekannter Flächen, die zum Teil in den vorhergehenden Kapiteln erwähnt wurden. A.1 Zum Laufzeitvergleich genutzte algebraische Flächen Ebene (plane) Zweischaliges Hyperboloid (hyperboloid) Cayley-Fläche (cayley) Clebsch-Fläche (clebsch) Steiner-Fläche (steiner) Kummer-Fläche (kummer) Stagnaro-Fläche (stagnaro) Boysche Fläche (boy) Barthsche Fläche vom Grad 6 (barth6) 79 A Renderings und Flächenbeschreibungen Septik mit singulärer Kreislinie (septic) Oktik mit 168 singulären Punkten (octic) Barthsche Fläche vom Grad 10 (barth10) Abgerundeter Würfel vom Grad 12 (roundedcube) Kürzel 80 Grad Flächenbeschreibung plane 1 x=0 hyperboloid 2 x2 − y 2 + z 2 + 1 = 0 cayley 3 clebsch 3 steiner 4 kummer 4 stagnaro 5 boy 6 barth6 6 4(τ 2 x2√ −y 2 )(τ 2 y 2 −z 2 )(τ 2 z 2 −x2 )−(1+2τ )(x2 +y 2 +z 2 −1)2 = 0 mit τ = 1 2 (1 + 5) septic 7 (x2 + y 2 − 1)3 y + z 2 = 0 octic 8 barth10 10 roundedcube 12 x2 + y 2 + z 2 + 2xyz − 1 = 0 81(x3 + y 3 + z 3 ) − 189(x2 (y + z) + y 2 (x + z) + z 2 (x + y)) + 54(xyz) + 126(xy + xz + yz) − 9(x2 + y 2 + z 2 ) − 9(x + y + z) + 1 = 0 x2 y 2 + y 2 z 2 + z 2 x2 − xyz = 0 √ −x4 − y 4 − z 4 + 4(x2 + y 2 z 2 + y 2 + x2 z 2 + z 2 + y 2 x2 ) − 12 3xyz − 1 = 0 (p + r)2 (q 3 + s3 ) + (q + s)2 (p3 + r3 ) + 8(qrs + prs + pqs +√pqr)(q + 2 r 2 (q + s) = 0 mit p = z − 1 + 2x, q = s)(p + r)√+ 4q 2 s2 (p + r) + 4p√ √ z − 1 − 2x, r = −(z + 1 + 2y) und s = −(z + 1 − 2y) 64(1 − z)3 z 3 − 48(1√ − z)2 z 2 (3x2 + 3y 2 + 2z 2 ) + 12(1 − z)z(27(x2 + y 2 )2 − 2 2 2 24z (x + y ) + 36 2yz(y 2√ − 3x2 ) + 4z 4 ) + (9x2 + 9y 2 − 2z 2 )(−81(x2 + 2 2 2 2 2 y ) − 72z (x + y ) + 108 2xz(x2 − 3y 2 ) + 4z 4 ) = 0 (8x4 − 8x2 + 1)2 + (8y 4 − 8y 2 + 1)2 + (8z 4 − 8z 2 + 1)2 − 1 = 0 2 )+ 8(x2 −τ 4 y 2 )(y 2 −τ 4 z 2 )(z 2 −τ 4 x2 )(x4 +y 4 +z 4 −2x2 y 2 −2y 2 z 2 −2z 2 x√ (3+5τ )(x2 +y 2 +z 2 −1)2 (x2 +y 2 +z 2 −(2−τ ))2 = 0 mit τ = 21 (1+ 5) x12 + y 12 + z 12 − 1 = 0 A.2 Weitere bekannte algebraische Flächen A.2 Weitere bekannte algebraische Flächen Whitney-Umbrella (whitney) Ding Dong (dingdong) weitere Kubik (cubic) Octdong (octdong) Quartik mit tetraedrischer Symmetrie (tetrahedral) weitere Quartik (quartic) Torus (torus) Tropfen (blob) Cassini-Fläche (cassini) Chubs (chubs) Cross Cap (crosscap) Tangled Cube (tangledcube) Enzensberger Stern (enzensberger) Herz (heart) Huntsche Fläche (hunt) 81 A Renderings und Flächenbeschreibungen Kleinsche Flasche (kleinbottle) Kürzel 82 Doppeltorus (doubletorus) Grad Fläche vom Grad 13 (surf13) Flächenbeschreibung z 2 − x2 y = 0 whitney 3 dingdong 3 cubic 3 octdong 4 tetrahedral 4 quartic 4 torus 4 blob 4 cassini 4 chubs 4 crosscap 4 x4 + tangledcube 4 x4 + enzensberger 6 400(x2 y 2 + y 2 z 2 + z 2 x2 ) − (1 − x2 − y 2 − z 2 )3 = 0 heart 6 hunt 6 kleinbottle 6 doubletorus 8 surf13 13 x2 + y 3 − y 2 + z 2 = 0 4(x3 + y 3 + z 3 + 1) − (x + y + z + 1)3 = 0 x2 + y 2 + z 4 − z 2 = 0 (x2 + y 2 + z 2 )2 + 8xyz − 10(x2 + y 2 + z 2 ) + 25 = 0 x4 + y 4 + z 4 − 15xyz = 0 (x2 + y 2 + z 2 + R2 − r2 )2 − 4R2 (x2 + y 2 ) = 0 mit r = 0.3 und R = 1 4(x2 + y 2 ) − 1 + 2z − 2z 3 + z 4 = 0 ((x − 1)2 + z 2 )((x + 1)2 + z 2 ) − y 4 = 0 1 2 =0 3 2 2 5 2 2 2 2 2 x y − 3x y + 2 x z + 2 y z − 3yz 2 + 23 z 4 y 4 + z 4 − 1.6(x2 + y 2 + z 2 ) + 1.024 = 0 x4 + y 4 + z 4 − x2 − y 2 − z 2 + (2x2 + y 2 + z 2 − 1)3 − 1 2 3 10 x z =0 − y2 z3 = 0 4(x2 + y 2 + z 2 − 13)3 + 27(3x2 + y 2 − 4z 2 − 12)2 = 0 (x2 + y 2 + z 2 + 2y − 1)((x2 + y 2 + z 2 − 2y − 1)2 − 8z 2 ) + 16xz(x2 + y 2 + z 2 − 2y − 1) = 0 (x2 (1 − x2 ) − y 2 )2 + 12 z 2 − x13 + y 13 + z 13 − 1 = 0 1 80 (1 + x2 + y 2 + z 2 ) = 0 B Grammatik der Formeln algebraischer Flächen Der in RealSurf implementierte Parser akzeptiert die zur folgenden Grammatik gehörende Sprache. Der erste Teil der Grammatik ist regulär und wird vom Scanner-Generator Flex [Frea] verwendet. Der daraus erstellte Scanner bestimmt Token für den Parser-Generator Bison [Freb]. Zusätzlich entfernt er Kommentare und Leerzeichen zwischen Token aus der Eingabe. Als Kommentar werden alle Zeichen zwischen /∗ und ∗/ und zwischen // und dem Zeilenende betrachtet. Der zweite Grammatikteil müsste zur Eingabe in Bison als LALR(1)Grammatik vorliegen. Die hier abgedruckte kontextfreie Variante der Grammatik ist jedoch deutlich übersichtlicher. Die LALR(1)-Grammatik ist im RealSurf-Quelltext enthalten. Der generierte Parser prüft die syntaktische Korrektheit einer Eingabe und baut daraus einen Syntaxbaum für die weitere Verarbeitung auf. Beide Grammatikteile sind in erweiterter BackusNaur-Form nach ISO/IEC 14977:1996(E) [JTC96] verfasst. B.1 Grammatik für Flex letter = "a" | "l" | "w" | "H" | "S" | | | | | "b" "m" "x" "I" "T" | | | | | "c" "n" "y" "J" "U" | | | | | "d" "o" "z" "K" "V" | | | | | "e" "p" "A" "L" "W" | | | | | "f" "q" "B" "M" "X" | | | | | "g" "r" "C" "N" "Y" | | | | | "h" "s" "D" "O" "Z" | | | | ; "i" "t" "E" "P" | | | | "j" "u" "F" "Q" | | | | "k" "v" "G" "R" nonzerodigit = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; digit = "0" | nonzerodigit ; integer = "0" | ( nonzerodigit { digit } ) ; float = integer [ "." digit { digit } ] [ ( "E" | "e" ) [ "+" | "-" ] digit { digit } ] ; x y z = "x" | "X" ; = "y" | "Y" ; = "z" | "Z" ; 83 B Grammatik der Formeln algebraischer Flächen pi e = "PI" ; = "E" ; function1 = "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "exp" | "log" | "sqrt" | "ceil" | "floor" | "abs" | "sign" ; function2 = "pow" | "atan2" ; reserved = x | y | z | pi | e | function1 | function2 ; identifier = ( letter { letter | digit } ) - reserved ; B.2 Grammatik für Bison constant = integer | float ; parameter = constant | identifier | "-" parameter | parameter "+" | parameter "-" | parameter "*" | parameter "/" | parameter "^" | "(" parameter | function1 "(" | function2 "(" parameter parameter parameter parameter integer ")" parameter ")" parameter "," parameter ")" ; (* Startsymbol *) polynomial = x | y | z | parameter | "-" polynomial | polynomial "+" | polynomial "-" | polynomial "*" | polynomial "/" | polynomial "^" | "(" polynomial 84 polynomial polynomial polynomial parameter integer ")" ; C Inhalt der DVD Die dieser Arbeit beliegende DVD enthält • den Quelltext der entwickelten Anwendung RealSurf • eine für Microsoft Windows (32 Bit) übersetzte Version des Programms • ein RealSurf-Demonstrationsvideo, da das Programm nicht mit allen OpenGL 2.0 Grafikkarten kompatibel ist (getestet wurden die Grafikkarten NVidia Quadro FX 4000, GeForce 6600 GT, GeForce 6800 GT und GeForce 7600 GS) • das in Abschnitt 2.3.3 vorgestellte GLSL-Beispiel • diese Diplomarbeit im PDF-Format. 85 C Inhalt der DVD 86 Literaturverzeichnis [3Dl] 3Dlabs: GLSLvalidate. http://developer.3dlabs.com/downloads/glslvalidate/ index.htm, Abruf: 23. April 2007 [Abe26] Abel, Niels H.: Beweis der Unmöglichkeit algebraische Gleichungen von höheren Graden als dem vierten allgemein aufzulösen. In: Journal für die reine und angewandte Mathematik, 1826, 65–84 [AMMH02] Akenine-Möller, Tomas ; Moller, Tomas ; Haines, Eric: Real-Time Rendering. A. K. Peters, Ltd., 2002. – ISBN 1–56881–182–9 [App68] Appel, Arthur: Some techniques for shading machine renderings of solids. In: Proceedings of the Spring Joint Computer Conference 32 (1968), S. 37–49 [ATI] ATI: RenderMonkey. http://ati.amd.com/developer/rendermonkey/index.html, Abruf: 23. April 2007 [BFH+ 04] Buck, I. ; Foley, T. ; Horn, D. ; Sugerman, J. ; Fatahalian, K. ; Houston, M. ; Hanrahan, P.: Brook for GPUs: stream computing on graphics hardware. In: ACM Transactions on Graphics (TOG) 23 (2004), Nr. 3, S. 777–786 [Boy02] Boyd, John P.: Computing Zeros on a Real Interval through Chebyshev Expansion and Polynomial Rootfinding. In: SIAM Journal on Numerical Analysis 40 (2002), Nr. 5, S. 1666–1682. – ISSN 0036–1429 [Chi00] Childs, Lindsay N.: A Concrete Introduction to Higher Algebra. 2. Springer, 2000. – ISBN 0–387–98999–4 [CHL04] Coombe, Greg ; Harris, Mark J. ; Lastra, Anselmo: Radiosity on graphics hardware. In: GI ’04: Proceedings of the 2004 conference on Graphics interface, Canadian Human-Computer Communications Society, 2004. – ISBN 1–56881– 227–2, S. 161–168 [Cra] Crane, Keenan: GPU Fluid Solver. http://graphics.cs.uiuc.edu/svn/kcrane/ web/project_fluid.html, Abruf: 19. März 2007 [EMNW05] Engeln-Müllges, Gisela ; Niederdrenk, Klaus ; Wodicka, Reinhard: Numerik-Algorithmen: Verfahren, Beispiele, Anwendungen. Springer, 2005. – ISBN 3–540–62669–7 [ESSB] Endrass, Stephan ; Schneider, Hans Huelf Ruediger Oertel K. ; Schmitt, Ralf ; Beigel, Johannes: Surf - Visualization of real algebraic geometry. http:// surf.sourceforge.net/, Abruf: 10. Mail 2007 [Frea] Free Software Foundation: flex: a fast lexical analyzer. sourceforge.net/, Abruf: 30. Mai 2007 http://flex. 87 Literaturverzeichnis [Freb] Free Software Foundation: GNU Bison, the GNU parser generator. http:// www.gnu.org/software/bison/, Abruf: 30. Mai 2007 [GG83] Grant, J. A. ; Ghiatis, A.: Determination of the Zeros of a Linear Combination of Chebyshev Polynomials. In: IMA Journal of Numerical Analysis 3 (1983), Nr. 2, S. 193 [Gie02] Giedat, Rainer: Numerische Interpolation mit Tschebyscheffschen Polynomen. Version: 2002. http://www2.informatik.hu-berlin.de/~haddenho/pdf/ tschebyscheff_interpolation.pdf, Abruf: 16. April 2007 [Her95] Herold, Helmut: lex und yacc. 2. Addison-Wesley, 1995. – ISBN 3–89319–879–2 [Heu00] Heuser, Harro G.: Lehrbuch der Analysis 1. 13. BG Teubner Verlag, 2000. – ISBN 3–519–42235–2 [Jen96] Jensen, Henrik W.: Global illumination using photon maps. In: Rendering Techniques 96 (1996), S. 21–30 [JT70] Jenkins, M. A. ; Traub, J. F.: A Three-Stage Algorithm for Real Polynomials Using Quadratic Iteration. In: SIAM Journal on Numerical Analysis 7 (1970), Nr. 4, S. 545–566 [JTC96] JTC, ISO: Information technology–Syntactic metalanguage–Extended BNF. In: ISO-Standard ISO/IEC 14977 (1996) [KBR04] Kessenich, J. ; Baldwin, D. ; Rost, R.: The OpenGL Shading Language version 1.10. 59. Version: 2004. http://oss.sgi.com/projects/ogl-sample/registry/ ARB/GLSLangSpec.Full.1.10.59.pdf, Abruf: 27. März 2007 [Kea96] Kearfott, R. B.: Interval computations: Introduction, uses, and resources. In: Euromath Bulletin 2 (1996), Nr. 1, S. 95–112 [KF05] Kilgariff, Emmett ; Fernando, Randima: The GeForce 6 Series GPU Architecture. In: GPU Gems 2. Addison Wesley, März 2005, Kapitel 30, S. 471–491 [KGGK94] Kumar, V. ; Grama, A. ; Gupta, A. ; Karypis, G.: Introduction to parallel computing: design and analysis of algorithms. Benjamin-Cummings Publishing Co., Inc. Redwood City, CA, USA, 1994. – ISBN 0–8053–3170–0 [Kön04] Königsberger, Konrad: Analysis. 2. 5. Springer, 2004. – ISBN 978–3–540– 20389–6 [LKHW05] Lefohn, Aaron E. ; Kniss, Joe M. ; Hansen, Charles D. ; Whitaker, Ross T.: A streaming narrow-band algorithm: interactive computation and visualization of level sets. In: SIGGRAPH ’05: ACM SIGGRAPH 2005 Courses, ACM Press, 2005, S. 243 [LMB95] Levine, John R. ; Mason, Tony ; Brown, Doug: Lex & Yacc. 2. O’Reilly, 1995. – ISBN 1–56592–000–7 88 Literaturverzeichnis [Lue] Luebke, David: GPGPU: General-Purpose Computation on Graphics Hardware. http://www.gpgpu.org/sc2006/slides/01.luebke.Introduction.pdf, Abruf: 19. März 2007 [LWS93] Lafortune, Eric P. ; Willems, Yves D. ; Santo, H. P.: Bi-directional Path Tracing. In: Proceedings of CompuGraphics (1993), S. 145–153 [Mat78] Matthiessen, Ludwig: Grundzüge der antiken und modernen Algebra der litteralen Gleichungen. 1. BG Teubner Verlag, 1878 http://historical.library.cornell. edu/cgi-bin/cul.math/docviewer?did=01920001&seq=7 [Mig92] Mignotte, Maurice: Mathematics for computer algebra. Springer-Verlag New York, 1992. – ISBN 3–540–97675–2 [MMWW99] Merziger, Gerhard ; Mühlbach, Günter ; Wille, Detlef ; Wirth, Thomas: Formeln und Hilfen zur höheren Mathematik. 3. Binomi Verlag, 1999. – ISBN 978–3–923923–35–9 [Mul56] Muller, David E.: A Method for Solving Algebraic Equations Using an Automatic Computer. In: Mathematical Tables and Other Aids to Computation 10 (1956), Nr. 56, S. 208–215 [NVi] NVidia: NVemulate. http://developer.nvidia.com/object/nvemulate.html, Abruf: 23. April 2007 [NVi06a] NVidia: CUDA Homepage. Version: 2006. http://developer.nvidia.com/object/ cuda.html, Abruf: 21. März 2007 [NVi06b] NVidia: Microsoft DirectX 10: The Next-Generation Graphics API. Version: 2006. http://www.nvidia.de/content/PDF/Geforce_8800/Microsoft_ DirectX_10_Technical_Brief.pdf, Abruf: 19. März 2007 [NVi07] NVidia: Cg Toolkit 1.5. Version: 2007. http://developer.nvidia.com/object/ cg_toolkit.html, Abruf: 23. April 2007 [OLG07] Owens, John D. ; Luebke, David ; Govindaraju, Naga: A Survey of GeneralPurpose Computation on Graphics Hardware. In: Computer Graphics Forum 26 (2007), Nr. 1, S. 80–113 [PBMH02] Purcell, Timothy J. ; Buck, Ian ; Mark, William R. ; Hanrahan, Pat: Ray Tracing on Programmable Graphics Hardware. In: ACM Transactions on Graphics 21 (2002), Nr. 3, S. 703–712 [PDC+ 03] Purcell, Timothy J. ; Donner, Craig ; Cammarano, Mike ; Jensen, Henrik W. ; Hanrahan, Pat: Photon Mapping on Programmable Graphics Hardware. In: Proceedings of the ACM SIGGRAPH/Eurographics Symposium on Graphics Hardware, Eurographics Association, 2003. – ISBN 1–58113–739–7, S. 41–50 [Pol03] Polthier, Konrad: Imaging maths - Inside the Klein bottle. Version: 2003. http://plus.maths.org/issue26/features/mathart/index-gifd.html, Abruf: 30. Mai 2007 89 Literaturverzeichnis [pov] POV-Ray - The Persistence of Vision Raytracer. http://www.povray.org/, Abruf: 10. Mail 2007 [PSG06] Peercy, Mark ; Segal, Mark ; Gerstmann, Derek: A performance-oriented data parallel virtual machine for GPUs. In: SIGGRAPH ’06: ACM SIGGRAPH 2006 Sketches, ACM Press, 2006. – ISBN 1–59593–364–6, S. 184 [PTVF96] Press, William H. ; Teukolsky, Saul A. ; Vetterling, William T. ; Flannery, Brian P.: Numerical Recipes in C: The Art of Scientific Computing. 2. Cambridge University Press, 1996. – ISBN 0–521–43108–5 [Rap06] RapidMind Inc.: Sh - Embedded Metaprogramming Language. Version: 2006. http://www.libsh.org/, Abruf: 27. März 2007 [SA04] Segal, Mark ; Akeley, Kurt: The OpenGL Graphics System: A Specification, Version 2.0. Version: 2004. http://www.opengl.org/documentation/specs/ version2.0/glspec20.pdf, Abruf: 27. März 2007 [Sch] Schmidt, Meik: Wie alles begann - DirectX 5, DirectX 6, DirectX 7, DirectX 8 und DirectX 9. http://www.pc-erfahrung.de/grafikkarte/directx.html, Abruf: 19. März 2007 [SSS+ 06] Shou, Huahao ; Song, Wenhao ; Shen, Jie ; Martin, Ralph ; Wang, Guojin: A Recursive Taylor Method for Ray Casting Algebraic Surfaces. In: CGVR, 2006, S. 196–204 [Sto02] Stoer, Josef: Numerische Mathematik 1. 8. Springer, 2002. – ISBN 3–540– 66154–9 [Sun] SunFlow Rendering 11. April 2007 System. http://sunflow.sourceforge.net/, Abruf: [TS05] Thrane, Niels ; Simonsen, Lars O.: A Comparison of Acceleration Structures for GPU Assisted Ray Tracing. Version: 2005. http://www.larsole.com/files/ GPU_BVHthesis.pdf [Tur90] Turkowski, Ken: Properties of surface-normal transformations. In: Graphics gems, Academic Press Professional, Inc., 1990. – ISBN 0–12–286169–5, S. 539– 547 [Wat82] Watkins, David S.: Understanding the QR Algorithm. In: SIAM Review 24 (1982), Nr. 4, S. 427–440 [Wei99] Weisstein, Eric W.: Kummer Surface. Version: 1999. wolfram.com/KummerSurface.html, Abruf: 19. Juni 2007 http://mathworld. [Whi80] Whitted, Turner: An Improved Illumination Model for Shaded Display. In: Communications of the ACM 23 (1980), Nr. 6, S. 343–349. – ISSN 0001–0782 [WMK04] Wood, Andrew ; McCane, Brendan ; King, Scott A.: Ray Tracing Arbitrary Objects on the GPU. In: Proceedings of Image and Vision Computing New Zealand (2004), S. 327–332 90 Literaturverzeichnis [Woo] Woodhouse, Ben: GL Easy Extension. http://elf-stone.com/glee.php, Abruf: 23. April 2007 [WSS05] Woop, Sven ; Schmittler, Jörg ; Slusallek, Philipp: RPU: A Programmable Ray Processing Unit for Realtime Ray Tracing. In: Proceedings of ACM SIGGRAPH 2005, 2005, 434–444 91 Literaturverzeichnis 92 Abbildungsverzeichnis 2.1 2.2 2.3 2.4 2.5 2.6 Beispiele von GPGPU-Anwendungen . . . . . . . . . . . . . . . Vergleich des Wachstums der CPU- und GPU-Geschwindigkeit Vereinfachter Aufbau einer Fixed-Function-Grafikpipeline . . . Vereinfachter Aufbau einer programmierbaren Grafikpipeline . . Mögliche Organisation der Berechnungen auf Quadrupeln . . . Einzelschritte des GLSL-Beispiels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 5 6 8 18 3.1 3.2 3.3 Generierung der Strahlen beim rekursiven Raytracing . . . . . . . . . . . . . . Vergleich zwischen Raycasting, rekursivem Raytracing und Photon-Mapping . GPU-basiertes Raycasting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 23 24 4.1 4.2 4.3 Abbildungen von Quadriken . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verwendung des Normalenvektors bei der Beleuchtungsberechnung . . . . . . Beispiele nicht-orientierbarer Flächen . . . . . . . . . . . . . . . . . . . . . . . 27 29 30 5.1 5.2 5.3 Sensibiltät der Polynominterpolation gegenüber Störungen . . . . . . . . . . . Rechenschema zur Bestimmung der dividierten Differenzen . . . . . . . . . . . Berechnung der dividierten Differenzen mit linearem Speicheraufwand . . . . 35 37 38 6.1 6.2 Beispielhafte Ausführung des Bisektionsverfahrens . . . . . . . . . . . . . . . Beispielhafte Ausführung der Regula-Falsi . . . . . . . . . . . . . . . . . . . . 49 50 7.1 7.2 7.3 7.4 7.5 Erläuterung zur Implementierung des D-Chain-Verfahrens . . . . Syntaxbaum zur Eingabe x3 ∗ (x − 1) + y 2 + z 2 . . . . . . . . . . Startansicht der grafischen Benutzeroberfläche von RealSurf . . Aktivitätsdiagramm der RealSurf-Benutzeroberfläche . . . . . Animation zwischen verschiedenen Flächen der Kummer-Familie . . . . . 62 64 66 66 67 8.1 8.2 8.3 8.4 8.5 8.6 Verbesserung der Bildqualität durch Optimierung der Strahlen . . . . . . . . Qualitätsvergleich der Algorithmen zur Berechnung der Polynomkoeffizienten Qualitätsvergleich der Lösungsformeln . . . . . . . . . . . . . . . . . . . . . . Qualitätsvergleich der Verfahren zur Nullstellenisolation . . . . . . . . . . . . Darstellungfehler bei Verwendung der global konvergenten Iterationsverfahren Skalierbarkeit der Raycasting-Algorithmen . . . . . . . . . . . . . . . . . . . . 69 72 72 73 73 74 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 Abbildungsverzeichnis 94 Tabellenverzeichnis 8.1 8.2 Laufzeitvergleich der Verfahren zur Berechnung der Polynomkoeffizienten . . . Laufzeitvergleich der Verfahren zur Berechnung der Polynomnullstellen . . . . 75 76 95