G ENERATIVE M ODELLIERUNG Diplomarbeit, vorgelegt von Sven Havemann Rheinische Friedrich-Wilhelms-Universität Bonn Institut für Informatik III 30. November 1997 Versicherung Hiermit versichere ich an Eides Statt, daß ich die vorliegende Arbeit „Generative Modellierung“ selbständig verfaßt und keine anderen als die angegebenen Quellen und Hilfsmittel verwendet habe. Bonn, den 7. Dezember 1997 Sven Havemann 3 Danksagung An dieser Stelle möchte ich ganz herzlich all jenen danken, ohne die diese Arbeit nicht in dieser Form möglich gewesen wäre. An erster Stelle ist Herr Professor Dr. D. W. Fellner zu nennen, der mir die Freiheit gegeben hat, eine faszinierende Idee zu verfolgen. Für diese Idee danke ich – unbekannterweise – Herrn John M. Snyder. Der Grafikgruppe, meinen heutigen und den ehemaligen Mitstreitern, gilt mein Dank für die lange gute Zusammenarbeit, die vielen anregenden Diskussionen und die freundschaftliche Atmosphäre innerhalb der Gruppe. – Ihnen verdanke ich das Fundament, das ich benutzen durfte. Widmen möchte ich diese Arbeit meinen Eltern, denen ich ich für das Verständnis danke, das sie mir stets entgegengebracht haben, und die besonders während meines Studiums immer hinter mir standen. 5 Inhaltsverzeichnis 1 Einleitung 1 2 Evolution der Geometrischen Modellierung 2.1 Die Produktion einer Animation . . . . . . . . . . 2.2 Konzepte der Modellierung . . . . . . . . . . . . . 2.3 Präzisierung des Oberflächenbegriffs . . . . . . . . 2.4 Konventionelle Modellieransätze . . . . . . . . . . 2.4.1 Approximationsbasiertes Modellieren . . . 2.4.2 Objektorientiertes Modellieren . . . . . . . 2.4.3 Parametrisches Modellieren . . . . . . . . 2.5 Generatives Modellieren . . . . . . . . . . . . . . 2.5.1 Funktionale Konzepte in der Modellierung 2.5.2 Paradigmenwechsel . . . . . . . . . . . . . 2.6 Zusammenfassung . . . . . . . . . . . . . . . . . 3 Grundlagen 3.1 GENMOD-Programmiersprache nach Snyder 3.2 MRT . . . . . . . . . . . . . . . . . . . . . . 3.3 Open Inventor . . . . . . . . . . . . . . . . . 3.4 GeomView . . . . . . . . . . . . . . . . . . 3.5 TBag . . . . . . . . . . . . . . . . . . . . . 3.6 ActiveVRML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Generative Modellierung 4.1 Generative Modellierung in C++ . . . . . . . . . . . . 4.1.1 Abbildungen als Objekte . . . . . . . . . . . . 4.1.2 Vergleich des Genmod-Paketes mit GENMOD 4.2 Generative Modellierung in Haskell . . . . . . . . . . 4.2.1 Warum Haskell? . . . . . . . . . . . . . . . . 4.2.2 Formulierung von Genmod in Haskell . . . . . 4.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . 5 Implementationen von Genmod in C++ und Haskell 5.1 Implementation in C++ . . . . . . . . . . . . . . . . . 5.1.1 Der Genmod-Kern . . . . . . . . . . . . . . . 5.1.2 Die Kurven- und Flächenbibliothek . . . . . . 5.1.3 Integration von Kurven und Flächen in Genmod 5.2 Implementation in Haskell . . . . . . . . . . . . . . . i . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 10 13 16 17 19 21 23 24 25 26 . . . . . . 27 29 33 37 38 40 41 . . . . . . . 43 44 44 50 54 55 58 62 . . . . . 65 67 67 69 73 75 INHALTSVERZEICHNIS ii 5.2.1 5.2.2 Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Instanzen der Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . 75 77 6 Beispiele und Erfahrungsbericht 6.1 Genmod-Beispiele . . . . . . . . . . . . . . . . . . 6.1.1 Sinusfläche mit Spirale . . . . . . . . . . . . 6.1.2 Morphing . . . . . . . . . . . . . . . . . . . 6.1.3 Sweep-Werkzeug . . . . . . . . . . . . . . . 6.1.4 Bézier-Blending . . . . . . . . . . . . . . . 6.1.5 Bézier-Blending mit Kugelnormale . . . . . 6.1.6 Bézier-Blending mit Kugeltangente . . . . . 6.1.7 Stuhl . . . . . . . . . . . . . . . . . . . . . 6.2 Haskell-Beispiele . . . . . . . . . . . . . . . . . . . 6.2.1 Frenet-Rahmen als Kurven . . . . . . . . . . 6.2.2 Parametrisierte Transformation . . . . . . . . 6.2.3 Funktion als Fläche und patch trimming . . . 6.2.4 Offset einer Fläche in Richtung der Normalen 6.2.5 Parametrisierte Konstruktion: Zaun . . . . . 6.2.6 Parametrisierte Konstruktion: Wendeltreppe . 6.2.7 rail product und wire product . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 80 81 82 83 84 85 86 87 87 88 89 90 91 92 94 7 Zusammenfassung 7.1 Modellierung ist funktional objektorientiert 7.2 Modellieren mit Abbildungen ist effizient . 7.3 Mögliche Realisierungen . . . . . . . . . . 7.4 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 95 96 97 99 Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . 102 A Haskell-Code 105 A.1 Die Datei Genmod.hs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 A.2 Die Datei Vectors.hs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 Kapitel 1 Einleitung In dieser Arbeit soll ein neuer Ansatz zur praktischen Geometrischen Modellierung beschrieben werden, die Generative Modellierung. Die Konstruktion virtueller Räume ist ein aufwendiges Geschäft. Die Vielfalt der Formen und Objekte unserer dreidimensionalen Welt scheint schier unerschöpflich, es verlangt ein hohes Maß an Kreativität, all diese Formen zu erfassen, ihren Aufbau und ihre innere Logik zu entschlüsseln, um sie schließlich nachkonstruieren und in einer eigenen, selbst definierten, simulierten Welt aufleben lassen zu können. Doch dies lohnt den Aufwand, denn die Gegenstände in einer solchen virtuellen Welt sind manipulierbar, man kann ihre Form in schier unmöglicher Weise verändern, eigene physikalische Gesetze wirken lassen, und so kann man Dinge zeigen, die es vorher nicht gab, Räume und Formen abbilden, die in Wirklichkeit nicht existieren und nie existieren werden. Um etwas abbilden zu können, muß man aber zunächst eine Möglichkeit finden, es überhaupt zu beschreiben. Selbst simpelste Gegenstände haben eine innere Logik, es ist nicht damit getan, ihre Form einfach abtasten zu lassen, ihre Oberfläche Punkt für Punkt einzulesen, um sie in den virtuellen Raum aufzunehmen. Daher begann man zunächst damit, nur einfachste Objekte zu verwenden, Kugeln, Quader und Zylinder, so waren die ersten virtuellen Welten von ganz einfachen Gegenständen bevölkert. Kompliziertere Gegenstände erhielt man dadurch, daß man sie aus einfacheren zusammensetzte, zum Beispiel dadurch, daß man Gegenstände verschweißt oder voneinander abzieht, wie bei einer Hantel als Zylinder mit Kugeln an den Enden, oder einem Becher als großem Zylinder, von dem ein kleinerer Zylinder abgezogen wird. Wie aber beschreibt man Gegenstände, die sich nicht als eine Ansammlung von Elementarobjekten betrachten lassen? Man reduziert die Objekte zunächst auf das Wesentliche. Man abstrahiert von massiven Gegenständen und betrachtet nur noch noch ihr Äußeres, die Form, die man darstellen möchte: Ihre Oberfläche. Der nächste Schritt ist also, statt der Gegenstände selber nur noch durch ihre Oberfläche zu beschreiben, aber nicht als Punktmasse, sondern man sucht nach Nähten, nach Knicken in der Oberfläche, zwischen denen kontinuierliche, glatte Bereiche liegen. Dies führt zur Theorie der Freiformflächen und -kurven, wo man eine kompakte Beschreibung für Oberflächenstücke sucht und nach Möglichkeiten, diese einzelnen Stücke, Patches, an Nähten zu verschweißen und so zu einer geschlossenen Hülle zusammenzusetzen. Auf diese Weise sind viele verschiedene Darstellungen von Oberflächentypen entstanden, Freiformkurven und Freiformflächen als Gegenstand der Geometrischen Modellierung wurden die Grundelemente zur rechnerinternen Darstellung virtueller Gegenstände. Modelliert man heute Gegenstände mit einem gängigen Computerprogramm zum Bauen virtueller Räume, einem Modeller, so bietet sich ein ganzer Zoo von Kurven, Flächen und Elementargegenständen, die man miteinander kombinieren kann, um eine virtuelle Szene einzurichten. 1 2 KAPITEL 1. EINLEITUNG Ein Modeller bietet dazu einen ganzen Werkzeugkasten von Hilfsmitteln, derer man sich bedienen kann, um für seine Räume Schränke zu bauen und Tische, Stühle und Lampen. Architekten entwerfen ganze Häuser am Rechner, Ingenieure konstruieren präzise technische Bauteile. Ist ein Gegenstand einmal definiert, kann man ihn in seiner Szene so oft kopieren, wie man möchte, ihn an verschiedenen Stellen in verschiedener Größe darstellen, verzerren, variieren und weiterbehandeln. Man hat sich also einen großen Schritt entfernt davon, Gegenstände als Netze von Punktmassen zu beschreiben und Elementarobjekte zu gruppieren; Freiformflächen sind zur Standardform der Beschreibung von Gegenständen geworden. Einem Computergafiker bieten sich neben der Benutzung eines Modellers noch viele andere Darstellungsmittel. Er kann in eigenen Programmen Softwarebibliotheken verwenden, denen er Freiformflächen zur interaktiven Darstellung übergibt, es existieren Dateiformate, in denen er die Oberfläche von Gegenständen nicht nur als Punktnetz, sondern als Freiformflächen speichern oder über ein Netz zu anderen Rechnern schicken kann, und Programme, die ferngesteuert Transformationen auf Objekte anwenden können und das Ergebnis zurückschicken, und schließlich gibt es Szenenbeschreibungssprachen, in denen ein ganzer virtuelle Raum in hierarchischer Form zusammen mit Ereignissen und Animationsabläufen beschrieben werden kann. Andererseits hat die Beschreibung virtueller Welten auf dieser Ebene auch gravierende Nachteile. Je komplizierter eine Szene wird, je detailreicher die Gegenstände, umso deutlicher wird es, daß man eine Beschreibung auf einem noch höheren Niveau braucht, um diese Welten handhabbar zu halten, um nicht buchstäblich in einem Meer von Flächenstücken zu ertrinken. – Das bedeutet, man sucht nach einer Möglichkeit, die Einflußmöglichkeiten auf eine Handvoll der richtigen Regler zu beschränken, die es einem ermöglichen, mit einem Griff einem Hochhaus ein weiteres Stockwerk zu verpassen, sämtliche Fenster mit einem Schlag auszuwechseln und zu bestimmen, daß statt eines Flachdaches nunmehr ein Sattel- oder ein Mansardendach zu verwenden ist. Balkone sollen nicht mehr eckig und kantig aussehen, sondern man gibt ornamentierte Träger vor, die sie stützen, und die an jeder Stelle individuell, aber nach festen Regeln in die Fassade zu integrieren sind. Um dies zu erreichen, ist ein Modellierer heute gezwungen, selber zu programmieren. Er muß ein Programm schreiben, das ein Modell erzeugt, er kann sich einer Softwarebibliothek bedienen, die ihn in sofern unterstützt, als sie eine Schnittstelle zur Anzeige bestimmter Darstellungsprimitive zur Verfügung stellt. Will man ein Modell programmieren können, also in eigener Weise parametrisieren und regelgesteuert erzeugen, so ist man gezwungen, selber zu programmieren, um aus gegebenen Parametern Objekte zu machen, die von der Schnittstelle des verwendeten Anzeigemoduls verarbeitet werden können. Im einfachsten Fall sind das nur Listen von Dreiecken im Raum, im günstigeren Fall bietet die Schnittstelle die Möglichkeit, eine Reihe von Freiformflächen zu übergeben, oder sie ist gar in der Lage, verschiedene Grundprimitive darzustellen, sodaß man Kugeln oder Zylinder nicht erst in Einzelflächen zerlegen muß. Eine Beschreibungssprache für Modelle auf einem höheren Niveau aber hat sich bisher noch nicht durchgesetzt, und es scheint eine große Herausforderung zu sein, die richtige Sichtweise, die passende Abstraktion für eine solche Darstellung zu finden. Die Generative Modellierung könnte einen wichtigen Schritt in diese Richtung bedeuten. Die Grundidee dazu geht zurück auf ein Buch von John M. Snyder, in dem er beschreibt , wie nicht mit Objekten, sondern mit Abbildungen modelliert wird. Das bedeutet, die oben entworfene regelgeleitete Konstruktion von Gegenständen kann tatsächlich expliziert werden, man erhält eine Möglichkeit, den Konstruktionsprozeß selber zu beschreiben und zu parametrisieren statt nur das Objekt, das schließlich entsteht. Im folgenden soll dazu im einzelnen gezeigt werden, in welcher Weise man diese Idee als 3 konsequente Weiterentwicklung der bestehenden Technologie ansehen kann. Eine Entwicklung in diese Richtung scheint “in der Luft zu liegen”, wenn man die Entwicklung der Modellierung betrachtet und die verwendeten Modeller vergleicht, insbesondere im Hinblick auf die neueste Generation der parametrischen Modeller. Im dritten Kapitel werden die Arbeiten vorgestellt, die als Basis für die vorliegende Implementation der Generativen Modellierung dienen. Zum einen wird der theoretische Hintergrund erläutert sowie der Weg beschrieben, den Snyder gegangen ist, um die Idee des Modellierens mit Geometrieerzeugenden, mit Generatoren, zu realisieren, zum anderen wird die Softwareumgebung erläutert, die der vorliegenden Arbeit zugrundeliegt. In der Abteilung für Computergrafik der Universität Bonn ist der MRT als objektorientierte Programmierplattform entstanden, in die viele computergrafische Verfahren zur Darstellung von Szenen, dem Rendering, eingeflossen sind. Ist ein virtueller Raum gegeben, kann man mit Hilfe des MRT Bilder von fotorealistischer Qualität erzeugen und dabei auf viele Aspekte der Darstellung Einfluß nehmen, spiegelnde und transparente Objekte darstellen, Lichtquellen durch Rauch scheinen oder eine Szene indirekt beleuchten lassen, man kann auf die Oberfläche von Gegenständen Bilder kleben und Objekte aus lichtdurchlässigem Material zeigen, die in ihrem Innern gefärbt sind. Dieser Arbeit nun lag die Idee zugrunde, eine Grundlage zu schaffen, um die Generative Modellierung in den MRT zu integrieren. Das Endziel ist also, eine neue Modellieridee mit der fotorealistischen Darstellung modellierter Gegenstände zu kombinieren. Diese große Aufgabe wurde auf zwei Wegen angegangen. Einmal ist rein technisch die Infrastruktur geschaffen worden, um Abbildungen in der notwendigen Weise verketten zu können, und zwar auf Grundlage eines objektorientierten Designs. Dazu wurden dann die Objekte der Modellierung, Freiformkurven und -flächen, implementiert, und daraus schließlich Abbildungsobjekte gewonnen, mit denen auf diesen Objekten operiert werden kann. Das System gestattet es damit, Funktionen zu schreiben, die Abbildungsobjekte erzeugen, mit denen Freiformobjekte in flexibler Weise manipuliert werden können, und eine Reihe von Beispielen zeigt, wie variabel diese eingesetzt werden können. Damit ist ein Fortschritt gegenüber reinen Anzeigebibliotheken wie oben beschrieben erzielt worden, denn ein Benutzer muß zwar immernoch programmieren, um zu modellieren, aber er muß nicht mehr auf der untersten Ebene ansetzen und Einzelflächen definieren, sondern er hat die Möglichkeit, ein parametrisiertes Objekt zu definieren. Ein Stuhl ist nicht mehr eine Ansammlung von Freiformflächen, sondern ein Werkzeug, der Benutzer spezifiziert nurmehr ein Skelett eines Stuhles, und die zusammengesetzte Abbildung konstruiert für ihn daraus die Einzelflächen, die den eigentlichen darstellbaren Stuhl definieren. Der implementierte objektorientierte Mechanismus gestattet es zudem, zusammengesetzte Abbildungen zur Laufzeit eines Programms zu definieren, denn Abbildungen sind nun Objekte, die angelegt, miteinander verbunden und wieder gelöscht werden können. Das ist gerade der entscheidende Punkt bei der Modellierung, ein Benutzer ist nicht mehr gezwungen, spezielle Funktionen zu schreiben, die eine Softwarebibliothek mit den Datenstrukturen versorgt, die sie als Anzeigeprimitive verlangt, sondern er kann eine Funktion schreiben, um die Konstruktion selber zusammenzusetzen. Das entstandene Softwarepaket ist also dazu prädestiniert, weniger als Programmierschnittstelle für Anwendungsprogrammierer zu dienen, sondern vielmehr in ein Programm integriert zu werden, mit dem Konstruktionen zur Laufzeit beschrieben werden können. Dies kann prinzipiell auf zwei Weisen geschehen: Einmal in Form eines interaktiven Modellers, zum anderen befehlsgesteuert. Solche Befehle kann man in einer Datei speichern, die als Programmiersprache für Geometrische Modellierung beschreibt, wie einerseits die allgemeinen Werkzeuge und andererseits die speziellen Parameter aussehen, die für ein bestimmtes Modell verwendet werden sollen. Der Benutzer ist also nicht mehr auf die feste Menge von Werkzeugen angewiesen, die ihm vom KAPITEL 1. EINLEITUNG 4 Modeller serviert werden, sondern er kann modellspezifische Werkzeuge entwerfen, in einer Datei speichern, aus einer Datei einlesen und zu anderen Rechnern schicken. Im zweiten Teil dieser Arbeit wurde daher nach einem Sprachkonzept gesucht, das geeignet ist, als Beschreibungssprache und Dateiformat für zusammengesetzte Abbildungen zu dienen, als Programmiersprache für Geometrische Modellierung. Dabei hat sich gezeigt, daß das imperative Sprachparadigma bestimmten Beschränkungen unterliegt, die eine prinzipielle Grenze darstellen. Als dem Problemfeld wesentlich besser angepaßt erscheint dagegen das funktionale Programmierparadigma, das sich dadurch auszeichnet, daß die fundamentale Operation des Modellierens mit Abbildungen, die Komposition, integraler Bestandteil einer funktionalen Sprache ist. Es wurde daher eine Minimalimplementation der wesentlichen Kurven und Flächen in einer funktionalen Sprache durchgeführt. An Hand einer Reihe von Beispielen kann sodann demonstriert werden, daß sich eine solche Sprache sehr gut zur Beschreibung parametrisierter Modelle eignet, denn relativ komplizierte Objekte lassen sich in wenigen Zeilen in verständlicher Weise definieren. Diese Arbeit hat also zwei wesentliche Ergebnisse. Zum einen fand eine Übertragung der Idee der Generativen Modellierung auf eine C++-Klassenbibliothek statt, die wesentliche Anforderungen objektorientierten Designs erfüllt, und zwar Erweiterbarkeit und modularen Aufbau. Zum anderen wurde eine Beschreibungsmöglichkeit für solche Generativen Modelle gefunden, die wesentliche Hinweise auf die Frage liefert, wie die Abbildungsbibliothek erweitert werden muß, um tatsächlich als Implementation einer Art Programmiersprache für Geometrisches Modellieren zu dienen. Der Brückenschlag zwischen beiden Teilen steht allerdings noch aus. Es ist nicht klar, inwieweit die implementierte C++-Bibliothek von Abbildungen, das Genmod-Paket, in Richtung einer funktionalen Programmiersprache ausgebaut werden muß, um in der Lage zu sein, in der gewünschten Weise zu modellieren. Einige Hinweise auf mögliche Antworten werden in der abschließenden Zusammenfassung gegeben. Im folgenden wird der Verlauf der Argumentation in den folgenden Kapiteln Arbeit grob umrissen, um einen Leitfaden zu liefern. Heutige Modelle sind unzureichend parametrisierbar Mit steigenden Anforderungen an virtuelle Welten und komplexe Modelle werden zwei Designziele bei der Entwicklung von Modellern und Modellbeschreibungssprachen immer wichtiger, und zwar die Änderbarkeit und die Wiederverwendbarkeit von gefundenen Lösungen für Modellierprobleme. Objektorientierte Modeller unterliegen zwei wesentlichen Einschränkungen Ein rein objektorientierter Modeller bietet die Möglichkeit, Objekte in einer Szene zu plazieren und die Größe und Form der Objekte solange zu modifizieren, bis eine Szene den Anforderungen genügt. Diese Arbeitsweise ist bei komplizierten Szenen mit vielen Objekten, die nach bestimmten Regeln variiert werden sollen, sehr mühsam und unnatürlich. – Verlust der Konstruktionsgeschichte Will man eine Reihe ähnlicher Objekte erzeugen, deren Konstruktionsprozeß sich nur jeweils an ganz bestimmten Stellen unterscheidet, so ist man gezwungen, ab der Stelle, wo man ändern möchte, jedesmal alle Konstruktionsschritte leicht abgeändert zu wiederholen. – Verlust funktionaler Abhängigkeiten Viele der Größen, die ein Modell definieren, hängen von anderen Größen ab. Ändert 5 sich ein Teil eines Modells, so hat das möglicherweise Auswirkungen auf andere Teile, und es muß bei Änderung viel nachgebessert werden, obwohl man vielleicht eine Regel angeben kann, wie Größen eines Modells voneinander abhängen oder zu ändern sind: Halbiere die Breite von jedem zweiten Fenster dieses Hochhauses. Die erste Einschränkung kann mit parametrischen Modellern behoben werden Parametrische Modeller ermöglichen es, die Konstruktion eines Objektes mitzuschneiden und nachträglich auf die angewandten Veränderungen Einfluß zu nehmen, Parameter zu variieren und diese Variationen sogar zu animieren. Das wird ermöglicht durch eine Erweiterung des objektorientierten Paradimas. Die Aufhebung der zweiten Einschränkung führt zum Generativen Modellieren Will man mit wenigen Parametern Einfluß auf eine Konstruktion nehmen, so muß spezifizierbar sein, in welcher Weise diese Parameter in die Konstruktion einfließen. Das bedeutet, man muß den Datenfluß beschreiben können, um zu explizieren, durch welche Berechnungen welche Größen des Modells aus den Eingabeparametern hervorgehen. Die Konstruktion des Modells kann dann für jeden Satz von spezifizierenden Eingaben neu durchgeführt werden. Generative Modelle lassen sich funktional beschreiben Das geeignete Konzept für die Realisierung der Generativen Modellierung ist die Abbildung. Das Modellieren mit Abbildungen erfordert es, höhere Funktionen zu verwenden sowie an jeder Stelle bezug nehmen zu können auf sämtliche bereits definierten Objekte. Beides ist bei einer funktionalen Sprache von vornherein gegeben. Es ist das Ziel dieser Arbeit, einen Weg zu weisen, wie Benutzer von kommerziellen Modelliersystemen in die Lage versetzt werden können, ihr spezifisches Wissen über ihr Arbeitsgebiet innerhalb der dreidimensionalen Modellierung in Problemlösungen umzusetzen, die auf hohem Niveau änderbar und wiederverwendbar sind. Eine solche Lösung brächte Modellierern aller Bereiche einen dramatischen Zuwachs an Arbeitsproduktivität. 6 KAPITEL 1. EINLEITUNG Kapitel 2 Evolution der Geometrischen Modellierung Die Computergrafik als Teilgebiet der Informatik beschäftigt sich mit der Synthese von Bildern und Bildsequenzen und den theoretischen Fragestellungen, die aus dem zunehmenden Einsatz bei wissenschafticher Visualisierung, in der Werbung und bei der Realisierung von Spezialeffekten in Spielfilmen erwachsen. Geometrische Modellierung ist der Zweig der Computergrafik, bei dem es um die Behandlung geometrischer Objekte in Datenverarbeitungsanlagen geht, um ihre Repräsentation mit Hilfe entsprechender Datenstrukturen und die Berechnung geometrischer Größen mit effizienten Algorithmen. Dieses Gebiet ist von zunehmender praktischer Relevanz, weil die Zahl der Anwendungen, die sich einer dreidimensionalen Darstellung bedienen, sprunghaft steigt, wobei einerseits die Größe der Modelle zunimmt als auch andererseits die Komplexität der abgebildeten virtuellen Räume. Damit gewinnt auch die Schnittstelle zwischen der Maschine und dem Anwender an Bedeutung und bestimmt zum Teil maßgeblich den Aufwand, den die Modellierung eines bestimmten Raumes oder Gegenstands bedeutet. Untersucht man die Evolution der Ansätze, die den dabei benutzten Modelliersystemen zugrundeliegen, und betrachtet die Arbeitsweise der Anwender beim Lösen von Modellieraufgaben, so ergibt sich dadurch die Möglichkeit, Entwicklungstrends abzulesen und besser zu verstehen, was das Wesen der dreidimensionalen Modellierung ausmacht. Um das Arbeitsgebiet einzugrenzen und die nötige Terminologie einzuführen, werden im folgenden einige der wesentlichen Konzepte vorgestellt, die im Rahmen der Computergrafik und der Geometrischen Modellierung entwickelt worden sind. 2.1 Die Produktion einer Animation Die Produktion eines computergenerierten Filmes läuft in einer Weise ab, die mit der Vorgehensweise bei einer konventionellen Filmproduktion vergleichbar ist. Die einzelnen Einstellungen werden von einem Operator, dem Modellierer, am Computer mit Hilfe eines Modelliersystems erstellt. In der Terminologie der Computergrafik nennt man ein solches System einen Modeller. Die Produktion von der künstlerischen Idee bis zum fertigen Film ist ein vielstufiger Prozeß. Im folgenden werden die wesentlichen Schritte einer solchen Produktion beschrieben, um die Probleme zu charakterisieren, die solche Anwendungen mit sich bringen, und um eine Einordung des Beitrages dieser Arbeit zu ermöglichen. Unabhängig von der Anwendung, seien es Spezialeffekte für einen Spielfilm, eine Visuali7 8 KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG sierung wissenschaftlicher Foschungsergebnisse, die Konstruktion von Bauteilen in der Industrie oder von Bauten in der Architektur mit abschließender dreidimensionaler Darstellung, oder ob es darum geht, eine virtuelle Welt auf einem verteilten System zu bauen, stets wird die gleiche Abfolge von Produktionsschritten durchlaufen, mit verschiedenen Gewichtungen je nach Gebiet. Der Modellierer hat zunächst die Aufgabe, einen virtuellen Raum zu erstellen. Diese Bezeichnung rührt daher, daß mit zunehmender Komplexität der Anwendungen nicht mehr nur einzelne Gegenstände1 , englisch Solids, modelliert werden wie im klassischen computerunterstützten Konstruieren, dem CAD (’computer aided design’), sondern extrem detailreiche Innenräume, Straßenzüge und ganze Landschaften, die nur im Rechner existieren. Um die Geometrie einer solchen Szene effizient zu erstellen, kann sich der Modellierer einer Bibliothek von vordefinierten geometrischen Grundkörpern und bereits modellierten Gegenständen bedienen, oder er kann selber neue Gegenstände konstruieren. Der Begriff Gegenstand bezeichnet dabei pauschal jede Form eines dreidimensionalen Objektes, seien es nun Möbel, technische Bauteile oder ganze Bauwerke, die man in einer Sequenz synthetischer Bilder, einer Animation, zeigen möchte. Bei der Szenenkonstruktion manipuliert der Modellierer die vom 3D-Modeller gezeigte dreidimensionale Ansicht der Gegenstände. Die Konstruktion vieler Modelle geht dabei von zweidimensionalen Schnitten oder Grundrissen aus, ein 3D-Modeller kann daher als eine Erweiterung eines 2D-Modellers angesehen werden. Im weiteren wird von Modellierung die Rede sein, wenn zwei- oder dreidimensionale geometrische Modellierung gemeint ist. Als Eingabegeräte für einen interaktiven Modeller dienen neben der Tastatur zur Eingabe von Punkten auf dem zweidimensionalen Bildschirm noch Geräte wie die Maus bzw. der Trackball, Digitizertabletts oder Lichtgriffel. Mit Stellrädern, englisch ’paddles’, können Werte auf einer eindimensionalen Skala manipuliert werden. Stellvertretend für diese Zeigegeräte, die in [Fel92] detailliert beschrieben werden, wird im folgenden von der Maus gesprochen. Ist die Form der Gegenstände, ihre Geometrie also, eingegeben, werden den Objekten Materialien zugewiesen, das heißt, eine Farbe, eine Musterung und das Reflexionsverhalten der Oberfläche der Gegenstände werden definiert. Ein Tisch erhält beispielsweise eine Holzmaserung, ein Becher soll aussehen, als wäre er aus glänzendem Metall, und ein Glas bekommt die Zusatzinformation, daß es getönt transparent ist. Diese Gegenstände werden dann in der Szene plaziert. Damit wird die sogenannte Szenenhierarchie festgelegt: Der Tisch wird neben ein Regal gestellt, das Glas oder der Becher auf den Tisch, und die einzelnen Böden des Regals werden mit Büchern gefüllt. Die Informationen im Szenengraphen, der der Szenenhierarchie zugrundeliegt, legen somit fest, welche Gegenstände in bezug auf andere Gegenstände plaziert werden. Dies hat den Vorteil, daß zum einen die Geometrie der Gegenstände getrennt ist von ihrer Verwendung in einer Szene, zum anderen erhält man so eine Gruppierung; man kann also etwa den Tisch mit den Gläsern an eine andere Stelle setzen, ohne daß jedes Glas einzeln bewegt werden müßte. Die Szenenhierarchie benutzt dabei nur Verweise auf Gegenstände, Referenzen, so kann man zum Beispiel mehrere Gläser auf den Tisch setzen, ohne daß man tatsächlich mehrere Gläser modellieren müßte. Schließlich wird die Definition einer Szene noch durch das Aufstellen einer Reihe von Lichtquellen vervollständigt, um die Gegenstände auch zu beleuchten. Die Position der Lichter ist ebenfalls in der Szenenhierarchie festgehalten, so erhält man etwa eine Stehlampe durch Gruppierung eines geometrischen Objektes, des Lampenfußes, mit einer Lichtquelle. Der Szenengraph enthält also eigentlich eine Reihe von Koordinatentransformationen, mit denen der Übergang von einem lokalen Koordinatensystem eines Gegenstands – oder einer Gruppe von Gegenständen – auf ein übergeordnetes Koordinatensystem beschrieben wird. Das Bezugssystem, in das die gesamte Szene eingebettet ist, heißt das Weltkoordinatensystem, kurz WKS, 1 In der Literatur ist zumeist von “Objekten” die Rede. In dieser Arbeit wird der Begriff “Gegenstand” verwandt, um Verwechslungen zu vermeiden, die durch den vielfach überladenen Begriff Objekt entstehen könnten. 2.1. DIE PRODUKTION EINER ANIMATION 9 im Gegensatz zum LKS der einzelnen Gegenstände. Läßt man diese Positionsinformationen im Szenengraphen zusätzlich von einer simulierten Zeit abhängen, so bewegen sich die Gegenstände, ein Glas auf dem Tisch fällt um oder eine Murmel kullert einen Regalboden entlang. Um schließlich die Abbildung einer Szene auf ein Bild oder eine Bildsequenz zu erhalten, wird abschließend eine virtuelle Kamera in der Szene plaziert. Mit Hilfe eines Renderingverfahrens wird aus der Szene und der mit der Kamera definierten Projektion schließlich ein Bild gewonnen. Es gibt dabei für das Rendering zwei grundsätzlich verschiedene Ansätze: Approximationsbasiertes Rendern. Die Oberfläche sämtlicher Gegenstände in einer Szene wird in ein Dreiecks- oder ein Polygonnetz umgewandelt. Moderne Grafikhardware ist in der Lage, große Mengen gefärbter 3D-Dreiecke schnell anzuzeigen, sodaß ein interaktives Arbeiten mit mehreren Frames pro Sekunde möglich ist. Die approximative Darstellung ist also Basis für die 3D-Sicht, die ein Modeller während einer Konstruktion präsentiert. Strahlschußbasiertes Rendern. Beim Raytracing-Verfahren wird der Weg von Lichtstrahlen durch die Szene simuliert. Dazu ist es nötig, den Schnitt eines Strahls mit einem Gegenstand schnell ausrechnen können, weil oft Millionen solcher Schnitte berechnet werden müssen, um ein Bild zu erzeugen. Dafür kann man auf diese Weise fotorealistische Ergebnisse erzielen, weil Phänomene wie Spiegelungen und Transparenz berücksichtigt werden. Mit Hilfe des Raytracing kann man physikalisch gesprochen den Lichttransport von spekulären zu spekulären und von spekulären zu diffusen Oberflächen modellieren. Neuere Entwicklungen des Rendering berücksichtigen aber auch andere Arten des Lichttransportes und sind motiviert von allgemeinen Theorien über die Ausbreitung elektromagnetischer Wellen. Die beiden Sichtweisen einer Oberfläche, einerseits als Polygonapproximation und andererseits als zweidimensionale Menge von Punkten im Raum, sind auch für viele andere Verfahren der Computergrafik grundlegend. In einem speziellen Teilgebiet, der ’computational geometry’, ist man an schnellen Algorithmen auf linearen Strukturen interessiert, man betrachtet etwa Mengen von Streckenzügen, Schnitte von Polygonen oder Polyedern oder die Bestimmung von konvexen Hüllen. Im ’computer aided geometric design’ (CAGD) dagegen beschäftigt man sich insbesondere mit Freiformflächen, wo man Konzepte wie die Geometrische Stetigkeit und Fragestellungen wie das n-Patch-Problem entwickelt. Diese Gebiete werden im folgenden Abschnitt 2.2 noch näher erläutert. Die Verwendung von Gegenständen stellt also gewisse Anforderungen an die rechnerinterne Darstellung einer Szene mit ihrer Geometrie und die dabei eingesetzten Datenstrukturen. Diese interne Darstellung der Geometrie ist das Modell des Gegenstands und kann ihrerseits aus einer komplexen Hierarchie von Datenstrukturen bestehen. In der Wahl eines bestimmten Modells kommt aber bereits zum Ausdruck, welcher Ansatz bei der Modellierung benutzt wird und welches Modellierparadigma einem Modeller zugrundeliegt. Der Vergleich solcher Ansätze ist Teil der vorliegenden Arbeit. Umwandlung in eine Approximation sowie der Strahlschnitt sind elementare Operationen, die jedes Modell unterstützen sollte. Abstrakter ausgedrückt sind die Grundoperationen eines Modells einerseits das Sampling einer zweidimensionalen Punktmenge im Raum und andererseits das Lösen von Nullstellenproblemen auf dieser Menge. 10 2.2 KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG Konzepte der Modellierung Die Computergrafik hat eine reiche Fülle von verschiedenen Ideen zur Darstellung von dreidimensionalen Objekten hervorgebracht. Diese Vielfältigkeit liegt in dem Bedürfnis begründet, die verschiedensten Arten von natürlichen Formen mitsamt ihres Verhaltens zu simulieren. Zunächst gibt es einfache geometrische Grundkörper wie Quader oder die platonischen Polyeder wie Tetraeder oder Ikosaeder. Andere Grundkörper haben gekrümmte Oberflächen, beispielsweise Kugeln, Kegel oder Zylinder. Eine Verallgemeinerung dieser gekrümmten Objekte führt zu Freiformflächen wie Bézier- oder Splineflächen. Basis für viele Freiformflächen sind Freiformkurven, Kurven im Raum, die dann zu Flächen kombiniert werden können, etwa zu interpolierenden Coons-Flächen. Kurven und Flächenelemente, Patches, stellen heute die Grundelemente der gängigsten Modeller dar. Es sind vielfältige Operationen zur Kombination dieser Elemente vorhanden. Verallgemeinert man den Zylinder, indem man statt eines Kreises beliebige geschlossene ebene Kurven als Querschnitt zuläßt, erhält man ein die Extrude-Operation. Verallgemeinert man weiter, indem man eine beliebige Querschnittsfläche eine beliebige Raumkurve entlangzieht, so stellt das überstrichene Volumen ein Sweep-Objekt dar. Aus zwei parametrischen Raumkurven c0 ; c1 erhält man eine Loft-Fläche, indem korrespondierende Parameterwerte mit einer Strecke verbunden werden, die resultierende Funktion p(u; v) = uc0 (v) + (1 , u)c1 (v) ist ein parametrisches Patch. Allgemein sind Freiformpatches in parametrischer Darstellung also Abbildungen p : (u; v) 7! (x; y; z) vom Zweidimensionalen ins Dreidimensionale. Schließt man einen Zylinder mit einer Kugel ab, deren Radius größer ist als der Zylinderradius, so erhält man an der Verbindung einen Knick. Blending beschäftigt sich mit der knickfreien Verbindung von Flächenstücken. Eine zweifach stetige Verbindung zwischen Patches erhält man beispielsweise, wenn man sich vorstellt, eine kleine Kugel würde einen solchen Knick entlangrollen. Die umgekehrte Operation, beispielsweise das Abrunden oder Abschrägen von Kanten eines Würfels, ist das Bevelling. Legt man um jeden Punkt einer Kurve eine Kreisscheibe von gegebenem Radius und betrachtet die Vereinigung dieser Mengen, so ist die Randkurve dieses Gebietes eine sogenannte Offsetkurve. Entsprechend kann man die Offsetfläche zu einem Patch oder einer Kurve betrachten, die Oberfläche des Volumens, das aus der Vereinigung von Kugeln um Punkte der Fläche oder der Kurve entsteht. Solche Konstruktionen benutzt man etwa, um die Bahn eines kugelförmigen Fräskopfes zu verfolgen und zu bestimmen, welches Volumen aus einem anderen Körper herausgeschnitten würde. Viele Gegenstände im täglichen Leben und vor allem in industriellen Anwendungen sind Kombinationen aus einfacheren Körpern. Die Technik der constructive solid geometry (CSG) beruht auf Boole’schen Operationen von Grundkörpern. So kann man einen CSG-Baum definieren, in dem die Knoten neue Objekte sind, die durch Schnitt, Vereinigung oder Differenz von anderen Körpern bestehen, um ein Bauteil aus verschiedenen Teilen zusammenzusetzen. Eine Bohrung simuliert man dann, indem man von einem Gegenstand verschiedene Zylinder abzieht, die in ihn hereinragen, oder eine Fräsung, indem man ein Fräsvolumen wie oben beschrieben von einem Gegenstand subtrahiert. Eine weitere Verallgemeinerung erhält man, wenn man verformte Gegenstände betrachtet. Mit Deformationen kann man Gegenstände verdrehen oder verbiegen, dies kann auch zeitabhängig geschehen oder bei ’space warps’ in Form einer räumlichen Deformationswelle. Während diese Konzepte zum gängigen Repertoire von modernen Modellern gehören, gibt es noch viele weitere Anwendungen, die in spezielleren Forschungsgebieten behandelt werden. Um beispielsweise Vielteilchenphänomene zu simulieren, sei es ein Feuerwerk, Regen oder Schnee, benutzt man Partikelsysteme. Partikel haben eine Position im Raum, gegebenenfalls eine Masse, 2.2. KONZEPTE DER MODELLIERUNG 11 eine Orientierung, eine Farbe und ein Alter. Sie werden von Kraftfeldern beeinflußt, dadurch kann man Wirbel oder Ströme simulieren, indem die entsprechenden Bewegungspfade, Trajektorien, über die Zeit berechnet werden. Interagierende Partikel kann man zudem dazu benutzen, um Massepunkte zu Netzen zu verbinden und so Textilien zu simulieren oder Kraftfelder auf Teile von Oberflächen wirken zu lassen. Physikalisch basiertes Modellieren (PBM) nutzt Techniken wie inverse Kinematik, um Gelenkmechanismen (IK-Chains) zu beschreiben. Bewegt man einen sogenannten Endeffektor wie das Ende eines Roboterarms und läßt es eine bestimmte Raumkurve beschreiben, so werden die nötigen Bewegungen der Gelenke berechnet und der Mechanismus nachgeführt. Mit einer Erweiterung dieser Technik lassen sich auch ganze Skelette bewegen, so kann man einen Zweibeiner einen Weg entlanggehen lassen, einfach indem man eine Fußspur vorgibt. Im CAGD wurden für Freiformkurven und -flächen viele verschiedene Darstellungen gefunden, die zumeist auf Polynomen oder stückweise definierten Polynomen beruhen, zwischen denen eine gewisse Stetigkeit verlangt wird. Neben Bézier- und B-Spline-Darstellungen kommen dort NURBS (Non-Uniform Rational B-Splines) oder andere allgemeiner parametrisierte Basisfunktionen zum Einsatz. Basisfunktionen sind wichtig, um den Verlauf von Funktionen mit Hilfe von Kontrollpunkten auf eine intuitive Weise festzulegen, wenn man die Basisfunktionen als Koeffizienten der Kontrollpunkte betrachtet. Kurven kann man mit dem Konzept der Tensorproduktdarstellung zu rechteckigen Flächen kombinieren. Daneben benutzt man dreieckige Flächenstücken und n-sided patches, mehrseitigen Füllflächen, um Patches möglichst flexibel zu größeren Verbänden zusammensetzen zu können. Daraus ergibt sich die Fragestellung, welche Übergangsbedingungen Patches an ihren Rändern zu erfüllen haben. Ein hilfreiches Konzept ist in diesem Zusammenhang die Geometrische Stetigkeit, eine Formstetigkeit höherer Ordnung, die von einer spezifischen Parametrisierung unabhängig ist. Neben der parametrischen Darstellung p : (u; v) 7! (x; y; z) von Freiform-Flächen kann man auch implizite Flächen betrachten. Diese erhält man als eine zweidimensionale Menge von Nullstellen einer Funktion f : R3 ! R, also als Isofläche der Form f(x; y; z) 2 R3 j f (x; y; z) = 0g. Bei der Metaball-Technik werden einzelne Raumpunkte mit einem Potential umgeben, die Summe der Potentiale ergibt dann eine Dichtefunktion. Man bestimmt den Schnitt eines Strahls p(t ) = p0 + tv mit der Isofläche zum Schwellwert d durch eine iterative Nullstellensuche für die reelle Funktion q(t ) = f ( p(t )) , d. Verschießt man Strahlen, die parallel zu den Koordinatenachsen verlaufen, und bestimmt sämtliche Schnittpunkte, so erhält man mit Hilfe des ’marching cube’Algorithmus eine Approximation der Isofläche. So erfüllen implizite Flächen beide elementaren Anforderungen an Modelle. Neben impliziten Flächen kann man auch implizite Kurven betrachten. Diese erhält man oft auf natürliche Weise, wenn man Schnittprobleme betrachtet. Sind s0 und s1 beispielsweise Flächen in expliziter, das heißt parametrischer Form, so ist die Schnittkurve beider Flächen als die Menge der Parameterwerte (u0; v0; u1; v1 ) gegeben, für die gilt: s0(u0; v0 ) = s1 (u1; v1). Ebenso läßt sich das Strahlschnittproblem für solche Flächen als die Menge der (u; v; t ) formulieren, für die gilt s(u; v) = p0 + tv. Dabei ist im übrigen bemerkenswert, daß das Strahlschnittproblem für implizite Flächen zu einem eindimensionalen und das für parametrische Flächen zu einem dreidimensionalen Nullstellenproblem führt, also im allgemeinen schwerer zu lösen ist. Ein grundsätzliches Problem ist nun der Wechsel von impliziter zu parametrischer Form. Dabei sucht man etwa die parametrische Darstellung einer Schnittkurve zweier Flächen. Das kann einmal die Raumkurve c sein, die Menge der Punkte des R3 , die beide Flächen gemeinsam enthalten, oder andererseits eine Kurve c0 im Parameterraum einer der Flächen, sodaß man die Raumkurve durch Komposition c = s0 c0 erhält. Die Auflösung einer impliziten Gleichung geschieht aber in der einen oder anderen Form immer durch ein Iterationsverfahren und ist entsprechend aufwendig. 12 KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG Es verbietet sich also, alle Punkte einer impliziten Kurve durch Iteration auszuwerten. Um dies zu umgehen, benutzt man Kurveninterpolation: Man berechnet nur einige Punkte des Schnittes und interpoliert zwischen diesen. Eine solche Näherungslösung gilt aber nur mit einer bestimmten Genauigkeit, und es gibt durchaus Anwendungen, wo die benötigte Genauigkeit nicht im Vorhinein bekannt ist. Fährt die Kamera im Laufe einer Animation immer näher an eine Schnittkurve heran, so kann die lokal benötigte Genauigkeit sprunghaft ansteigen. Wegen dieser Schwierigkeiten ist der Übergang zwischen impliziter und parametrischer Darstellungen noch ein aktives Forschungsgebiet, und die Lösung hängt zunächst noch von der spezifischen Problemstellung der avisierten Anwendung ab. – Eine mögliche Lösung dieses Problems mit Hilfe sogenannter Inklusionsfunktionen im Rahmen der Generativen Modellierung wird im Abschnitt 3.1 dargestellt. Während sich Effekte wie Rauch oder Nebel auch im approximativen Rendern verwenden lassen, interessiert man sich beim volume rendering für die transparente Darstellung räumlicher Dichtefunktionen. Beispielsweise gibt es medizinische Datensätze, die ein dichtes räumliches Gitter von Gewebedichtewerten enthalten, die durch Computertomographie bestimmt wurden. Stellt man sich das Gewebe transparent vor, wobei zunehmende Dichte zunehmende Lichtabsorption bedeutet, so kann man durch Integration der Lichtintensität entlang eines Strahles eine Darstellung mit starker räumlicher Wirkung erzeugen. Viele natürliche Strukturen besitzen die Eigenschaft der Selbstähnlichkeit. Fractal terrains sind computergenerierte Landschaften, die durch einen iterativen Prozeß erzeugt werden, der immer feinere Strukturen hervorbringt. L-Systeme sind Grammatiken, also Bildungsvorschriften für Worte einer abstrakten Sprache. Interpretiert man die Buchstaben dieser Worte geometrisch als Verzweigungen oder Äste, so kann man, wie Lindenmayer herausgefunden hat, die Morphologie vieler Pflanzen simulieren. Ein weiteres Gebiet, wo die Darstellung von Oberflächen und Dichtefunktionen eine große Rolle spielt, ist die ’scientific visualization’. Numeriker benutzen aufwendige Verfahren, um Strömungen von Flüssigkeiten oder Gasen zu berechnen, Minimalflächen zu bestimmen oder herauszufinden, wo technische Bauteile den größten Belastungen ausgesetzt sind. Man benutzt dabei zum Beispiel die Finite Elemente Methode, um die zugrundeliegenden Differentialgleichungen approximativ zu lösen. Das Ergebnis einer solchen Rechnung, einer Simulation oder einer Optimierung, zu visualisieren, kann schwierig sein, weil zumeist in hochdimensionalen Räumen gerechnet wird, sodaß man auf relevante Größen projizieren muß. Bereits die Darstellung einer komplexen Funktion f : C ! C stellt hohe Anforderungen an das Vorstellungsvermögen, wenn man etwa Realund Imaginärteil getrennt als Höhenfelder auf einem Ausschnitt der komplexen Ebene darstellt oder in Farbkanälen kodiert. Ist das Rechenergebnis ein räumliches Vektorfeld, beispielsweise die Fließgeschwindigkeit in einem simulierten Wirbel, so ist man gezwungen, Hilfskonzepte zur Darstellung zu benutzen, sei es eine Partikelanimation, sei es, daß man zeigt, wie sich ein Ausschnitt der Flüssigkeit in der Strömung deformiert, oder man benutzt volume rendering wie in manchen Wettersimulationen. In der scientific visualization bedient man sich also eigentlich der vorher vorgestellten Darstellungselemente, und das Finden einer geeigneten Darstellungsmethode ist selbst zu einem eigenen Forschungsgebiet geworden. Damit wird klar, in welchem Maße eine flexible Handhabung der konventionellen Darstellungselemente vonnöten ist, um Anwendern eine leistungsfähige Schnittstelle zur Darstellung ihrer räumlichen Modelle anzubieten. 2.3. PRÄZISIERUNG DES OBERFLÄCHENBEGRIFFS 2.3 13 Präzisierung des Oberflächenbegriffs Die genannten Konzepte der Modellierung waren in vielen Fällen die Antwort auf konkrete Probleme und sind ursprünglich anwendungsgetrieben entwickelt worden. Will man die Ausdruckskraft von Modellern mit ihren zugrundeliegenden Ansätzen klassifizieren, ist es aber nötig, von diesen Anwendungen ein wenig zu abstrahieren. Grundlegende Konzepte der Modellierung und Konstruktion sind zunächst einmal Kurven und Flächen. Wie bereits angesprochen, kann man diese in impliziter und in parametrischer Form darstellen. Wegen der Schwierigkeiten bei dem Umgang mit impliziten Darstellungen, wo die Auswertung auf Iterationsverfahren beruht, deren Implementierung aufwendig und die im Vergleich zur direkten parametrischen Auswertung wenig effizient sind, wurden parametrische Kurven und Flächen das bevorzugte Darstellungselement in der Modellierung. Modellierung mit Metaballs und Berechnung des Schnitts von Strahl und Fläche aber sind einerseits unverzichtbare implizite Konzepte, andererseits ist es wie erwähnt nicht zweckmäßig, alle Probleme, die sich als implizite Probleme stellen lassen, mit einer einzelnen allgemeinen Lösungsmethode anzugehen. Dies hängt auch mit der Dimensionalität der Lösungsmenge der Gleichungen zusammen, die ganz verschieden sein kann von der Dimension der Lösungsmenge als Teilmenge des R3 . Der erwähnte Schnitt zweier parametrischer Flächen hat eine vierdimensionale Lösungsmenge, während die Schnittmenge als Raumkurve eine eindimensionale Teilmenge des Dreidimensionalen ist. Weiter ist für die Standardkugel etwa der Wechsel zwischen den Darstellungen sehr einfach, somit kann man sich für das Sampling der expliziten und für den Strahlschnitt der impliziten Formulierung bedienen und kommt ohne jedes Iterieren aus. Sampling einer impliziten Isofläche per marching cube dagegen geht von einem ganz anderen algorithmischen Ansatz aus, es sind viele Nullstellenprobleme zu lösen, und Annäherung einer Flächenschnittkurve durch Kurveninterpolation ist ein weiteres Konzept, das in anderem Zusammenhang sinnvoll sein kann. Um dieses Problem auszuklammern, soll die Klasse der Objekte, auf denen die Modellierung im mathematischen Sinne als solche operiert, auf sehr allgemeine Weise definiert werden. Die einzige Forderung, die man in diesem Zusammenhang an die Oberfläche eines Gegenstands stellt, ist, daß sie in Teile zerlegt werden kann, und daß es eine parametrische Darstellung eines solchen Teils gibt, selbst wenn man sie im konkreten Fall nicht angeben oder effizient finden kann. Definition: Oberfläche eines Gegenstands als Mannigfaltigkeit Die Oberfläche eines Gegenstands ist eine kompakte orientierbare geschlossene zweidimensionale Fläche in R3 . Das bedeutet: Um jeden Punkt der Fläche läßt sich eine stetige bijektive Abbildung (Karte) von der Fläche nach R2 angeben Die Umkehrung einer Karte ist eine Parametrisierung der Fläche Im allgemeinen läßt sich eine solche Abbildung nicht auf die ganze Oberfläche fortsetzen Eine geschlossene Oberfläche hat keinen Rand, das heißt, um jeden Punkt der Fläche gibt es eine Karte mit offenem Definitionsbereich Die Fläche ist orientierbar, es gibt also ein Inneres und ein Äußeres der Fläche Die Oberfläche ist endlich und umschließt damit ein endliches Volumen KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG 14 Man fordert von der Oberfläche eines Gegenstandes also nicht, daß sie als Ganze parametrisierbar sein muß. Das hat zum einen den Grund, daß man auch an Gegenständen höheren Geschlechts interessiert ist, die beliebig viele Löcher enthalten. Der Torus ist ein Beispiel für einen Gegenstand des Geschlechts eins, während die Kugel Geschlecht null hat, und Gegenstände mit höherem Geschlecht als eins lassen sich nicht durch eine einzelne Parametrisierung erfassen. Zum anderen kann eine Oberfläche nach dieser Charakterisierung durchaus Stellen enthalten, wo sie nicht differenzierbar ist, wo man also keine eindeutige Tangentialebene angeben kann, wie an den Kanten oder Ecken eines Würfels. Doch beschränkt man diese Stellen in der Regel auf einzelne Punkte oder auf Kurven, also Knicke innerhalb der Oberfläche. Dann kann man die Oberfläche entsprechend in einen Verbund von Einzelflächen zerlegen, die jeweils in ihrem Innern differenzierbar sind. Daher ergänzt man die Definition einer Oberfläche auf folgende Weise. Definition: Oberfläche eines Gegenstands als Patchkomplex Die Oberfläche eines Gegenstands kann in einzelne Patches unterteilt werden. Ein parametrisches Patch ist eine kompakte orientierbare reguläre Fläche mit abgeschlossenem Rand. Das bedeutet, daß sich das Patch nicht selbst schneidet und daß es an allen Stellen eine eindeutige Tangentialebene gibt. Benachbarte Patches sind entlang ihres Randes verklebt. Das heißt, daß der Rand eines Patches gleichzeitig der Rand des Nachbarpatches ist, zwischen Patches gibt es also einen mindestens GC 0 -stetigen Übergang. Stellen, wo mehr als zwei Patches zusammentreffen, sind Knoten des Patchkomplexes. Randstücke zwischen zwei Knoten nennt man Kanten des Patchkomplexes. Statt von Knoten spricht man manchmal auch von Ecken. – Die Regularität einer Fläche ist ein differentialgeometrisches Konzept, das die “Glattheit” eines Flächenstückes sicherstellt. Sie wird zum Beispiel in [do 93] folgendermaßen definiert. Definition: Reguläre Fläche Eine Menge S R3 ist eine reguläre Fläche, wenn es zu jedem Punkt p 2 S eine Umgebung V R3 gibt und eine Abbildung f : R2 U ,! V \ S R3 , U offene Menge, für die gilt, daß f ein differenzierbarer Homöomorphismus ist, das heißt eine bijektive differenzierbare Abbildung mit stetiger Inverser, und daß es an jeder Stelle p 2 S eine eindeutige Tangentialebene gibt. Ein Patchkomplex besitzt damit eine topologische Struktur, die man isoliert von der geometrischen Gestalt betrachten kann. Dies ist die Struktur eines eingebetteten Graphen [Prö92], der vom gleichen Geschlecht ist wie die Oberfläche eines Gegenstands. In einem eingebetteten Graphen gibt es neben Kanten und Ecken auch noch Flächen, die von den Kanten begrenzt werden, in diesem Fall gerade die einzelnen Patches. Diese grundsätzliche Trennung zwischen geometrischer und topologischer Struktur wird in [BFH95] vorgestellt, eine detaillierte Ausarbeitung im Rahmen einer Diplomarbeit findet man in [Ben97]. 2.3. PRÄZISIERUNG DES OBERFLÄCHENBEGRIFFS Definition: 15 Topologische Struktur eines Patchkomplexes Ein eingebetteter Graph besteht aus Knoten, Kanten und Flächen. Jede Kante verbindet zwei Knoten und berandet genau zwei Flächen. Es kann jedem Flächenrand ein konsistenter Umlaufsinn (Orientierung) in der Weise zugeordnet werden, daß jede Kante in beiden Richtungen durchlaufen wird. Weiter ist die Umgebung jeder Ecke topologisch betrachtet planar, es gibt keine Stellen, wo der Körper auf einen Punkt eingeschnürt wird. Für jeden Knoten gilt also: Die mit ihm inzidierenden Kanten können in einer zyklischen Liste so angeordnet werden, daß für je zwei aufeinanderfolgende Kanten a, b gilt: (a; b) ist Teil eines Flächenrandes. Ein für die Computergrafik wichtiger Spezialfall einer Oberfläche ist dabei der geschlossene Polygonkomplex, das Polyeder. Dabei sind alle Patches planare Polygone, die nicht konvex sein müssen und Löcher haben dürfen, an die sich andere Teile des Gegenstands anschließen. Polyeder haben in der Geschichte der Geometrie eine große Bedeutung und sind im Rahmen der Informatik Gegenstand der ’computational geometry’. Ein Modell für diese Struktur ist der BRep. Definition: BRep Ein BRep (’boundary representation’) ist ein polyedrischer Gegenstand. Seine Seitenflächen sind zweidimensionale Polygone, abgeschlossene planare Teilmengen des R3 . Der Rand einer Seitenfläche ist eine geschlossene Streckenfolge. Das Polyeder ist durch die Angabe der Lage der Knoten und der Verbindungsinformationen von Knoten, Kanten und Flächen eindeutig definiert. Ein Spezialfall eines Polyeders ist der Dreieckskomplex, wo alle begrenzenden Polygone Dreiecke sind, wodurch die Bedingung der Planarität automatisch erfüllt ist. — BReps werden mit Hilfe von Euler-Operatoren konstruiert. Diese benutzt man, um Ecken durch Kantenzüge zu verbinden, die dann Flächen beranden, und es können Flächen verklebt werden, etwa um aus einem langgestreckten Quader einen Ring zu machen, indem man die Abschlußflächen identifiziert. Zu jeder Euleroperation gibt es ebenso eine inverse Euleroperation, sodaß man von einer Operationenalgebra sprechen kann [Män88]. — In vielen Fällen kann es zweckmäßig sein, nichtgeschlossene Körper zu modellieren, ein einzelnes Patch oder einen Verband mehrerer Patches, der nicht geschlossen ist, aber vielleicht als Teil einer Form an verschiedenen Stellen eines Gegenstands eingesetzt werden soll. Solche nichtgeschlossenen Oberflächen haben dann einen oder mehrere Ränder, die als Schnittstellen für die spätere Verklebung mit anderen Teiloberflächen dienen. Eine solche Teilform kann man aber ebenfalls als geschlossenen Patchkomplex modellieren, wenn man die Ränder mit Flächen schließt, die nur konzeptionell, das heißt topologisch, vorhanden sind, aber nicht angezeigt werden und keine Parametrisierung besitzen. Solchen Flächen fehlt also vorerst die Geometrie, die durch ein späteres Verkleben nachgereicht wird. Definition: Hollow-Fläche Eine Hollow-Fläche ist ein Patch eines Patchkomplexes, das topologisch vorhanden ist, aber nicht parametrisiert werden kann. Hollows werden von allen geometrischen Algorithmen ignoriert und nicht gerendert. Der Rand eines Hollows ist eine geschlossene Raumkurve, die einzelnen Randstücke werden durch die Ränder der umgebenden Patches definiert. Zwei Patchkomplexe können miteinander verklebt werden, indem die Ränder zweier gegeneinander orientierter Hollows miteinander identifiziert werden. Dabei werden die Hollows beider Oberflächen im topologischen Sinne mit Hilfe von Euler-Operationen entfernt und ein zusammenhängender Graph erzeugt. KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG 16 So kann ein einzelnes Patch, ein einzelnes Dreieck, bereits als geschlossener Körper angesehen werden, indem man auf seiner Rückseite ein Hollow als weiteres Patch annimmt. Nach obiger Definition ist es ausgeschlossen, daß ein Hollow zu weiteren Hollows benachbart ist, denn der Rand eines Hollows muß tatsächlich geometrisch vorhanden sein. Somit dienen Hollows gerade als Anschlußstellen für das Zusammensetzen von Gegenständen. Bei Patchkomplexen mit Hollow-Flächen kann man natürlich nicht mehr von einem umschlossenen Volumen sprechen. Modellierung ist die Modellierung einzelner Patches und das Verkleben zu einem geschlossenen Verbund, der die Oberfläche eines Gegenstands definiert. Diese Sichtweise der Modellierung ist motiviert vom mathematischen Begriff der Mannigfaltigkeit. Aus dieser Perspektive ist die planare Approximation nur ein spezieller Verband von Freiformflächen, dies gestattet eine vereinheitlichte Betrachtungsweise, wo nicht mehr unterschieden wird zwischen geometrischen Grundkörpern, die als Ganzes gegeben sind, zwischen verschiedenen Repräsentationen von Freiformflächen und zwischen großen Dreiecksverbänden, weil die gleiche topologische Struktur zugrundeliegt. Um diesen Ansatz in einem Modeller zu realisieren, muß daher eine einheitliche Datenstruktur für die verschiedenen Typen von Oberflächen und zusammengesetzten Oberflächen implementiert werden, was sehr aufwendig sein kann. Zudem können Effizienzaspekte eine Rolle spielen, denn wenn in einer Anwendung nur eine bestimmte Flächenklasse benötigt wird, kann es sinnvoll sein, Overheads zu vermeiden, die durch die Spezialisierung einer sehr allgemein angelegten Datenstruktur erzeugt werden. Andererseits können Speziallösungen sehr aufwendig zu erweitern sein, wenn sich die Notwendigkeit einmal ergibt. Es gibt also keine Patentlösung für das zu verwendende Modell, doch der im nächsten Abschnitt erläuterten historischen Entwicklung von Modellierparadigmen liegt eine gewisser Trend hin zu größerer Allgemeinheit zugrunde. Identifiziert man Gegenstände mit geschlossenen Patchkomplexen, so erhält man die erwähnte hierarchische Gliederung von Punkten, Kurven und Flächen. Der Produktionsprozeß geht dagegen zumeist in der umgekehrten Richtung vor sich, indem zunächst Kurven gezeichnet werden, diese Kurven dienen zur Definition von Teilflächen mit den entsprechenden Randkurven, die verschiedene Flächenelemente werden entlang ihrer Randkurven verklebt oder verblendet. Patchkomplexe sind in sofern das allgemeinste Konzept, weil bereits ein einzelner Kurvenzug topologisch gesehen einen Patchkomplex darstellen kann, als zwei Knoten, eine Kante und eine Hollow-Fläche, die den Kurvenzug als geschlossene Randkurve besitzt. Eine einzelne Fläche ist ebenfalls, wie erwähnt, ein Patchkomplex, wenn man den Rand als Rand eines Hollows betrachtet. Die Kombination und Erweiterung zu der geschlossenen Oberfläche eines Gegenstands, sei es eine planare Approximation oder ein Komplex von Freiformflächen, stellt sich somit in einem einheitlichen Zusammenhang dar. 2.4 Konventionelle Modellieransätze Ausgangspunkt der Untersuchung war die Frage, wie Modellieransätze zu klassifizieren sind. In dreißig Jahren Entwicklung der computerunterstützten Modellierung sind viele Modellieransätze und -verfahren entstanden, die zunächst nur ad hoc zur Lösung bestimmter Anwendungsprobleme entworfen worden sind. Dabei war oft klar, wie die Lösung für bestimmte Probleme auszusehen hat. – Heute hat die Modellierung aber eine Diversifizierung erfahren, die von rein synthetischen kurzen Werbefilmen über aufwendig in Filmaufnahmen integrierte Animationssequenzen bis zur Konstruktion kompletter Produkte in der computerunterstützten Fertigung reicht. Im CAD müssen 2.4. KONVENTIONELLE MODELLIERANSÄTZE 17 in technischen Bereichen Modelle hoher Genauigkeit erzeugt werden, wobei besondere Ansprüche an die Beherrschbarkeit der Modelle und die Kontrollierbarkeit der Form im Detail gestellt werden, während bei synthetischen Filmen ästhetische Gesichtspunkte im Vordergrund stehen und eine Anpassung der Bedienung von Modellern an die künstlerische Denkweise wichtig ist. Es sind dabei zwei Probleme stark in den Vordergrund getreten, die unabhängig vom Anwendungsgebiet eine Rolle spielen und die die Entwicklung neuer Modeller vorangetrieben haben. Änderbarkeit Ein Modellierkonzept muß die Möglichkeit offenhalten, bei Änderung von Designvorgaben ein Modell in jeder notwendigen Weise nachträglich zu verändern. Wiederverwendbarkeit Einmal entwickelte Lösungen für Modellierprobleme müssen auch an anderen Stellen wieder verwendet werden können, damit eine nur leicht variierte Aufgabe nicht ganz von neuem gelöst werden muß. Zum einen stellt sich nun die prinzipielle Frage, ob es überhaupt einen Modellieransatz geben kann, der die ganze Bandbreite der Anwendungen mit ihren spezifischen Erfordernissen abdecken kann und gleichzeitig diese Bedingungen erfüllt, andererseits ist man im akademischen Bereich natürlich an der Frage interessiert, ob es eine Möglichkeit gibt, all diese Ansätze zu formalisieren und quantitativ oder qualitativ zu vergleichen. Dies ist auf der Basis von Beschreibungssprachen für Modelle eher möglich, als wenn man versucht, die Leistungsfähigkeit interaktiver Programme zu messen. Andererseits bedeutet Modellierung nicht nur, eine kompakte, verständliche textuelle Beschreibung von Modellen zu erreichen, vielmehr ist Interaktivität ein integraler Bestandteil der Arbeit, virtuelle Räume zu entwerfen. Ein besonderes Problem, das die praktische Relevanz eines Modellieransatzes entscheidend bestimmt, ist die Frage, wie effizient sich modellieren läßt. Die Modelliereffizienz eines Modellers läßt sich einerseits ganz klar über den zeitlichen und finanziellen Aufwand bestimmen, der nötig ist, um eine bestimmte Modellieraufgabe zu erfüllen, andererseits kommen dabei aber auch schwer meßbare Größen wie Einarbeitungszeit und Beherrschbarkeit eines Modelliersystems hinzu. Zudem ist wegen der besprochenen Komplexität der Systeme und der Aufgaben zu beobachten, daß sich Anwender ein Repertoire von Vorgehensweisen zulegen, die sie einsetzen, um die konzeptionellen Schwächen eines Modelliersystems zu umgehen. So werden die im folgenden besprochenen Hilfskonzepte durchaus als Stärken denn als konzeptionelle Schwächen gesehen, es stellt sich natürlich die Frage, ob Alternativen zum ’state of the art’ der Modellierung bestehen, die effizienteres Arbeiten möglich machen. Konventionelle Modellieransätze können grob in die Kategorien approximationsbasiert, objektorientiert und parametrisch eingeordnet werden. Die historische Entwicklung von Modellern kann insgesamt als eine Entwicklung zu Ansätzen gesehen werden, die der Aufgabenstellung immer besser angpaßt sind. Das bedeutet, man versucht, eine möglichst natürliche Sichtweise der Modellierung zu entwickeln. 2.4.1 Approximationsbasiertes Modellieren Approximationsbasierte Modeller zeichnen sich dadurch aus, daß sämtliche beschriebenen Konzepte, die beim Modellieren benutzt werden, intern auf Polygonkomplexen basieren. Körper werden nicht in ihrer abstrakten Form als Kugeln oder Zylinder verarbeitet, sondern eine Szene enthält ausschließlich BReps und Streckenzüge, die die Form von Flächen und Kurven bis zu einer vorgegebenen Genauigkeit annähern. Dies hat den Vorteil, daß es eine einheitliche Datenstruktur gibt, KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG 18 auf der alle Algorithmen intern operieren. Der Modeller bietet Operationen allein für die Manipulation von BReps an, und all diese Operationen produzieren wieder BReps. Die computational geometry bietet einen reiche Fundus von solchen Algorithmen, und für viele Hilfsoperationen auf Polygonen und Polyedern gibt es wohlfundierte Lösungen citeMehlhorn84, [SDK96]. Punktlokalisation, ’point in polygon’- und ’point in polyeder’-Tests Schnitte von Strecken mit Polygonen, von Polygonen und von Polyedern Algorithmen auf konvexen Polygonen, Bildung konvexer Hüllen Schnitte von Halbräumen, lineare Programmierung Polygonales Clipping, Sichtbarkeitsbestimmung Datenstrukturen für Raumunterteilungen Distanzbestimmung, Kollisionsdetektion Triangulierung, Tetraedierung, Sweep-Paradigma Aus diesen Routinen kann man effiziente Algorithmen für höhere Operationen zusammensetzen, man kann Deformationen, Boole’sche Operationen zwischen Primitivobjekten und lokale Manipulationen allein auf BRep-Basis beschreiben. Für bestimmte Problemklassen bietet die Modellierung auf BRep-Basis also sehr effiziente Lösungsmöglichkeiten an. Ein Beispiel ist das CSG, das im allgemeinen für Freiformflächen als implizites Problem wie erläutert sehr schwer zu lösen ist. CSG auf BReps dagegen ist ein diskretes Problem, und die Klasse der polyedrischen Körper ist unter Schnittbildung abgeschlossen, im Gegensatz etwa zu der Klasse der von bikubischen Bézier-Patches begrenzten Gegenstände. CSG ist damit ein Beispiel für eine Problemklasse, die einzig auf Basis von Approximationen zufriedenstellende, wenn auch schwer zu implementierende Lösungen besitzt. Der approximationsbasierte Ansatz besitzt jedoch auch gravierende Nachteile. Genauigkeitsprobleme Da die ursprüngliche Form einer Kugel, eines Kegels, einer Freiformfläche nicht mehr verfügbar ist, kann an einzelnen Stellen der Approximation nicht mehr nachträglich die Genauigkeit erhöht werden, wenn sich herausstellen sollte, daß dies nötig wird. Performanceprobleme In vielen Anwendungen werden Toleranzgrenzen vorgegeben, die ein Objekt einhalten muß. Um einen Kegel, eine Kugel etc. sehr genau anzunähern, benötigt man unvernünftig große Netze, die Probleme bei Rechenzeit und Speicherplatz verursachen. Niedriger Abstraktionsgrad Das Hauptproblem liegt damit also schon im Ansatz begründet. Es ist nicht vernünftig, sich auf die Darstellung von Polyedern zu beschränken, wenn man eigentlich gar keine Polyeder meint, sondern deformierte Quader und Kugeln, die miteinander verschnitten werden sollen und nachher wieder deformiert werden. - Das bedeutet, das Problem besteht im niedrigen Abstraktionsgrad des Modellierparadigmas. Das Problem, die richtige Sichtweise für ein Problem zu finden, läßt sich auch auf folgende Weise formulieren: Es gilt, die richtigen Freiheitsgrade zu isolieren. Für approximationsbasierte Modeller bedeutet dies, daß es keinen Sinn macht, eine Kugel, die durch Mittelpunkt und Radius eindeutig definiert ist, durch ein Dreiecksnetz von Tausenden von Punkten und Abertausenden von Freiheitsgraden zu modellieren. - Die Entwicklungsgeschichte der Modellierung läßt sich also nachträglich als Suche nach der richtigen Sichtweise interpretieren, in der immer komplexere Modelle neue Anforderung an die Handhabbarkeit, mithin die Isolierbarkeit der richtigen Freiheitsgrade gestellt haben. 2.4. KONVENTIONELLE MODELLIERANSÄTZE 2.4.2 19 Objektorientiertes Modellieren Konventionelle interaktive Modellierung der heute kommerziell dominierenden Systeme geht von einem Objektansatz aus. Die Gegenstände sind dabei durch eine Reihe von Eigenschaftsfeldern definiert, deren Inhalt interaktiv bestimmt wird. Ein gegebenes Objekt wird durch Parameteränderung solange modifiziert, bis es den Vorgaben des Modellierers entspricht. Die dabei modellierten Gegenstände sind Objekte im Sinne der objektorientierten Programmierung (OOP). Das bedeutet, das Grundobjekt Quader enthält beispielsweise als Informationen Länge, Breite und Höhe. Der Objektansatz kann auch dahingehend erweitert werden, daß nicht nur Gegenstände durch Objekte repräsentiert werden, sondern auch Operationen wie Transformationen oder andere Modifikationen an Gegenständen. Position und Orientierung im Raum werden über Transformationsobjekte bestimmt, Deformationen wie Verdrehungen etwa besitzen dann als Objektinformationen zum Beispiel Drehachse und Drehwinkel pro Achsenabschnitt. So können alle vorgenannten Konzepte, die in einem Modeller benutzt werden, als Instanzen von Objektklassen formuliert werden, die in einer Klassenhierarchie angeordnet sind. Ein ausgewähltes Objekt stellt sich nach außen als ein Satz von Parametern dar, die interaktiv bestimmt werden können. Einerseits gibt es dazu die Technik der Dialogboxen, in die per Tastatur Werte eingetragen werden können, andererseits benutzt man Gizmos oder Handles, das sind Hilfsobjekte, die im Dreidimensionalen dargestellt werden und den Einfluß von Parameteränderungen unmittelbar zeigen sollen. Der Aufruf von Grundobjekten und Operationen geschieht dabei entweder durch Auswahlmenüs oder durch Icons, die auf einer Werkzeugleiste angeordnet sind. Man spricht bei Operationen, die auf Kurven, Flächen oder Gegenständen operieren, daher auch von Werkzeugen. Objektorientierte Modeller besitzen damit ein Modell, das es gestattet, die abstrakte Darstellung eines Gegenstands beizubehalten. Intern kann ein solcher Modeller jedoch ebenfalls approximationsbasiert arbeiten, entscheidend ist aber, daß die BReps in der Szene eine Verbindung zum Objekt behalten, sodaß etwa die Genauigkeit einer Approximation nachträglich noch verändert werden kann. Ein solches System kann man daher als rein objektorientierten Modeller beschreiben, wo es mit Ausnahme eines Szenengraphen keine “höheren” oder Metaobjekte gibt, die benutzt werden können, um Verbindungen zwischen Objekten zu modellieren oder Mengen von Objekten zu bilden. In einem rein objektorientierten Modeller hat man daher nur Zugriff auf die gegenwärtig definierten Objekte. Daher unterliegt man bei diesem Ansatz hauptsächlich den Beschränkungen, daß die Konstruktionsgeschichte eines fertig modellierten Gegenstands nicht mehr verfügbar ist, und daß funktionale Abhängigkeiten zwischen den Parametern verschiedener Objekte nicht expliziert werden können. 2.4.2.1 Verlust der Konstruktionsgeschichte Die erste Einschränkung bedeutet dabei, daß das Ziel der objektorientierte Modellierung allein das modellierte Objekt ist. Es wird eine Szene definiert, indem Objekte definiert werden. Primitive wie Kreise, Linien, Polygonzüge oder Kurven im Zweidimensionalen, bzw. Kugeln, Quader und Freiformflächen im Dreidimensionalen werden als Objekte verstanden. Diese Objekte haben als Instanzen von Objektklassen bestimmte Eigenschaftsfelder, Slots, die gefüllt werden müssen, um ein Objekt zu definieren. Diese Felder enthalten dabei Daten wie 3D-Vektoren für Raumpositionen oder Skalare, um Winkel oder Längen zu spezifizieren. Sind alle Eigenschaften aller Objekte festgelegt, ist damit die Szene definiert und kann gerendert werden. Der Konstruktionsprozeß ist bei zunehmender Komplexität der Objekte aber mehrstufig. Das bedeutet, daß Objekte kombiniert werden, um aus ihnen neue Objekte zu erzeugen. Beispiele sind das erwähnte CSG, oder die Konstruktion einer Blending-Fläche aus zwei Raumkurven, zwischen KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG 20 denen linear interpoliert wird. Ist die Konstruktion abgeschlossen, so verfügt der Modellierer zwar über den Gegenstand, den er für seine Zwecke braucht, aber der Konstruktionsprozeß ist abgeschlossen und kann nicht nachträglich mit variierten Parametern für die bei der Konstruktion benutzten Objekte wiederholt werden. Somit ist jedes modellierte Objekt ein Unikat in dem Sinne, daß es zwar oft beliebig kopiert oder weiter verändert werden kann, der Modellierprozeß selbst aber ist beendet. Dies gilt insbesondere für interaktive Eingaben durch Mausbewegungen, mit denen Objekte auf zunächst bequeme Weise durch die erwähnten Handles manipuliert werden können. Diese Bewegungen sind aber nicht auf identische Weise zu wiederholen oder gar nachträglich abzuändern, es kann nur iterativ immer weiter verändert werden. 2.4.2.2 Verlust funktionaler Abhängigkeiten Die zweite Einschränkung, der Verlust funktionaler Abhängigkeiten, bezieht sich auf einen tiefliegenderen Mangel objektbasierter Modeller. Denn auch interaktive Eingaben, die im Verlauf des Modellierprozesses getätigt werden, sind nicht allein von gestalterischen Gesichtspunkten bestimmt. Insbesondere bei der computergestützten Konstruktion müssen geometrische Konstruktionen vielen Bedingungen und Vorgaben genügen. Ein einmal erstelltes Modell entspricht diesen dann zwar, aber bereits kleine Änderungen der Vorgaben haben unter Umständen ein Wiederholen vieler Arbeitsgänge zur Folge. Der Grund für diese Schwäche ist, daß im objektorientierten Paradigma die Slots der Objekte in der Szene zwar Werte enthalten, aber in der Regel keinen Bezug zu den Berechnungen, die diese Werte liefern. Es gibt viele Fälle, wo dies eine Rolle spielt, insbesondere in CAD-Programmen und bei der 2D-gestützten Konstruktion. Bestimmte Maße beziehen sich aufeinander, wie etwa bei Bohrungen oder bei Verbindungen von Teilen Andere Werte sind aus Berechnungen hervorgegangen, wie etwa ein Polygon, das in einen Kreis eingepaßt wird, Gegenstände, die einander berühren, oder bei der Ausrichtung von Gegenständen in bestimmter Weise Bei Animationen ist die mangelnde Dynamik der Abhängigkeitsbeziehungen besonders augenfällig, wenn Gegenstände, deren Bewegung einen Bezug aufeinander hat, mit getrennten Bewegungspfaden modelliert werden müssen Um diesen Schwächen zu begegnen, verfügen CAD-Programme und Modeller über diverse Hilfmechanismen. Insbesondere das Modelliersystem AutoCAD der Firma Autodesk [Aut96c] ist ein quasi-Standard zum 2D-gestützten Konstruieren und bietet eine Palette von Werkzeugen an, die den Zeichengewohnheiten von Ingenieuren und Architekten entgegenkommen. Four Views-Technik Von Modellern werden meist vier Sichten angeboten, eine perspektivische, sowie Draufsicht, Sicht von links und von vorn. Konstruiert man in diesen Sichten, ist bereits die Orthogonalität vieler Konstruktionselemente gesichert. Es ist sehr einfach, mit dieser Technik rechteckige Räume zu konstruieren und diese über-, neben- oder hintereinander zu plazieren. Aufwendige Freiformkonstruktionen wie Teile von Automobilen oder Flugzeugen dagegen profitieren deutlich weniger von dieser Konstruktionsweise. Es bleibt zu spekulieren, ob diese rechteckige Sicht der Dinge eine natürliche ist und den Zwecken der Konstruktion angepaßt wurde, oder ob es dadurch einfach vorwiegend zu rechteckig konstruierten Objekten kommt. 2.4. KONVENTIONELLE MODELLIERANSÄTZE 21 Fang Ist etwa in AutoCAD ein Punkt einzugeben, kann der Benutzer über sogenannte Fangmodi spezifizieren, ob dieser zum Beispiel durch Schnitt zweier Linien, durch Fällen eines Lotes oder als Berührungspunkt der Tangente an einen Kreis gegeben sein soll. AutoCAD bietet dem Modellierer aber überhaupt keinen Rückgriff auf die Konstruktionsgeschichte. Veränderungen können daher extrem aufwendig werden, weil die gefangenen Punkte bei veränderten Modellparametern jedesmal neu gefangen werden müssen. Gitter Eine andere Hilfe ist das Gitter, wo nicht der per Maus eingegebene Punkt direkt verwendet wird, sondern der auf einem Hilfsgitter am nächsten liegende. Dies bedeutet, man schränkt die zur Auswahl stehenden Punkte ein, um so Bezug auf bestimmte, in einer bestehenden Konstruktion möglicherweise bereits verwendete Punkte nehmen zu können. Dies funktioniert allerdings nur eingeschränkt, Kreise etwa haben in der Regel bloß vier Punkte, die genau auf dem Gitter liegen. Lokale Hilfskoordinatensysteme Um die Beschränkung auf xy-, yz und zx-Ebene als Konstruktionsflächen aufzuheben, können Konstruktionen in bezug auf ein im Raum liegendes lokales Koordinatensystem vorgenommen werden. Diese Technik ist ein Vorläufer der Szenenhierarchie, denn lokal konstruierte Objekte werden intern ins Weltkoordinatensystem eingebettet, sodaß nachträgliche Änderungen des lokalen Koordinatensystems keinen Einfluß mehr auf die Konstruktion haben. Modellieren mit constraints Um Abhängigkeiten zwischen Konstruktionselementen zu spezifizieren, gibt es die Technik des constraint-basierten Modellierens, wo explizite Bedingungen formuliert werden, die ein Modell zu erfüllen hat. So kann man fordern, daß Elemente einander berühren sollen, daß bestimmte Winkel einzuhalten sind, oder wo sich Geraden schneiden sollen. Dies stellt den Benutzer vor die schwierige Aufgabe, nicht zuwenige und nicht zuviele Bedingungen vorzugeben, anderenfalls ist das zu lösende Gleichungssystem unter- oder überbestimmt. AutoCAD etwa besitzt ein entsprechendes Erweiterungsmodul, die constraint-Technik ist somit durchaus von praktischer Relevanz. Problematisch dabei ist die große Anzahl denkbarer Nebenbedingungen, die nicht alle vom System angeboten werden können, und die Erweiterung dieser Idee auf dreidimensionale Modelle, wo die möglichen constraints entsprechend komplexer werden. Die revolutionäre Wirkung der Informationsverarbeitung in der Rationalisierung schematischer Arbeitsabläufe liegt darin begründet, daß Computerprogramme Anwendern die Möglichkeit bieten, Informationen regelgesteuert bearbeiten zu lassen. In der dreidimensionalen Modellierung ist ein Anwender aber noch immer gezwungen, selber alle Arbeitsschritte zur Definition eines virtuellen Raumes durchzuführen, und nicht die Konstruktion, sondern das Modell selber zu beschreiben, zu erarbeiten. Daher ist es wichtig, eine Methode zur Beschreibung von Konstruktionen zu finden, nicht nur eine verständliche geometrische Programmiersprache, sondern auch eine Möglichkeit zur Visualisierung dieser Beschreibung. 2.4.3 Parametrisches Modellieren Um die erste Beschränkung des rein objektorientierten Ansatzes, den Verlust der Konstruktionsgeschichte eines Objektes, aufzuheben, gibt es eine konsequente Weiterentwicklung des rein objek- 22 KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG torientierten Ansatzes, den parametrischen Modeller. Dieser behält mit Hilfe von Metaobjekten die abstrakte Objektinformation nicht nur für Gegenstände bei, sondern auch für die Operationen, die auf die Gegenstände angewandt worden sind. Das bedeutet, daß in einem viel weiteren Rahmen auf die Parameter Einfluß genommen werden kann, die während des Konstruktionsprozesses eingegeben worden sind. Ein Beispiel für die Realisierung dieser Idee ist der Modeller 3D-Studio Max der Firma Kinetix [Aut96b], [Aut96a]. Dort gibt es das Konzept des Modifier Stack, ein Metaobjekt, das zunächst nur einen Gegenstand enthält und dann sukzessive um die angewandten Deformationen wie Verjüngen und Biegen mit den jeweils eingegebenen Parametern ergänzt wird. Damit ist die Konstruktionsgeschichte eines Objektes also zugänglich, und die interaktiv eingegebenen Parameter von Deformationen oder Transformationen können nachträglich nach Bedarf verändert werden, während die Reihenfolge der Anwendung erhalten bleibt und die Auswirkung von Parameteränderungen auf das fertige Objekt direkt im Dreidimensionalen demonstriert werden kann. Ein zusätzlicher Vorteil dieser Idee ist, daß sämtliche Parameter der Objekte, wie zum Beispiel die Stärke eines räumlichen Turbulenzfeldes, mit Hilfe der Keyframe-Technik animiert werden können. Dabei werden auf einer Zeitleiste verschiedene Zeitpunkte eines Intervalls angewählt. Der Modellierer trägt die gewünschten Parameter in die zu animierenden Objekte ein, durch diese Werte wird dann eine kontinuierliche Interpolationskurve gelegt, die zu den den vorgegebenen Zeitpunkten die vorgegebenen Werte annimmt. Somit kann für jeden Zeitpunkt einer Animation durch Interpolation eine Szene erzeugt werden, auf diese Weise kann man zeitliche Verläufe in einem weiten Rahmen modellieren. Grenzen gibt es für diesen Mechanismus nur dort, wo sich eine Transformation auf bestimmte Parameter eines Objektes bezieht, weil diese Parameter dann nicht mehr nachträglich verändert werden können. Beispielsweise gibt es in 3D-Studio Max Werkzeuge zur Manipulation einzelner Punkte des BReps eines Gegenstands. Danach aber kann die Approximationsgüte natürlich nicht mehr verändert werden, weil die manipulierten Punkte dann nicht mehr existieren würden. - Zum anderen wird der Mechanismus des Modifier-Stack in 3D-Studio Max nicht durchgängig angewandt, es gibt eine ganze Reihe von Änderungen, die man an Objekten, an Teilobjekten oder der Szene bzw. an einer Subszene durchführen kann, die nicht in irgendeiner Form in Metaobjekten gespeichert werden. Dies stellt aber natürlich weniger eine prinzipielle Schwäche des Ansatzes dar als vielmehr eine der konkreten Realisierung. Modifier können sich dabei auch auf Referenzen eines Gegenstands beziehen. Das bedeutet, daß man zunächst die Grundform eines Gegenstands modellieren kann, um dann verschiedene Instanzen dieses Gegenstands jeweils unterschiedlich weiterzubearbeiten. Das Konzept eines Modifier-Stacks funktioniert bei 3D-Studio Max vor allem deswegen so gut, weil dort im wesentlichen mit globalen Operationen gearbeitet wird, beispielsweise Verzerrungen oder Transformationen, die sich jeweils auf ein gesamtes Objekt beziehen. Ein anderer Modeller, Rhino der Firma McNeel Associates, ist zunächst nur rein objektorientiert, basiert aber anders als 3D-Studio Max intern nicht auf der approximativen Darstellung der Gegenstände als BReps, sondern benutzt als Basis-Datenstruktur NURBS, verwendet also einen Freiform-Ansatz. Rhino verfolgt dabei eine klare Trennung von Kurven, Flächen und Gegenständen und bietet mannigfache Werkzeuge an, um Kurven zu Flächen zu verblenden, um aus Flächen Kurven zu extrahieren, um aus Flächen und Kurven Gegenstände zu machen, und um aus Gegenständen durch Schnitte Kurven oder Flächen zu gewinnen. Somit gibt es eine große Anzahl von Werkzeugen, die auf mehr als einem Objekt als Eingabe operieren. Für einen solchen Modeller macht ein ModifierStack aber wenig Sinn, in diesem Fall müßte man sich einer Baumstruktur von Operationen bedienen. Man kann jedoch die Ausdrucksfähigkeit eines parametrischen Modellers mit Modifier-Stack 2.5. GENERATIVES MODELLIEREN 23 oder -Baum noch weiter erhöhen, wenn man zuläßt, daß ein Teil eines Stacks oder ein Teilbaum als eigenständiges Objekt behandelt wird. Besonders bei werkzeugreichen Freiform-Modellern wie Rhino können Konstruktionen über sehr viele Zwischenschritte erfolgen. Dabei werden unter Umständen viele Objekte auf sehr ähnliche Weise erzeugt. Es wäre daher hilfreich, wenn man eine Reihe von Konstruktionsschritten als neues eigenständiges Werkzeug behandeln könnte und die Teile der Konstruktionen, die jeweils anders sind, als Parameter dieser Werkzeuge zu sehen wären. Auf diese Weise könnte sich ein Modellierer eine Werkzeugbibliothek anlegen, die auf seine spezifischen Aufgaben zugeschnitten ist. Das würde bedeuten, man könnte beispielsweise ein Fenster nicht einfach als Objekt, sondern als ein Werkzeug modellieren, das lediglich eine Profilkurve erhält sowie Höhe und Breite, und das Werkzeug konstruiert daraus entsprechend einen profilierten Fensterrahmen mit Gehrungen in den Ecken durch Rückgriff auf einen Satz von Elementarwerkzeugen des Modellers, oder indem andere Werkzeuge aus der Bibliothek des Anwenders benutzt werden. Ein entsprechendes Konzept gibt es jedoch in keinem der kommerziellen Modeller, die bei Konstruktionsaufgaben im CAD oder bei Filmprojekten eingesetzt werden, es handelt sich daher um eine, wenn auch naheliegende, so doch hypothetische Möglichkeit zur Erweiterung parametrischer Modeller. Sieht man von den spezifischen Einschränkungen existierender Modeller ab und führt die Idee des Modifier-Stacks in dieser Weise konsequent weiter, kann man die Ausdrucksfähigkeit des parametrischen Modellierparadigmas folgendermaßen charakterisieren. Modifier-Baum Alle Schritte der Konstruktion eines Gegenstandes werden protokolliert, zu allen Operationen und ihren Parametern werden entsprechende Objekte, Modifier, in einem baumartig strukturierten Metaobjekt gespeichert. Modifier-Baum der Gesamtszene Dieser Mechanismus wird durchgängig für alle Operationen benutzt, die einen Einfluß auf die Geometrie der Gesamtszene haben, mithin sind die Modifier-Bäume der einzelnen Gegenstände Zweige des Szenenbaumes. Modifier-Baum mit eigener Identität Eine Reihe von Modifikationen kann als solche, unabhängig vom Basisobjekt, als neues Werkzeug betrachtet und in eine Bibliothek von anwenderdefinierten Werkzeugen eingetragen werden, die genauso benutzt werden können wie die eingebauten Elementarwerkzeuge des Modellers. 2.5 Generatives Modellieren Über die parametrische Modellierung läßt sich in der geschilderten Weise die erste der genannten Beschränkungen des rein objektorientierten Modellieransatzes beheben, den Verlust der Konstruktionsgeschichte. Die zweite Einschränkung jedoch besteht jedoch nach wie vor. Parametrische Modelle ermöglichen, wenn sie entsprechend realisiert sind, nachträglichen Zugriff auf sämtliche während der Konstruktion interaktiv spezifizierten Variablen. Diese Parameter aber sind natürlich nicht völlig willkürlich, sondern es bestehen vielfältige Abhängigkeiten. Gegenstände werden in virtuellen Räumen plaziert, ihre Position ist also abhängig von den Dimensionen, in denen modelliert wird. Gegenstände liegen auf anderen Gegenständen, diese Abhängigkeit wird mit Hilfe lokaler Koordinatensysteme in der Szenenhierarchie modelliert. Gegenstände werden in einer Reihe, auf einem Kreisbogen oder in anderen regelmäßigen Strukturen angeordnet, oder es werden Transformationen angewandt, deren Parameter regelgesteuert variiert werden. 24 KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG Konstruktionen dieser Art sind natürlich vor allem wichtig bei industriellen Anwendungen, wo technische Bauteile genauen Maßvorgaben genügen müssen. Bei Filmproduktionen oder allgemein im kreativen Bereich aber ist es ebenso wichtig, die vielen Parameter der Objekte in einer Szene zu beherrschen, um mit einer überschaubaren Menge von Eingriffen Änderungen auf einem hohen Abstraktionsniveau durchführen zu können, die sich auf viele Objekte zugleich auswirken. Dies ist im Zusammenhang mit der angestrebten Isolierung der richtigen Freiheitsgrade zu sehen, die ja ein grundsätzliches Problem der Modellierung darstellt. Weil der constraint-Ansatz wie beschrieben aber wegen der vielen denkbaren Arten von constaints und dem Problem über- bzw. unterbestimmter Systeme wenig praktikabel ist, muß über einen allgemeineren Ansatz nachgedacht werden. Interaktive Programmierbarkeit eines Modellers, eine interpretierte Sprache, in der die einzelnen Modellierschritte ausgedrückt werden können, unter Benutzung angepaßter Datenstrukturen, wäre eine mögliche Lösung, um die Ausdruckskraft eines Modellers zu erhöhen. Ein Modeller, der mit einer Programmiersprache Turingmächtigkeit erlangt, stellt das Optimum an Ausdrucksfähigkeit dar, wenn auch möglicherweise nicht an Modelliereffizienz. Um die Ausdrucksfähigkeit nicht auf Kosten der Effizienz zu erhöhen, sollte daher nach einer Lösung gesucht werden, die Konzepte einer Programmiersprache realisiert, ohne zur Eingabe von Programmtexten zu zwingen. Dabei mag erstaunen, daß prozedurale Konzepte wie Schleifen oder if-then-else-Verzweigungen im Kontext des Modellierens wenig Sinn machen. Dies liegt darin begründet, daß die Schwierigkeit der Modellierung weniger in einem Mangel an algorithmischer Flexibilität liegt, sondern mehr in der Beschreibung aufwendiger Datenstrukturen. Als Resultat eines Modellierprozesses erhält man nicht einen komplizierten Algorithmus, sondern eine Szene, gegeben durch komplexe, verschachtelte, untereinander verbundene Datenstrukturen. Der Modellierprozeß selber ist iterativ, allgemein werden Objekte in einer Szene schrittweise verändert. In der Modellierung kommen zudem hochstrukturierte Datenstrukturen zum Einsatz, wobei zwischen den Daten viele funktionale Abhängigkeiten bestehen. Daher sollte geprüft werden, in wiefern sich funktionale Konzepte ergänzend zum bestehenden objektorientierten Ansatz verwenden lassen. 2.5.1 Funktionale Konzepte in der Modellierung Der Kern einer Integration funktionaler Konzepte ist es, die statischen Beziehungen in objektorientierten Modellen dadurch dynamischer zu machen, daß die funktionalen Abhängigkeiten expliziert werden. Slots enthalten somit nicht mehr nur etwa eine Zahl als Parameter, sondern eine Funktion, die eine Zahl produziert und dafür wiederum bestimmte Eingabeparameter braucht, die entweder ebenfalls auf irgendeiner Berechnung beruhen, oder die einfach zu Parametern des Modells erklärt werden und erst im weiteren Verlauf des Modellierens spezifische Werte erhalten. Somit erhält man parametrisierte Konstruktionen. Dies bedeutet aber gerade, daß Modellierung und funktionale Programmierung in sofern eng verwandt sind, als in beiden Gebieten die Abbildung das zentrale Konzept ist. Der Modellierer wendet iterativ Funktion um Funktion, die ihm sein Modeller zur Verfügung stellt, auf die Szene und ihre Elemente an, bis er sein Ergebnis “berechnet” hat; er bildet also Szenen auf Szenen ab. Andererseits kennt die funktionale Programmierung noch eine Reihe von Konzepten, die in der Modellierung bislang nicht verwendet werden. Dazu gehören höherere, also funktionswertige Funktionen, die auch Funktionen als Parameter erhalten können, sowie das Prinzip der referentiellen Transparenz, das man sich als Grundlage für ein unbegrenztes Undo vorstellen kann. Zwei Beispiele mögen die Nützlichkeit des funktionalen Paradigmas für die Modellierung demonstrieren. 2.5. GENERATIVES MODELLIEREN 25 Deformationen parametergesteuert angewandt Gegeben die Aufgabe, einen einfachen Zaun entlang einer Freiform-Kurve in der xy-Grundfläche zu modellieren. Die senkrechten Sprossen des Zaunes seien Zylinder mit elliptischem Querschnitt und die kürzere Hauptachse einer Sprosse soll in Tangentenrichtung der Kurve orientiert sein. - Diese mit einem konventionellen Modeller mühsame Aufgabe ist offensichtlich empfindlich gegen Änderung der zugrundeliegenden Freiformkurve und gegen Änderungen der Zaunhöhe. Funktional gesehen sind dies aber nur Eingabeparameter des Konstruktionsprozesses. Austauschen von Stilelementen Neben Zaunverlauf und Höhe kann aber auch die Form der Sprossen als Eingabeparameter der Konstruktion gesehen werden. Angenommen, die Form der Sprossen soll geändert werden, die Zylinderform wird durch einen Kegelabschnitt ersetzt. Es ist nicht einzusehen, warum die Konstruktion nun komplett wiederholt werden sollte. – Würde man diese Änderbarkeit bei einem Modeller mit parametrisierbaren Modifier-Bäumen noch mit Hilfe der Referenztechnik realisieren, so stöße man aber spätestens an Grenzen, wo verlangt wird, an den Einzelelementen noch Modifikationen vorzunehmen, die von der Stelle abhängig ist, wo die Elemente eingesetzt werden, wie etwa das Blending benachbarter Sprossen, oder wenn die Sprossen parameterabhängig verformt werden sollen. Die Integration funktionaler Konzepte ist der Kern der Generativen Modellierung. Dabei betrachtet man die Funktionen als Erzeugende der Geometrie, die geometrischen Objekte und ihre Transformationen werden als Abbildungen betrachtet. Dies ist die konsequente Weiterführung von Ideen wie einem Modifier-Baum, und die funktionale Programmierung ist das dazu passende Programmierparadigma wegen der Entsprechung von Funktionen eines Programms und den Werkzeugen eines Modellers, und weil in beiden Fällen die wichtigste Verknüpfung die Komposition ist. Somit findet man hier eine theoretisch fundierte Grundlage, um den Konstruktionsprozeß eines Modells zu beschreiben. Andererseits ist nicht klar, ob eine allgemeine funktionale Programmiersprache das geeignete Werkzeug ist, um wirkliche Modeller zu implementieren, oder ob man Entsprechendes innerhalb des objektorientierten Paradigmas verwirklichen kann, wenn man Abbildungen über Metaobjekte implementiert. Beide Ansätze werden in Kapitel 4 untersucht. 2.5.2 Paradigmenwechsel Führt man die Idee des Modellierens mit Abbildungen konsequent weiter, bietet es sich an, auch die Basiselemente der Geometrie, Kurven und Flächen, nicht einfach als Objekte im Sinne der OOP zu begreifen, sondern ebenfalls funktional, als Abbildungen. Kurven werden in der generativen Modellierung daher in der parametrischen Form als Abbildungen eines Intervalls R ! R3 aufgefaßt, Flächen als Abbildungen von einem Intervall R2 ! R3. Diese Sichtweise gestattet eine elegante Definition von Werkzeugen, die Flächen oder Kurven bearbeiten, einfach als Transformationen R3 ! R3 , sodaß eine transformierte Fläche als Komposition von Funktionen R2 ! R3 ! R3 und eine transformierte Kurve als Funktion R ! R3 ! R3 begriffen werden kann, wobei die gleiche Transformation benutzt werden kann. So wird auch die natürliche Bedeutung höherer Funktionen deutlich: Eine Operation, die aus einer Fläche eine neue, zum Beispiel deformierte, Fläche erzeugt, ist als Funktion mit folgender Signatur anzusehen: surface_deformation: ( R2 ! R3 ) ! ( R2 ! R3 ) Will man eine Konstruktion definieren, wo an einer bestimmten Stelle ein solches Werkzeug benutzt wird, dieses Werkzeug aber austauschbar sein soll, so realisiert man dies, indem die Kon- 26 KAPITEL 2. EVOLUTION DER GEOMETRISCHEN MODELLIERUNG struktion parametrisiert wird und der entsprechende Parameter surface_deformation eben gerade diese Signatur erhält. Auf diese Weise realisiert man die Typsicherheit, und zwar vorzugsweise in einer für den Benutzer transparenten Weise. Ein modelliertes Objekt wird so nicht mehr nur als eine hochstrukturierte Verkettung von Daten angesehen, sondern kann selber Rückgabewert einer Abbildung sein. Damit realisiert man die Unabhängigkeit von zum Teil interaktiv spezifizierten Eingabedaten und der abstrakten Verknüpfung dieser Parameter, und in offensichtlicher Weise sind so die angestrebten Ziele Änderbarkeit und Wiederverwendbarkeit erreicht. Damit hat einen Paradigmenwechsel vollzogen, denn Objekt und Werkzeug sind nunmehr synonym zu verwenden. 2.6 Zusammenfassung Ordnet man die Evolution von Modelliertechniken in einen Zusammenhang, um Entwicklungstendenzen abzulesen, so ist zu beobachten, daß Änderbarkeit und Wiederverwendbarkeit von Modellen zunehmend an Bedeutung gewinnt. Dies wird dabei mit den Mitteln des objektorientierten Paradigmas in der Weise realisiert, daß Slots, Eigenschaftsfelder von Objekten, nicht nur Werte enthalten können, sondern auch Verweise auf andere Objekte. Damit sieht man eine Szene zusammen mit dem Produktionsprozeß als azyklischen Graphen von Objekten, wobei die Kanten Modifikationen darstellen, Übergänge von einem Ausgangsbjekt zu einem modifizierten Objekt. Diese Denkweise ermöglicht auch Konzepte wie einen Modifier-Stack, stellt also einen Beweis für die Tragfähigkeit des objektorientierten Paradigmas dar. Andererseits fehlt es in konventionellen Modellern an Möglichkeiten, um funktionale Abhängigkeiten zwischen Objekten auszudrücken. Automatische Berechnung von Objektparametern modellieren zu können ist aber wichtig, wenn man Gegenstände oder ganze Teilszenen auf hohem Niveau manipulieren möchte, und wenn man viele Gegenstände mit gleichartigen Konstruktionsschritten hat, die man gruppieren möchte, um eine Bibliothek von benutzerdefinierten parametrisierten Werkzeugen anzulegen. Die Generative Modellierung basiert auf dem Konzept der Abbildung. Zum einen kann man in diesem Zusammenhang sehr allgemein jede Veränderung bei der Modellierung einer Szene als Ergebnis einer Abbildung bezeichnen, andererseits kann man mit geeigneten Abbildungen auch statische Abhängigkeiten zwischen Objekten ausdrücken. Man kann daher davon sprechen, daß Modellierung funktional objektorientiert ist. Was dies im einzelnen bedeutet und wie die Idee des Modellierens mit Abbildungen präzisiert werden kann, ist Gegenstand dieser Arbeit und einige Ansätze dazu werden in den folgenden Kapiteln vorgestellt und untersucht. Kapitel 3 Grundlagen Es gibt eine ganze Reihe verschiedener Ansätze zur Definition von virtuellen Welten. In diesem Kapitel soll die Bandbreite der Möglichkeiten aufgezeigt werden, wie in der Praxis modelliert wird. Zum einen bewegt sich dies auf der Ebene von Dateiformaten, zum anderen gibt es Programmierschnittstellen, APIs (application programmer interfaces), die den Zugriff auf 3D-Objekte auf einer hohen Abstraktionsstufe gestatten. Daneben sind an Universitäten Programmpakete zur wissenschaftlichen Visualisierung entstanden, und es existieren Arbeiten zum Softwaredesign von Klassenbibliotheken, bei denen der Schwerpunkt auf das Finden einer wohldefinierten Klassenhierarchie als Grundlage für spätere Erweiterungen gelegt wurde. Von zunehmender Wichtigkeit im Zuge der Ausweitung des World Wide Web sind Beschreibungssprachen, um dreidimensionale Geometrien über Netzwerke zu schicken. Dies geschieht zum Teil mit der Intention, Anwendern an verschiedenen Orten die Möglichkeit zu bieten, sich in einem gemeinsamen virtuellen Raum zu treffen. Die Standardsprache für diesen Zweck ist die ’virtual reality modeling language’ VRML, ein ASCII-Dateiformat für Geometrien. Nachdem sich die erste Version dieser Beschreibungssprache als unzulänglich für einige der angestrebten Anwendungen erwiesen hat, wurde kontrovers diskutiert, wie ein zukünftiges Dateiformat aussehen könnte, das die Möglichkeiten von VRML erweitert. Im Zusammenhang dieser Arbeit ist dabei vor allem die Frage von Interesse, auf welcher Ebene Modellierung stattfinden soll, und welche Ausdruckskraft ein Dateiformat hat. – Ein Vergleich von Modellieransätzen auf Basis von Dateiformaten ist dabei zweifellos fundierter als der direkte Vergleich von interaktiven Modellern, die sich nur schwer formalisieren lassen, denn es läßt sich damit feststellen, welche Lösungen für bestimmte Modellierprobleme formuliert werden können. In einer Seminararbeit von Oliver Jucknath [Juc95] wird der Versuch gemacht, Kriterien für eine Programmiersprache festzulegen, die zur Beschreibung von dreidimensionalen Objekten und ganzen Szenen geeignet ist, vor allem im Hinblick auf ihre Verwendung als Dateiformat. Jucknath weist dabei auf eine ganze Reihe von Problemen im Zusammenhang mit der Ausdrucksfähigkeit und der Modelliereffizienz konventioneller Modeller hin, die zum Teil auch in dieser Arbeit schon angesprochen worden sind. Er präsentiert die Wendeltreppe als Paradebeispiel eines hochstrukturierten Modells, das für konventionelle Modeller problematisch ist und empfindlich gegenüber Änderungen der Konstruktionsvorgaben. Funktional gesehen sind Stufenhöhe, Rotationswinkel pro Stufe und Form einer Stufe aber nichts als Eingabeparameter eines Modells. Ein Dateiformat besitzt aber nicht nur eine Bedeutung für die Ein- und Ausgabe definierter Szenen, sondern auch für die interne Arbeitsweise eines Modellers und die Möglichkeiten, die er einem Benutzer bietet. Denn wenn in einer Ausgabedatei ein strukturiertes Objekt beschrieben werden soll, so muß der Benutzer auch eine Möglichkeit besitzen, dem Modeller diese Struktur mitzuteilen. Das Problem besteht also darin, im interaktiven Prozeß eine Art Programmierung zuzulassen. Jucknath schlägt 27 28 KAPITEL 3. GRUNDLAGEN vor, dem Benutzer eine hybride Ansicht des Modells zu präsentieren, einerseits die dreidimesionale Darstellung zusammen mit einer Werkzeugleiste wie in einem konventionellen Modeller, parallel dazu aber ein aus den Manipulationen in der Szene generiertes Skript, das bei der Benutzung eines Werkzeugs um die entsprechenden Funktionsaufrufe ergänzt wird. Das bedeutet, daß der Programmtext zum größten Teil automatisch generiert wird, der Benutzer hat aber auch die Möglichkeit, zwischen dreidimensionaler Ansicht und textueller Repräsentation zu wechseln. Einerseits gibt es nämlich Aufgaben, die einfacher direkt in 3D zu lösen sind, anderes kann aber symbolisch leichter ausgedrückt werden. Das bedeutet, man erspart dem Benutzer nicht gänzlich die Programmierarbeit, fordert aber auch nicht, daß der gesamte Produktionsprozeß abstrakt und ohne direkten Bezug zur dreidimensionalen Ansicht erfolgen muß. Neben dieser hybriden Eingabemethode schlägt Jucknath in seinem Papier auch einen graphischen Programmieransatz vor. In Programmen wie dem Bildverarbeitungssystem Khoros können sogenannte Dataflow-Maschinen zusammengesetzt werden. Dabei werden Werkzeuge durch grafische Icons repräsentiert, die über Verbindungsmöglichkeiten zu anderen Icons verfügen. Durch Ziehen von Verbindungslinien kann man den Datenfluß lenken und Ausgabeparameter mit Eingabeparametern verbinden. Ein solcher Datenflußgraph entspricht zunächst einem straightline-Programm, das weder Verzweigungen noch Schleifen noch Rekursion kennt. Schleifen und Verzweigungen erhält man dann durch spezielle Werkzeuge, die Abbruchbedingung bei zyklischen Verbindungen spezifizieren. Rekursion erhält man erst, wenn man das Konzept der Gruppierung von Werkzeugen zu neuen Werkzeugen einsetzt, was einer Funktionsdefinition bzw. der Idee der benutzerdefinierten Werkzeugbibliothek entspricht. Rekursion ist dann die Möglichkeit, ein neues Icon bereits in dem Graphen einzusetzen, das dieses Werkzeug definiert. Besitzt dieser Ansatz auch die gleiche Ausdruckskraft wie herkömmliches Programmieren, so ist das Zusammensetzen doch sehr mühsam, wenn die Werkzeuge auf einem sehr niedrigen Level arbeiten, etwa auf der Ebene von Ausdrücken einer Programmiersprache. Der Ansatz der Dataflow-Maschine ist daher im Zusammenhang mit komponentenbasierter Programmierung zu sehen, wo die Icons sehr mächtige Operationen repräsentieren, die man flexibel zusammensetzen kann. – Das Zusammensetzen von Abbildungen (u; v) 7! (x; y; z) bewegt sich jedoch zunächst auf einem vergleichsweise niedrigen Abstraktionsgrad, komponentenbasierte Programmierung ist mehr beim Programmieren im Großen anzuwenden. Ein weiteres Problem, auf das Jucknath in seiner Arbeit hinweist, ist die Parameterflut, die man nicht vermeiden kann, wenn man parametrisierte Objekte zu einem neuen Objekt zusammensetzt. Das neue Werkzeug erhält ja zunächst die Summe der Parameter aller Unterobjekte als eigene Eingabeparameter. Besonders bei komplizierten Objekten wie etwa einem Einfamilienhaus kann man sich viele mögliche Parameter vorstellen, die ein Anwender nachträglich noch verändern können möchte. Dazu mag die Form von Fenstern gehören, die Anzahl der Stockwerke, vielleicht sogar der gesamte Grundriß als ein komplexer Parameter, ganz abgesehen von Zierobjekten und Ornamenten, die in bestimmter regelmäßiger Weise an einer Fassade angebracht werden sollen. Stellt man sich ein Dateiformat vor, das all diese Änderungsmöglichkeiten in einer strukturierten Weise ermöglicht, so muß eine Parametergruppierung sattfinden, man sollte also in der Lage sein, eine Liste von Parametern komplett auszutauschen. Komplexe Parameter sind dabei als active styles vorzustellen, die selbständig Veränderungen an einem Modell durchführen können. Dies ist aber nur eine andere Sprechweise für Abbildungen als Parameter, auch hier wird also die Notwendigkeit von höheren Funktionen deutlich. Daher kann man viele Kriterien, die Jucknath in seinem Papier nennt, als Beleg für die Notwendigkeit eines Dateiformates anführen, das funktionale Konzepte integriert. Im Rahmen dieser Arbeit ist dabei die Frage nach dem richtigen Dateikonzept für die Generative Modellierung von Interesse, vor allem in Hinblick auf einige Unzulänglichkeiten der im folgenden vorgestellten GENMOD-Programmiersprache. 3.1. GENMOD-PROGRAMMIERSPRACHE NACH SNYDER 3.1 29 GENMOD-Programmiersprache nach Snyder Die Idee der Generativen Modellierung wurde ursprünglich in einem Buch von John M. Snyder [Sny92] am California Institure of Technology (CalTech) vorgestellt. Darin beschreibt er einen alternativen Weg der Modellierung, und zwar über eine interpretierte Programmiersprache. Das bedeutet, Snyder geht vollkommen ab von der herkömmlichen Idee eines interaktiven Modellers und zwingt den Benutzer, die Struktur seiner Gegenstände zu verstehen, um diese auf einer hohen Abstraktionsstufe über Abbildungen zu beschreiben. James Kajiya schreibt im Vorwort zu diesem Buch euphorisch: Before this work, I thought about a shape as a collection of polygons, or a sculpted surface. But that view is very limited. With a sculpted surface there’s really no difference between a spoon shape and a chair shape; it’s all a matter of positioning the control points in the right places. But a spoon shape has an inner logic, shared by all spoons – and that logic is completely different from that of a chair. John and I have spent many hours tying to discover the logic of different everyday shapes. It’s an intellectually challenging and exciting endeavour, one that is quite pleasurable when one hits on the right logic of a shape. Because of this, shapes are no longer just inscrutable lumps, but a series of puzzles – sometimes easy and sometimes difficult. Es geht bei der Generativen Modellierung also darum, eine Möglichkeit zu finden, die innere Logik von Gegenständen zu explizieren. Snyder definiert ein generatives Modell als eine Form, die durch die kontinuierliche Transformation einer anderen Form, des Generators, entsteht. Das Paradebeispiel für eine solche Form ist die Sweep-Fläche, der verallgemeinerte Zylinder, der dadurch entsteht, daß eine geschlossene Kurve eine Raumkurve entlanggezogen wird. Die Transformation ist in diesem Fall einfach eine Bewegung durch den Raum, doch im allgemeinen können generative Modelle aus einer großen Anzahl von Elementaroperationen zusammengesetzt sein. Bei solchen komplexen Werkzeugen wie dem Sweep spricht er von Metashapes, damit wird die Äquivalenz von Gegenstand und Werkzeug in generativen Modellen ausgedrückt. Snyder stellt dabei zunächst ganz abstrakt Kriterien für die gesuchte Beschreibung von dreidimensionalen Modellen auf. Unter anderem führt er an: Abgeschlossenheit unter Komposition Größe der Klasse darstellbarer Modelle Kompaktheit der Darstellung Kontrollierbarkeit des Modells Änderbarkeit Neben dem Problem, eine zutreffende Sprache für die Modellierung zu finden, behandelt Snyder in seinem Buch vor allem Ideen zur Intervallrechnung, mit denen sich unter anderem das Lösen von impliziten Gleichungen effizient und mit kontrollierter Genauigkeit durchführen läßt. Dies ist insbesondere wichtig, um constraint-Methoden verwenden zu können, und um implizite Funktionen zu explizitisieren. Der für die Programmiersprache gewählte Ansatz ist ein prozeduraler. Die Snyder-Gruppe in Pasadena hat dazu einen Interpreter für eine C-ähnliche Sprache konstruiert, die GENMODProgrammiersprache. Die Interpreter-Fähigkeit ist deshalb wichtig, weil der Modellierprozeß als interaktive Programmierung gedacht ist und die Turnaround-Zeiten bei Kompilation als zu lang angesehen wurden. Snyder sieht durchaus die Problematik, die in der Wahl einer solchen Benutzerschnittstelle liegt, und schlägt die Benutzung von GENMOD im wesentlichen zwei Gruppen vor, und zwar Programmierern und Forschern. Programmierer haben dabei die Aufgabe, das System KAPITEL 3. GRUNDLAGEN 30 an bestimmte Aufgabengebiete anzupassen, sodaß Anwender nicht mit der Sprache als solcher in Berührung kommen, für Forscher andererseits ist nach Snyder die Flexibilität in der Modellierung wichtiger ist als ein graphisches Benutzerinterface. In GENMOD lassen sich C-Funktionen schreiben, die als Input-Parameter Objekte vom Typ MAN erhalten und daraus ein neues MAN-Objekt zusammenbauen. Solche Funktionen sind daher gleichbedeutend mit Metashapes und werden in der Terminologie der Generativen Modellierung Operatoren genannt. Der Datentyp MAN – für ’manifold’, Mannigfaltigkeit – enthält allgemein eine Abbildung Rm ! Rn. Der Name impliziert, daß MAN-Objekte ganze Mannigfaltigkeiten enthalten, dem ist jedoch nicht so. Zwar können mehrere parametrische Patches zu einem gemeinsamen Parameterbereich verbunden werden, doch ein MAN kann nicht mehrere Parameterbereiche enthalten, wie es bei Mannigfaltigkeiten mit mehreren Karten der Fall ist. Die GENMOD-Programmiersprache stellt eine begrenzte Menge grundlegender Operatoren zur Verfügung, diese Grundoperationen sind dabei abgeschlossen in dem Sinne, daß von einem Operator als Rückgabewert stets eine Abbildung MAN, ein Vektor MAN_ARRAY oder eine Matrix MAN_MATRIX von Abbildungen erzeugt wird. Diese Grundoperationen sind dabei vollständig für eine sehr große Klasse von Formen, sowohl für parametrische Funktionen als auch für implizite Kurven und Flächen. Im einzelnen gibt es folgende Arten von Grundoperatoren: Elementare Operationen Das sind Konstanten und Koordinatenfunktionen, sowie arithmetische, logische, und mathematische Funktionen, dazu Vektor- und Matrizenfunktionen. Differentiation, Integration Elementaroperationen gestatten symbolische Differentiation, zusammengesetzte Operationen werden rekursiv mit Hilfe der Ableitungen der Einzelfunktionen differenziert. Kann nicht symbolisch abgeleitet werden, so geschieht dies in numerischer Näherung. Das Integral einer Funktion bezüglich einer Variablen ist ein Skalar, hängt diese Funktion noch von anderen Parametern ab, so ist das Integral eine Funktion dieser anderen Parameter. Indizieren, Branching Es können Funktionen bezüglich einer Variablen “aneinandergehängt” werden, die Parameterintervalle der einzelnen Funktionen werden zu einem kontinuierlichen Parameterbereich zusammengesetzt. Auf diese Weise lassen sich Funktionen tabellieren, insbesondere erhält man so Polygonzüge. Kurven und Flächen können in tabellierter Form aus Dateien eingelesen werden. Umkehrung einer monotonen Funktion Es kann die Inverse einer Funktion R ! R bestimmt werden, zu x aus dem Bildbereich erhält man die Stelle t, wo f (t ) = x gilt. Lösung eines n-dimensionalen constraints Dies ist im wesentlichen die Lösung eine Systems impliziter Gleichungen. Der Rückgabewert ist ein einzelner Punkt, der den constraint erfüllt, aber von den weiteren Parametern der Zielfunktionen abhängen kann. Um den Schnitt zweier Flächen p(u; v) und q(s; t ) bezüglich gegebenem t zu finden, kann man zum Beispiel den Ansatz MAN c=solve(p(u; v) = q(s; t)) wählen, t parametrisiert dann die zurückgegebene Kurve c. Minimieren unter Nebenbedingungen Dies erfolgt ähnglich wie die constraint-solution, die Nebenbedingungen spezifizieren hier jedoch zunächst zulässige Gebiete im Parameterraum der zu minimierenden Funktion. Der 3.1. GENMOD-PROGRAMMIERSPRACHE NACH SNYDER 31 Rückgabewert ist ein Punkt, wo die zu minimierende Funktion ihr Minimum über den zulässigen Punkten annimmt. Lösen von Anfangswertproblemen von Differentialgleichungen erster Ordnung Mit diesem Operator kann man die Trajektorie in einem Vektorfeld f bestimmen, indem für einen Anfangspunkt y0 die Funktion y berechnet wird, für die gilt, daß y(t0) = y0 und y0 (t ) = f (y; t ) ist. Komplizierte zusammengesetzte Operatoren oder Metashapes sollen es ermöglichen, Formen auf einem hohen Abstraktionsniveau zu beschreiben und die funktionale Gliederung eines Gegenstandes in den Vordergrund zu stellen, statt wie bei herkömmlichen Modellern die sehr anschauliche, aber letztendlich strukturverschleiernde 3D-Darstellung zur Grundlage der Arbeit zu machen, wo einfach solange an den verschiedenen Kontrollpunkten von Freiformflächen manipuliert wird, bis das Ergebnis einigermaßen stimmt. Andererseits verlangen viele Codefragmente, die solche Metashapes beschreiben, vom Modellierer, der in diesem Fall zu einem Programmierer wird, eine nicht unerhebliche räumliche Vorstellungskraft, wenn komplizierte Verknüpfungen zwischen den verschiedenen Koordinaten der jeweiligen Input-Kurven ausgeführt werden. Als Beispiel für die Definition eines höheren Operators soll die Definition eines speziellen Sweep dienen, des ’wire product’. Dabei geht es darum, ein zweidimensionales Profil, die ’cross-curve’ γ(u), eine ebenfalls zweidimensionale ’wire curve’ δ(v) entlangzuziehen. Der Tangentialvektor tδ (v) = (t1(v); t2(v)) der wire-curve wird benutzt, um durch die Transformation (x; y) 7! (,y; x) einen Vektor zu erzeugen, der senkrecht auf der wire-curve steht: tδ (v) = dδ1 dv (v) dδ2 dv (v) ! = t1(v) t2(v) ! ; ! 0 1 , t2(v) B γ (v) t1(v) + δ(u) C S(u v) = @ 1 A ; γ2 (v) In GENMOD sind die arithmetischen Operatoren von C überlagert, sodaß sie nicht nur auf Zahlen, sondern auch auf Vektoren und Matrizen anwendbar sind, @ ist das kartesische Produkt, mit dem Zahlen und Vektoren zu neuen Vektoren konkateniert werden. Zudem ist es möglich, extern mit einem 2D-Editor gezeichnete Kurven einzulesen. In GENMOD realisiert das man wireproduct S(u; v) dann mit folgendem Codefragment: MAN m_wire(MAN cross, MAN wire) { MAN that = m_derivative(wire,wire->input_list[0]); MAN t = m_normalize(that); MAN n = @(-t[1], t[0]); return @(n*cross[0]+wire, cross[1]); } MAN cross = m_crv("cross.crv",m_x(0)); MAN wire = m_crv("wire.crv", m_x(1)); MAN racket_frame = m_wire(cross,wire); Mit diesem Beispiel wird demonstriert, wie eine Abbildung aufgebaut wird. Was geschieht nun mit dem Resultat, dem zusammengesetzten Operator racket_frame? Ganz entscheidend für den Abbildungsbegriff, den Snyder verwendet, ist das Konzept der Operatormethoden, die auf Operatoren angewandt werden können. KAPITEL 3. GRUNDLAGEN 32 GENMOD unterstützt neben der Synthese von Operatoren nämlich auch die Analyse zusammengesetzter Abbildungen vom Typ MAN. Eine lokal rekursiven Operatormethode ist eine Methode, deren Ergebnis für einen Operator P( f1; :::; fn) in einem Punkt p ausgerechnet werden kann, wenn der Wert dieser Methode auf den Eingabefunktionen f1; :::; fn (vom Typ MAN) bekannt ist. So ist beispielsweise die Auswertung eine lokal rekursive Methode für den Additionsoperator, denn f + g kann berechnet werden, wenn die Werte von f und g bekannt sind. Berechnung des beR stimmten Integrals dagegen ist nicht lokal rekursiv für die Division, denn f =g kann nicht durch R R f und g berechnet werden. In GENMOD unterstützen sämtliche elementaren Operatoren die folgenden Operatormethoden: Auswertung Symbolisches Bilden der partiellen Ableitungen Berechnung der Inklusionsfunktion Auswertung ist für die Grundoperationen Integration, Umkehrung und Lösen des Anfangswertproblems zwar streng betrachtet nicht lokal rekursiv, kann aber durch mehrfaches Auswerten der Eingabefunktionen berechnet werden. Differentiation ist für die elementaren arithmetischen Funktionen durchaus lokal rekursiv, für Operatoren, die durch numerische Verfahren ausgerechnet werden wie das bestimmte Integral wird die Ableitung in numerischer Näherung ausgerechnet. Der Differentiations-Operator gibt angewendet auf ein MAN-Objekt ebenfalls ein MAN-Objekt zurück. Die intervallwertige Inklusionsfunktion einer Funktion gibt an, wie stark ihre Werte über einem bestimmten Parameterbereich variieren. Genauer heißt das, 2 f ist eine Inklusionsfunktion von f , wenn für alle x aus dem Parameterbereich von f gilt: f (x) 2 2 f (I ) für alle Intervalle I mit x 2 I. Für die elementaren arithmetischen Operationen lassen sich die Inklusionsfunktionen durch Intervallarithmetik bestimmen, wo sorgfältig darauf geachtet wird, Intervallgrenzen in die richtige Richtung zu runden, wenn ein Intervall durch einen Operator auf ein anderes Intervall abgebildet wird. Bei Funktionen wie Sinus, Wurzel etc. lassen sich die Bildintervalle durch Fallunterscheidungen bestimmen, im ungünstigsten Fall muß eine Maximum- bzw. Minimumsuche erfolgen. Es wird also ein nicht unerheblicher Aufwand getrieben, um nachzuverfolgen, welchen Wertebereich eine zusammengesetzte Funktion bezüglich eines Parameterintervalls hat, doch dies ist Grundvoraussetzung für eine effiziente Implementierung der Operatoren zum Lösen einer Menge logisch verknüpfter constraints und zum Minimieren unter Nebenbedingungen. Beide Operatoren gehen dabei in ähnlicher Weise vor, indem ein Grundintervall immer weiter aufgeteilt wird, die Inklusionsfunktionen der einzelnen constraints geben dann Auskunft darüber, ob es sich lohnt, ein bestimmtes Teilintervall weiter zu betrachten. Wenn ein constraint in einem bestimmten Gebiet nicht erfüllt werden kann, so enthält seine Inklusionsfunktion den Wert 1 =Wahr nicht mehr, man hat somit ein eindeutiges Kriterium zur Beurteilung von Gebieten, und beide Algorithmen können relativ einfach implementiert werden, weil die eigentliche Arbeit von den Inklusionsmethoden der einzelnen Operatoren erledigt wird. Diese beiden Operatoren haben außer bei Schnittbestimmungen noch viele weitere Einsatzmöglichkeiten in der Modellierung. Mit Hilfe der Minimierung unter Nebenbedingungen kann man Raytracing implementieren, wenn man den Punkt auf einem Strahl sucht, der einer parametrischen Fläche am nächsten liegt, und man kann eine Kollisionsdetektion durchführen, wenn man nach dem minimalen Abstand zwischen zwei Freiformflächen sucht. In einfacher Weise kann man auch Offsetkurven und -flächen zu einer gegebenen Menge S berechnen indem man Punkte der folgenden Menge bestimmen läßt: fxj miny2S dist(x; y) = rg. 3.2. MRT 33 Mit GENMOD ist es sogar gelungen, CSG auf Freiform-Gegenständen durchzuführen. Dazu wird mit Minimierung unter Nebenbedingungen der Schnitt zweier Oberflächen durchgeführt, indem die Projektion der Schnittkurve in die beiden Parameterbereiche der Flächen berechnet wird. Dies sind möglicherweise mehrere geschlossene Kurven, die einen Parameterbereich in zwei Teile aufteilen, die die Teile der Fläche repräsentieren, die im Inneren oder auf der Oberfläche des zusammengesetzten Gegenstands liegen, und entsprechend angezeigt oder weggelassen werden. Damit werden beide Oberflächen als getrimmte Patches dargestellt. Dieses Verfahren kann man nun aber fortsetzen, bei Schnitt mit einem dritten Freiform-Körper entstehen wiederum Schnittkurven mit den beiden ersten Körpern, und man kann schließlich die noch anzuzeigenden Teile der Oberflächen aller Körper bestimmen, wenn man die Punkte kennt, wo sich die projizierten Schnittkurven in allen Parameterbereichen schneiden. Die Ränder der weggetrimmten Bereiche erhält man schließlich durch stückweises Aneinandersetzen von Teilen dieser projizierten Schnittkurven. Im weiteren Verlauf dieser Arbeit wird die von Snyder eingeführte Terminologie der Generativen Modellierung beibehalten. Das bedeutet, es wird auch bei den Genmod-Implementationen in C++ und Haskell von elementaren und höheren Operatoren gesprochen, die ihrerseits synonym zu Werkzeugen und Metashapes sind. Syntaktisch gesehen handelt es sich dabei jedoch stets um die gleichen Objekte, in der Rückführung all dieser Begriffe auf ein gemeinsames Konzept liegt gerade die Bedeutung des in 2.6 angesprochenen Paradigmenwechsels. 3.2 MRT Das ’minimal rendering toolkit’ (MRT) ist eine objektorientierte Programmierplattform, die in der Abteilung für Computergrafik der Universität Bonn entstanden ist. Es wird sowohl zur Forschung als auch zur Lehre eingesetzt und besteht aus einer Menge von über Jahre gereiften, hochgradig portablen C++-Bibliotheken, in denen viele Konzepte und Ideen der Computergrafik realisiert sind. Der Schwerpunkt und die ursprüngliche Motivation liegt dabei im Rendering komplexer Szenen begründet. So bietet das Paket die Möglichkeit, Szenen zu raytracen, oder eine approximative Darstellung einer Szene unter Ausnutzung eventuell vorhandener Grafikhardware eines Rechners darzustellen. Weiterhin gibt es die Möglichkeit, auf eine Szene das Radiosity-Verfahren anzuwenden, um den diffusen Lichtaustausch zwischen einzelnen planaren Patches einer adaptiven Approximation der Oberflächen in der Szene zu berechnen. Das Ergebnis einer solchen Rechnung ist eine physikalisch korrekt eingefärbte Szene mit adaptiv verfeinerten BReps der Objekte. Neben den Bibliotheken enthält das System auch einige ausführbare Anwendungen, zum einen einen Raytracer, der auf Kommandozeilenebene arbeitet und eine Bilddatei berechnet, zum anderen einen interaktiven Viewer für statische Szenen. Forschungsgegenstand ist beim MRT neben den eigentlichen computergrafischen Algorithmen auch die Frage, welches Softwaredesign nötig ist, um Erweiterbarkeit und Wiederverwendbarkeit der Algorithmen zu ermöglichen. Das Paket ist in C++ implementiert und besteht in Version 2.07 aus insgesamt über 550 Klassen, die nach Schwerpunktgebieten in die folgenden Bibliotheken aufgegliedert sind. gen: Generische Funktionen und Datenstrukturen In dieser Bibliothek liegen über 100 Klassen von Basis-Datenstrukturen, Vektoren, Matrizen, Listen und Datenstrukturen für die Farbenbehandlung. cgi: 2D-API CGI ist ein ISO-Standard zum plattformunabhängigen Zeichnen im Zweidimensionalen, es KAPITEL 3. GRUNDLAGEN 34 gibt hier eine Reihe von Befehlen zum Zeichnen von Linien, Kreisen, Ellipsen, Polygonen, zum Setzen von Zeichenattributen, zum Farbmanagement, zur Ausgabe von Buchstaben und Strings, und eine abstrakte Schnittstelle zu 2D-Eingabegeräten. cgi3d: 3D-API Die 3D-Erweiterung von cgi behandelt das approximative Rendering auf Systemen, die über keine Hardwareunterstützung verfügen, und emuliert dies softwaremäßig durch einen Scanline-Renderer. pen: Graphical user interface (GUI) Penguin ist ein objektorientiertes GUI, das es ermöglicht, Ereignisse, die von einem grafischen Fenstermanager bei der Benutzerinteraktion generiert werden, plattformunabhängig auf Funktionen eines Programms abzubilden. Es stellt eine erweiterbare Menge von Dialogobjekten (Widgets) zur Interaktion mit dem Anwender zur Verfügung. apps3d: Anwendungen Dies sind vier Anwendungen, die die Bibliotheken integrieren, ein Kommandozeilen-Raytracer, und drei interaktive Viewer, darunter einer mit Penguin-Interface und ein Viewer speziell für Radiosity-Anwendungen. mrt: Kern des Rendering-Toolkit Hier findet sich eine Hierarchie von über 280 Klassen, die Zugriff auf sämtliche Elemente einer Szene bieten. Zum einen findet man hier die Definition von Szenenobjekten wie Lichtern, Oberflächeneigenschaften und geometrischen Grundobjekten, sowie eine Implementation von BReps, die CSG erlauben. Hier schließlich ist hier auch der eigentliche Raytracer zu finden, in den ständig neue Verfahren wie volume rendering, photon- und path-tracing integriert werden. Das MRT-Paket enthält somit die Datenstrukturen, die für die Modellierung relevant sind. In diesem Zusammenhang ist vor allem das Objekt-Konzept des MRT von Interesse, das durch die Basisklasse vorgegeben ist, von der alle anderen Gegenstände abgeleitet sind. Die Schnittstelle für Gegenstände, die von einer zweidimensionalen Oberfläche begrenzt werden, befindet sich in der Klasse t_Object und ist ausschnittsweise folgendermaßen gegeben: class t_Object { ... /** Returns bounding volume object. Currently this is an axis-aligned bounding box.*/ virtual t_BVol boundingVolume () const; /** This abstract method tests for intersections between a given ray and this object and calculates the closest intersection point.*/ virtual t_Bool intersect (t_Ray& ray) = 0; /** This method generates an approximation stored in the object. qualityControl(in) controls the quality of the approximation: q = -n (n<10 integral) <=> reduce by n steps q = +n,(n<10 integral) <=> increase by n steps q in [-0.5, 0.5] <=> set ’absolute’ quality, default=0, To force refinement of planar faces use offset 10 */ virtual t_Bool approxShape (t_Real qualityControl=0); 3.2. MRT 35 /** Inverse mapping function [x,y,z] --> [u,v] for texture mapping, returns the 3D-point mapped onto [0,1]x[0,1]. */ virtual t_Bool mapInvers (const t_3DVector& xyzPoint, t_2DVector& uvPoint) const; /** This method moves an arbitrary vertex to the object boundary by intersecting a ray emanating from the vertex with the object as a default implementation. */ virtual void adjustVertex (t_Vertex* v); ... }; Die Eingabe einer Szene in den MRT geschieht durch Parsen einer Szenenbeschreibungsdatei. Dieses System ist modular in dem Sinne, daß Parser austauschbar sind und nur verlangt wird, daß ein Parser schließlich ein Szenenobjekt liefert, das im weiteren Verlauf Grundlage der Berechnungen ist. Eine Szene enthält dabei sämtliche geometrischen Objekte mit den notwendigen Transformationen, kann anstelle eines Objektes aber auch rekursiv eine weitere Subszene enthalten. Man spricht deshalb auch vom Szenenbaum. Der entscheidende Aspekt bei dieser Herangehensweise ist, daß Gegenstände im MRT in einer Weise definiert sind, daß sie beide wesentlichen Anforderungen für Modelle erfüllen, die Umwandlung in eine approximative Darstellung mit einstellbarerer Genauigkeit durch die Methode approxShape() und den Schnitt der Oberfläche mit einem Strahl durch intersect(). Der Vorteil dabei ist, daß das abstrakte Objekt weiterhin die Schnittstelle für den Zugriff auf die Gegenstände in der Szene bietet. Algorithmen können sich beider Zugänge zu einem Objekt bedienen, wodurch sich manche Aufgaben viel effektiver lösen lassen, als wenn nur eine Repräsentation eines Gegenstands verfügbar wäre. Strahlschnitte lassen sich in vielen Bereichen verwenden, von der Kollisionsdetektion über Sichtbarkeitsbestimmungen bis zu Modellen für diffusen Lichtaustausch, und die Approximation profitiert ebenfalls von der Verbindung zur Objektbeschreibung, weil durch adjustVertex() lokale Verfeinerungen an einer Approximation vorgenommen werden können. Dies ist immens wichtig beispielsweise beim Radiosity-Verfahren, wo im Laufe der Berechnung festgestellt wird, daß ein bestimmtes planares Patch einen relativ großen Teil der Energie in der Szene erhält oder es dort eine große Änderung in der Lichtverteilung gibt, weshalb dieses Patch feiner unterteilt werden muß. MRT-Objekte haben dabei grundsätzlich eine geschlossene Oberfläche, entsprechen also der Definition eines Gegenstandes wie sie in Abschnitt 2.1 gegeben wurde. Im MRT-Paket finden sich demnach auch viele Standardprimitive, von Kugeln über Kegel und Quader bis zu Rotationsobjekten und Metaballs. Insbesondere sind Gegenstände konzeptionell als Patchkomplexe vorgesehen, was sich auch bei der Approximation in sofern niederschlägt, als es mehrere Normalvektoren an einer Ecke eines MRT-BReps geben kann. Auf diese Weise kann man Knicke in der Oberfläche eines Objektes modellieren, und dies wird bei der interaktiven Darstellung in sofern berücksichtigt, als Farbinterpolation zwischen benachbarten planaren Patches, wie sie beim Gouraud-Shading eingesetzt wird, nicht über Objektkanten hinweg durchgeführt wird, Kanten zeichnen sich also durch Farbunterschiede deutliche ab. Andererseits sind MRT-Objekte nicht gezwungen, intern eine Patchkomplex-Datenstruktur zu benutzen, sondern es bleibt den einzelnen Objekten überlassen, Probleme zu lösen wie bei einem adjustVertex() in der Nähe einer Kante herauszufinden, welchem der benachbarten Patches eine gegebene BRep-Ecke zuzuordnen ist. Dies führt insbesondere an Knicken zu Problemen, etwa wenn zwischen zwei BRep-Ecken auf dem Rand einer Kegelgrundfläche eine weitere Ecke eingefügt werden soll. Verfeinert man den BRep, indem der Mittelpunkt der Kante als neuer Eckpunkt 36 KAPITEL 3. GRUNDLAGEN eingefügt wird, so hat adjustVertex() eigentlich nichts zu tun, denn die neue Ecke liegt als solche ja korrekt auf dem Kegelboden, also auf der Oberfläche, wenn auch nicht auf dem Knick. Solche Probleme werden dabei mit speziellen Heuristiken für jedes Objekt einzeln gelöst. Neben geschlossenen Gegenständen sind im MRT aber auch verschiedene Klassen von Patches mit Rändern als Ableitungen von t_Object enthalten, wie Drei- und Vierecke, Kreisscheiben, bikubische Bézierpatches und Höhenfelder. Diese werden als einzelne Objekte ohne Zusammenhangsinformationen behandelt. Das ursprüngliche Eingabeformat des MRT sind msd-Dateien, (für mrt scene description) , die im wesentlichen eine Beschreibung einer Szenenhierarchie enthalten, die genau dem schließlich konstruierten Szenenbaum entspricht. Für sämtliche MRT-Objekte gibt es msd-Schlüsselworte, die Objekte werden durch Angabe der Parameter beschriebenen, die vom Parser an den C++Konstruktor übergeben werden. Msd-Dateien können Zahlen- und Vektorvariablen enthalten, zusätzlich gibt es das Konzept der Referenzobjekte, sodaß zwei verschiedene Einträge im Szenengraphen auf das gleiche Objekt zeigen können, das nur einmal angelegt werden muß. Dies ist insbesondere für datenintensive Objekte vorteilhaft, es gibt zum Beispiel Scanner-Objekte, die Datensätze von 3D-Scans enthalten, die sehr groß sein können, oder BRep-Objekte, die im indexed-face-set-Format eingelesen werden können. BRep-Objekte haben keine abstrakte Forminformation mehr, sind also nur durch eine planare Approximation gegeben, und führen auch damit den Strahlschnitt durch. Dabei kommen Datenstrukturen zur hierarchischen Raumunterteilung zum Einsatz, die das Finden des getroffenen Polygons eines BReps dramatisch beschleunigen. Msd-Dateien enthalten somit im wesentlichen eine statische Szene, die in ein einzelnes Bild gerendert wird, es gibt im MRT augenblicklich noch keine Animationsobjekte, die es ermöglichen, Szenengraphen von der Zeit abhängig zu machen, entsprechende Ansätze befinden sich jedoch in der Entwicklung. Der MRT bietet insgesamt also die Möglichkeit, fertige Szene mit verschiedenen Verfahren zu rendern. Für die Modellierung von Szenen selbst liefert der MRT allerdings keine Unterstützung. Diese Arbeit wird der MRT als Programmierplattform für die Implementation der Generativen Modellierung über eine C++-Klassenhierarchie eingesetzt. Damit wird erreicht, daß generative Modelle plattformübergreifend mit modernen Renderingmethoden dargestellt werden können. Ohne die grundlegende Infrastruktur, die ein solch umfangreiches Paket von Klassenbibliotheken bietet, wäre es nicht möglich gewesen, sich alleine auf die Modellieraspekte der Arbeit zu konzentrieren, dies ist ein Beleg für den Nutzen, den der MRT als wohlfundierte Arbeitsgrundlage auch in Bereichen bringt, die sich nicht mit seinem Kerngebiet, dem Rendering, beschäftigen. Insbesondere das Objektkonzept des MRT bietet eine Möglichkeit, wie generative Modelle bis zum letzten Schritt, dem Rendering, in der ihnen eigenen zusammengesetzten Form beibehalten werden können. Das bedeutet, daß die Voraussetzung dafür besteht, auch während des Renderingprozesses selber die analytische Beschreibung generativ modellierter Kurven und Flächen zu benutzen. Aus dieser Perspektive ist der MRT das ideale Werkzeug für die Darstellung generativer Modelle. 3.3. OPEN INVENTOR 37 3.3 Open Inventor OpenInventor [WG94], [FG94] ist ein C++-API der Firma Silicon Graphics Inc. (SGI) zur Konstruktion von Szenengraphen. Es definiert darüberhinaus ein Dateiformat und ist vor allem wegen seiner Verwendung als Quasi-Standard zur Beschreibung von 3D-Szenen von Bedeutung. Dieses Format diente als Vorlage für die erste Version von VRML, dem plattformunabhängigen Standard für die Beschreibung von Geometrien und Szenen zur Übertragung über das World Wide Web (WWW). Das C++-API setzt dabei auf der Grafikbibliothek OpenGL von SGI auf, einem low-level-API zum Rendern approximativer Szenen, dessen umständliche, stackbasierte Programmierung anwenderfreundlich gekapselt wird. – Das Szenenkonzept von Inventor sieht dabei nicht nur die Beschreibung von statischen Szenen vor, sondern es können auch ’events’ und ’engines’ definiert werden. Events werden von den Knoten im Szenengraphen verarbeitet, die wissen, wie sie zu reagieren haben, engines dienen zur Animation von Teilen einer Szene oder zum Spezifizieren einzuhaltender Bedingungen. Inventor-Knoten sind von der Basisklasse SoNode abgeleitet und lassen sich im wesentlichen den folgenden Kategorien zuordnen: Shape nodes enthalten die Geometrie eines Körpers Property nodes verändern Eigenschaften von Körpern oder Szenen Group nodes dienen zur Verwaltung von Subszenen Daneben gibt es noch eine Reihe spezieller Knotentypen, zum Beispiel für Kameras und Lichter. Geometrien sind entweder Elementarobjekte wie Würfel, Kegel, Kugel, Zylinder und Text, NURBS-Freiformkurven und -flächen, oder reguläre oder irreguläre Polygonnetze. Mit properties werden Materialien und Texturen vergeben, es können das für die Szene zu verwendende Beleuchtungsmodell, die Verarbeitung von level-of-detail-Informationen, die Maßeinheiten und atmosphärische Effekte definiert werden, ebenso wie Transformationen von Gegenständen oder Subszenen. Der Szenengraph ist geordnet, Graphen oder Subgraphen besitzen eine Liste, keine Menge, von Söhnen. Das Setzen von Eigenschaften wird dadurch reihenfolgeabhängig, ändert man zwischen zwei Knoten die Materialfarbe, so wirkt sich das nur auf den zweiten Knoten aus. Transformationen werden akkumuliert, in sofern ist das Stack-Konzept von OpenGL in Inventor enthalten. Mit einem speziellen Gruppenknoten, einem ’separator’, lassen sich jedoch die momentanen Eigenschaften sichern, nach Abarbeitung des darunterliegenden Subgraphen werden die alten Eigenschaften wiederhergestellt. Inventor enthält neben einer Hierarchie von Knoten, nodes, auch eine Hierarchie von Aktionen, actions, zur Analyse eines Graphen. Dabei wird der Szenengraph traversiert und die einzelnen Knoten ausgewertet. Aktionen sind einmal das Rendern einer Szene, dann die Berechnung einer bounding box, das Speichern in eine Datei, das Finden akkumulierter Transformationen, aber auch der Strahlschnitt, das Suchen eines Pfades zu einem bestimmten Knoten und das Verteilen von events. Knoten können auf Ereignisse mit dem Aufruf einer callback-Funktion reagieren, auf diese Weise kann eine Anwendung mit einem Szenengraphen kommunizieren. Ein der Szene übergeordnetes Objekt, ein ’viewer’, kann zudem mit ’sensors’ ausgestattet werden, die in bestimmten Situationen Ereignisse auslösen. – Inventor stellt dem Anwendungsprogrammierer durch die vielschichtige Ereignisverwaltung also viele Möglichkeiten zur Verfügung, eigene Funktionalität in die vorgefertigte Struktur einzubringen. Im Zusammenhang mit der Modellierung sind aber vor allem engines von Interesse. – Um Abhängigkeiten zwischen Größen in einer Szene auszudrücken, ist es nicht zweckmäßig, Abbildungen durch callback-Funktionen einer Applikation zu realisieren. Vielmehr gehören diese KAPITEL 3. GRUNDLAGEN 38 Abhängigkeiten zur Szene selber und müssen durch entsprechende Abbildungen beschrieben werden. Dazu gibt es eine Reihe spezieller engines, die in Snyders Terminologie in etwa Operatoren entsprechen. So kann man Vektoren und Matrizen in einzelne Komponenten zerlegen und wieder zusammensetzen, es gibt Zähler, engines zur Interpolation zwischen Zahlen, Vektoren und Rotationen, und es gibt ein SoCalculator-Objekt zur Berechnung arithmetischer Ausdrücke. Ein calculator besitzt je acht feste Eingabevariablen vom Typ Float, a-h, und vom Typ Vec3f, A-H. Um einen calculator zu spezifizieren, der zwei Zahlen a, f und zwei Vektoren A, B erhält, a von Grad nach Radian konvertiert und , f (A B) berechnet, benutzt man folgenden Code: SoCalculator* calc=new SoCalculator; calc->expression.setValue(0,"oa = a * M_PI / 180"); calc->expression.setValue(1,"oA = -f * cross(A,B)"); Es gibt dabei feste Ausgabevariablen oa-od und oA-oD, das Ergebnis einer Rechnung kann als Eingabe für das Datenfeld eines beliebigen Inventor-Knoten verwendet werden, wenn der Typ übereinstimmt. SoRotationXYZ* rot=new SoRotationXYZ; rot->axis.connectFrom(&calc->oA); rot->angle.connectFrom(&calc->oa); Die Liste der Knoten, die innerhalb einer Gruppe auf rot folgen, werden also einer Rotation unterzogen, die aus anderen Größen der Szene berechnet wird, zum Beispiel Richtungsvektoren von Translationen, Höhe und Breite eines Würfels und so weiter. – Andere engines sind in der Lage, einen relationalen Ausdruck auszuwerten und Wahr oder Falsch zurückzugeben, damit läßt sich ein Ereignis triggern oder eine Bedingung überprüfen, beispielsweise eine Kollision von Gegenständen erkennen. Modellierung ist auf diese Weise allerdings bloß sehr eingeschränkt möglich, da engines nur Eigenschaftsfelder von Inventor-Knoten verknüpfen können. Eine Eigenschaft von NURBSKurven ist aber point, eine Funktion, die ein C-Array von 3D-Koordinaten übergeben bekommt. Einzelne Kontrollpunkte lassen sich also nicht über engines steuern; im wesentlichen werden sie daher bei Transformationen eingesetzt, etwa um Objekte aus Teilen zusammenszusetzen, deren Bewegung wie bei Gelenken bestimmte Freiheitsgrade besitzt. Engines sind daher zwar durchaus ein interessantes Konzept, ihre Verwendung innerhalb von Inventor unterliegt aber Beschränkungen, die in ihrem Verwendungszweck als Steuerungsobjekte für Animationen und zum Prüfen von Bedingungen begründet liegen. 3.4 GeomView Geomview ist ein interaktives Programm zur Betrachtung und Manipulation von geometrischen Objekten in Form von BReps, entstanden am Geometry Center der Universität von Minnesota in Minneapolis. Es bietet insbesondere die Möglichkeit, mit passenden Projektionsverfahren nicht nur Objekte im euklidischen, sondern auch im sphärischen und hyperbolischen Raum anzuzeigen, sowie eine oder mehrere Projektionen mehrdimensionaler Objekte, zum Beispiel des vierdimensionalen Hyperkubus. Neben der Dimensionalität und dem Projektionsverfahren kann man auch das Aussehen von Objekten definieren, einige Materialkonstanten setzen, die Objekte als wireframe, per hidden-line-removal oder Gouraud-schattiert anzeigen lassen, man kann Lichter setzen und mehrere Ansichten (Kameras) mehrerer Szenen parallel in verschiedenen Fenstern interaktiv manipulieren. 3.4. GEOMVIEW 39 Geomview arbeitet jedoch nicht allein als interaktives Betrachtungsprogramm, das Objektbeschreibungen aus Dateien einliest, sondern es gestattet anderen Programmen auch den Zugriff auf Darstellung und Objekte. Dazu werden separate Anwendungsprogramme, Module, aus Geomview heraus gestartet und ihre Standardein- und ausgabe auf Geomview umgelenkt. Diese Programme können nun Kommandos der Geomview-Befehlssprache GCL (geomview command language) auf den Standardausgabekanal schreiben, das eigentliche Geomview-Programm verändert die Szene entsprechend und kann Benutzerinteraktionen synchron oder asynchron an das Modul zurücksenden. Das Modul kann dazu Interesse an bestimmten Geomview-Ereignissen äußern, etwa ’pick-events’, bei denen der Benutzer eine Seitenfläche eines BReps durch Mausklick anwählt. Geomview schickt diese Ereignisse in Form von GCL-Kommandos an die Standardeingabe des Anwendungsprogramms, das nicht nur durch Zurückschicken veränderter Geometrien reagieren kann, sondern durch GCL-Kommandos Zugriff auf viele der inneren Einstellungen des Host-Programmes besitzt. Die Kommandos fallen in die folgenden Kategorien: Starten von Modulen, Kommunikation mit anderen Modulen, Lesen und Schreiben von Geometrien, Kommunikation mit dem Betriebssystem und der Shell Sämtliche Einstellungen der Darstellung, verwendete Räume und Projektionsverfahren, Materialien, Transformationen Ereignisverwaltung GCL selbst ist eine einfache, an LISP angelehnte Programmiersprache mit Funktionsaufrufen der Form (fct a b c), mit Verzweigungen, doch ohne eigene Variablen und Möglichkeiten zur Definition von Funktionen. Einige GCL-Kommandos verlangen komplexe Argumente als Eingabe, geometrische Objekte in verschiedenen Formaten. Für diese OOGL-Objekte (object oriented graphics library) gibt es dann jeweils eine spezielle Syntax, die auch gleichzeitig als Dateiformat dient. Geomview kennt OOGL-Formate für folgende Objekte: Vierecksliste, Streckenzug, reguläres Vierecksnetz, Bezierfläche, BRep, Kugel Transformierte Objektinstanz, Listen (Subszene), transformierte Subszene Transformation, Kamera, Fenster Es existiert eine ganze Reihe von Modulen, Demonstrationen wie Rotationen im Vierdimensionalen vorzustellen sind, wie der hyperbolische Raum durch ein rechtwinkliges Dodekaeder aufgeteilt (tesseliert) werden kann, oder die Visualisierung einer einzugebenden komplexen Funktion f : C ! C . Eine Visualisierung zeigt ein Polygon im hyperbolischen Raum, läßt eine Kante selektieren und rotiert eine Kopie des Objektes um diese Kante, so kann man durch “Entfalten” modellieren. Daneben gibt es verschiedene Module, die aufgerufen werden können, wenn bereits eine Geometrie besteht, die man also als Werkzeuge auffassen kann. Es gibt Module zum Auftrennen von BReps in mehrere Schichten, zum Bemalen von Objekten im Dreidimensionalen, zur Stereosicht in zwei Fenstern, und ein Modul, das zum Aufzeichnen von Animationen dient. Anwendungsprogramme haben natürlich die Möglichkeit, zusätzliche Fenster zu öffnen, in denen die anwendungsrelevanten Parameter vom Benutzer verändert werden können. Geomview kann von einem Anwendungsprogramm somit als geometry engine benutzt werden, zum Beispiel zum Modellieren: Ein Modeller schickt anfänglich eine Szenenbeschreibung an Geomview, öffnet eine Werkzeugleiste, schickt dann bei Werkzeuganwendung ein Gizmo in die Szene, bearbeitet Picking und Dragging-Ereignisse für Handles der 3D-Sicht, und so entsteht iterativ ein Objekt, eine Szene. In gewisser Weise kommt diese Trennung auch der Konzeption der Generativen Modellierung entgegen, wie Snyder sie mit GENMOD vorgeschlagen hat. Man kann die Ausdruckskraft KAPITEL 3. GRUNDLAGEN 40 eines Modellieransatzes nach der Mächtigkeit der Schnittstelle zwischen Benutzerinteraktion und geometry engine beurteilen. Geomviews GCL ist eine Sprache, die für einfachste Steueraufgaben entworfen worden ist, entsprechend enthalten Dateien sämtlicher OOGL-Formate auch nur statische, approximative Beschreibungen von Objekten, seien sie drei- oder vierdimensional, mögen sie im euklidischen oder hyperbolischen Raum leben. Entscheidend ist jedoch, daß hier der Weg beschritten wurde, die Verwaltung der Ansicht einem zentralen Modul zu übergeben, mit dem über eine gegebene Sprache kommuniziert werden kann, die auch als Dateiformat funktioniert. Stellt man sich eine echte geometrische Programmiersprache wie GENMOD als Sprache für eine solche Schnittstelle vor, so reduziert sich der Entwurf eines Generativen Modellers auf ein Anwendungsprogramm, das mächtig genug ist, den Benutzer Programme eingeben zu lassen, und auf eine geometry engine, die in der Lage ist, diese zu interpretieren und die konstruierten Modelle darzustellen, sowie Interaktion mit der Darstellung an die Anwendung weiterzuleiten. 3.5 TBag Das TBag-System [ESYAE95] ist ein Softwarepaket, das es gestattet, interaktive 3D-Anwendungen in C++ zu schreiben. Das API von TBag, der ’geometry layer’, realisiert dabei eine funktionale Programmierschnittstelle, deren Ziel es ist, die Definition einer Geometrie einerseits extrem einfach und verständlich zu machen, andererseits eine Reihe von Optimierungen vornehmen zu können, um dem darunterliegenden TBag die Anwendung effizienter Algorithmen zu ermöglichen. In TBag wird unterschieden zwischen Anwendungsprogrammierern, die den geometry layer benutzen, und Programmierern, die das System erweitern möchten. Diesen wird die zugrundeliegende Klassenstruktur offenbart, um Vererbungsmechanismen benutzen zu können, Anwendungsprogrammierer benutzen lediglich die Basisklassen, durch überladene Operatoren ist eine sehr knappe Schreibweise möglich. Darüberhinaus wird dem Anwender die Konstruktion neuer Objekte durch Wrapper-Funktionen abgenommen, sodaß der Code alleine aus Funktionsaufrufen und Zuweisung an Referenz-Variablen besteht. Einige Basisobjekte sind bereits instantiiert, sodaß Code der folgenden Form möglich ist: extern Geometry& cube; extern Geometry& sphere; extern Color& yellow; Geometry& block_with_ball(Transform& xform, Color& color) { return ( cube*scale(1,4,1)*color + sphere*xlt(0,2,0)*yellow) ) * xform; } In dieser Funktion werden die überladenen Operatoren * und + verwendet, um einem Objekt vom Typ Geometry ein Attribut zu verleihen oder eine Transformation anzuwenden, und um zwei Szenen zu einer Gesamtszene zu verbinden, wobei auch ein einzelnes Objekt bereits eine Szene ist. – Weiter verfügt das TBag-System über einige Algorithmen zum ’constraint solving’, die es ermöglichen, einerseits ’constrainables’ in Template-Schreibweise zu definieren, andererseits durch ’assertions’ vorzuschreiben, daß bestimmte constraints stets einzuhalten sind. Constrainable<Transform&> trans1, trans2; assert( trans1*trans2 == identity_trans ); Constrainable<Geometry&> geo = block_with_ball(trans1,red)+ block_with_ball(trans2,blue) ; assert( trans1 == rot(y_axis,Time)*xlt(sin(Time),0,0) ); 3.6. ACTIVEVRML 41 Damit werden zwei Transformationen definiert, wobei die eine immer die Inverse der anderen sein soll, eine Geometrie aus transformierten gelben Bällen mit Quadern, einer rot und einer blau, und schließlich soll die eine Transformation stets eine Komposition einer Rotation und einer Translation sein, beide zeitabhängig. Diese prägnante Schreibweise ist jedoch nur möglich, weil Attribuierung und Subszenenbau nicht auf die extern deklarierten Objekte cube und sphere selber wirken, sondern auf Kopien, die von den Operatoren vor Anwendung der Attribute angelegt werden. Daher führt der geometry layer von TBag die Beschränkung der Wertkonstanz ein, einmal konstruierte Objekte werden nicht mehr verändert. Einerseits bedeutet diese Restriktion, daß bei naiver Implementation erhebliche Kosten dadurch entstehen, daß stets bei jedem Ausdruck neue Objekte angelegt werden, andererseits ermöglicht es dem darunterliegenden TBag-System, constraints in effizienter Weise zu lösen, weil es an den Ausdrücken Optimierungen vornehmen kann, die nicht möglich wären, wenn sich Teile der Ausdrücke möglicherweise nachher ändern würden. Man hat daher Seiteneffekte ausgeschlossen, muß allerdings mehr Aufwand bei der Speicherverwaltung in Kauf nehmen. Betrachtet man die zugrundeliegende Klassenhierarchie von Geometrien, Transformationen und Attributen sowie Hilfsklassen wie Farben, Punkten und Vektoren, so ist auffällig, daß die Funktionalität nicht über abstrakte Basisklassen und virtuelle Funktionen realisiert wird. Dies ist aber konsistent mit der Sichtweise von Objekten als konstanten Werten, die nicht mehr verändert werden. Zudem wurde es von den Entwicklern von TBag als nicht zumutbar erachtet, Basisklassen später um weitere Methoden zu erweitern, wenn alle Objekte einer Hierarchie eine zusätzliche Fähigkeit bekommen sollen. Stattdessen wurde daher ein Mechanismus der Programmiersprache CLOS verwandt, das multiple dispatching. Dabei werden Methodentabellen konstruiert, die für jede Funktionalität vorsehen, wie welche Klasse auf eine Anfrage einer bestimmten Signatur zu reagieren hat. Macht man eine Memberfunktion einer Klasse zu einer externen Funktion, so erhält diese ein zusätzliches Argument, die Instanz der Klasse. Daher lassen sich Memberfunktionen wie normale Funktionen in eine Tabelle einfügen. Das Zufügen einer neuen Funktionalität zu einer Klassenhierarchie beschränkt sich damit auf das Anlegen einer neuen Tabelle. Damit wird es möglich, etwa bei Einführung einer neuen Grafikbibliothek sämtliche bestehenden Objekte an die neuen Rendering-Fähigkeiten anzupassen, ohne die Klassen selber zu berühren. 3.6 ActiveVRML Ein gravierender Nachteil von TBag ist, daß Anwendungsprogrammierer gezwungen sind, ihre Szenen durch einen Präprozessor bearbeiten zu lassen, einen C++-Compiler zu starten, zu linken, um schließlich festzustellen, daß einige Objekte die falsche Größe besitzen. Es besteht daher nach wie vor die Notwendigkeit, eine geometrische Programmiersprache zu finden, die die Ausdruckskraft von TBag mit einem interpretierten Ansatz verbindet. Ein interessanter Vorschlag für die Nachfolgeversion 2.0 des erwähnten VRML 1.0 wurde von C. Elliott, einem der Entwickler von TBag, für die Firma Microsoft eingereicht. Dort hat man von einer Abwärtskompatibilität zu VRML 1.0 abgesehen und stattdessen eine funktional orientierte Programmiersprache als Beschreibungssprache vorgeschlagen, ActiveVRML [Ell96]. Dies geschah allerdings weniger im Hinblick auf geometrische Modellierung als vielmehr, weil man auf diese Weise wie in TBag Gegenständen sehr bequem Attribute verleihen kann und sich Szenengraphen mit Ereignissen kombinieren lassen. Die Syntax von ActiveVRML ist an StandardML angelehnt, eine funktionale Standardsprache und Vorläufer der in Kapitel vier vorgestellten Sprache Haskell. Dort werden auch die Konsequenzen des funktionalen Paradigmas für die Modellierung eingehend dargelegt. – Ein wesentliches Prinzip von ML wie auch von TBag ist auch KAPITEL 3. GRUNDLAGEN 42 hier verwirklicht, die Wertkonstanz: Objekte können konstruiert, aber nicht verändert werden. Ein Beispiel sind zwei hüpfende Bälle: ball = readGeometry("sphere.wrl"); ballHeight = (9-(time-3)^2) until bounceEvent => ballHeight; motion(y) = translate(time,y,ballHeight); bounceEvent = predicate(ballHeight < 0); newBall(y) = transformGeometry(motion(y),ball); ball1 = newBall(1.4); ball2 = newBall(2.2); pairOfBalls = beachBall union superBall; Im Prädikat ballHeight wird ein Verhalten definiert, ein oder mehrere Folgepfeile stehen für die Reaktion auf ein eintretendes Ereignis. Es startet dabei mit einer eigenen Zeit time, die bei Konstruktion auf 0.0 gesetzt wird; kommt es zu einem bounceEvent, so wird das alte ballHeigt durch ein neues mit neuer Zeit ersetzt. Die Bewegung motion wird davon jedoch nicht beeinflußt, ihre Zeit läuft weiter, der Ball hüpft also in x-Richtung ins Unendliche. Bemerkenswert ist die Parametrisierbarkeit der Konstruktion des hüpfenden Balles, daher lassen sich Instanzen durch Funktionsaufrufe konstruieren, schließlich entsteht eine zusammengesetzte Geometrie. Bei der Konzeption von ActiveVRML standen Animationsaspekte und Benutzerinteraktion klar im Vordergrund. Es gibt folgende Kategorien von Objekten: Grundtypen: Zahlen, Boole’sche Werte, Funktionsdefinition, Paare, Listen Modelliertypen: Farbe, 2D- und 3D-Vektoren, Bitmaps, Einlesen von Geometrien, Lichtquellen, Tonquellen, Kameras, Mikrophone Interaktion: Ereignisse, Verhalten, Picking In einer detaillierten Besprechung wird auf einer – mittlerweile gelöschten – WWW-Seite von Microsoft ein Vergleich gezogen zwischen ActiveVRML und dem zwischenzeitlich verabschiedeten VRML 2.0-Standard, der auf eine Erweiterung von VRML 1.0 hinausläuft. Dabei werden ActiveVRML-Beispielen solche in einer prozeduralen Sprache gegenübergestellt, eine Programmierweise ähnlich wie bei OpenInventor wird dort ’scene graph style’ genannt. – Als Vorteile von ActiveVRML werden in dieser Besprechung folgende Punkte geltend gemacht: Das Sprachkonzept ist rein funktional-deklarativ und damit konsequenter. Konstante Ausdrücke lassen sich besser optimieren. Aufbau statischer Szenen mit prozeduralen Programmen ist das falsche Konzept. Verhalten und Ereignisse lassen die gleiche Interaktivität wie Ereignisschleifen zu. Die Möglichkeit, geometrische Modellierung in ActiveVRML selber auszuführen, ist bedauerlicherweise nicht als Vorteil genannt worden, obwohl die Beispiele in Kapitel sechs auch ähnlich in ActiveVRML zu realisieren gewesen wären. Leider wird auf die formalen Eigenschaften von ActiveVRML in der Dokumentation überhaupt nicht eingegangen, es ist nicht klar, wie das verwendete Typsystem aussieht, ob Haskellähnliche Klassen existieren, geschweige, daß eine Grammatik der Sprache angegeben würde. Es gibt auch keine Hinweise auf eine Referenzmplementation, insbesondere werden Effizienzaspekte nicht besprochen. – Zudem erfordert die Organisation der Benutzerinteraktion in funktionalen Sprachen generell ein Umdenken im Vergleich zur Ereignis-Schleife prozeduraler Programme, man bemüht dort Konzepte wie Monaden oder ’continuation based I/O’ [Hud89], wo Übergangsfunktionen benutzt werden. Die Weiterentwicklung dieses Ansatzes ist von Microsoft nach der Ablehnung von ActiveVRML als neuem Standard offenbar sofort eingestellt worden. Kapitel 4 Generative Modellierung Nach der Analyse von Modellieransätzen sollen nun in zweiten Teil der Arbeit zwei Möglichkeiten vorgestellt werden, Generative Modellierung zu implementieren. Es werden dabei die beiden Ansätze verfolgt, die in der Analyse hergeleitet worden sind. Zum einen soll innerhalb des objektorientierten Paradigmas der Ansatz der Metaobjekte verfolgt werden, andererseits wird eine Implementierung in einer rein funktionalen Sprache vorgestellt. Dabei wird im besonderen darauf eingegangen, welche Fähigkeiten funtionaler Sprachen für die Modellierung besonders von Wichtigkeit sind, und in welcher Richtung die bereits recht umfangreiche C++-Implementation erweitert werden muß, um ein vollwertiges Modelliersystem zu liefern. Bei diesen Implementationen geht es allerdings zunächst nicht darum, einen kompletten Modeller zu bauen. Eine notwendige Vorbedingung dazu ist vielmehr, ein Konzept für die innere Organisation eines Generativen Modellers zu entwickeln, ein Modell, das es erlaubt, die geforderte Flexibilität im der Konstruktion von Gegenständen tatsächlich zu realisieren. Es wird in diesem Fall also der umgekehrte Weg beschritten: Statt einen interaktiven Modeller zu bauen und diesem mit einem Dateiformat zu versehen, um Modelle zu speichern und abzurufen, soll hier zuerst zum einen der innere Aufbau und zum anderen die passende Form für eine geometrische Programmiersprache definiert werden, um einem interaktive Programm gezielt die notwendigen Fähigkeiten verleihen zu können. Der vielversprechendste Ansatz für einen generativen Modeller ist eine hybride Darstellung von 3D-Modell und generiertem Programmtext, denn die Haskell-Beispiele in 6.2 geben einen Eindruck von den Einflußmöglichkeiten, die ein Anwender notwendigerweise haben muß. Neben den Zielen Änderbarkeit und Wiederverwendbarkeit von konstruierten Objekten wird dabei ebenso die Erweiterbarkeit des Modells auf programmierspachlicher Ebene im objektorientierten Sinn verfolgt. Beide vorzustellenden Konzeptionen bewegen sich dabei auf unterschiedlichen Ebenen und sollen getrennt voneinander behandelt werden, auch wenn die gleiche Idee zugrundeliegt. Die im Abschnitt 2.3 gegebene Definition von Oberflächen als Patchkomplexe ist jedoch in beiden Modellen nicht realisiert, obwohl viele der Beispielkonstruktionen in Kapitel 6 zusammenhängende Flächenkomplexe zum Ergebnis haben. Zunächst einmal ist jedoch nicht klar, wie die Idee des Modellierens mit Abbildungen, die natürlicherweise im Raum der Freiformflächen lebt, auf Objekte mit einer zugrundeliegenden diskreten Struktur wie Patchkomplexe übertragen werden kann. Daher geht es an dieser Stelle zunächst darum, wie Kurven und Flächen durch die Kombination elementarer Operatoren realisiert werden können. — Patchkomplexe sind jedoch die Richtschnur, an der man sich auch in diesem Bereich langfristig zu orientieren hat. 43 KAPITEL 4. GENERATIVE MODELLIERUNG 44 4.1 Generative Modellierung in C++ Grundlage für die C++-Implementation ist eine Einbindung in die MRT-Plattform. Wie bereits erläutert, dient der MRT im wesentlichen zum Rendern von statischen Szenen, die über Skriptdateien eingelesen werden. Eine erste Erweiterung in Richtung Modellierung wäre das Einlesen von Objektbeschreibungsdateien, die zum Beispiel das Resultat eines Modellierprozesses enthalten. Das ist so vorzustellen, als würde man die Aktionen des Benutzers bei der Modellierung mit einem interaktiven Modeller mitschneiden und diese Aktionen in einer Skriptdatei speichern, die von einem Renderer wie dem MRT wieder eingelesen werden kann, um die modellierten Objekte darzustellen. Dieser Ansatz würde im wesentlichen der Herangehensweise von Snyder entsprechen. Es bliebe dabei unbenommen, dies später zu einem interaktiven Modeller auszubauen, indem die Modellieraktionen nicht aus einer Skriptdatei gelesen werden, sondern Benutzerinteraktionen zu einem Modell verarbeitet. Für die gemeinsame Schnittstelle von Skriptdatei und Interaktion zum Rendering-Modul kann man sich einen Mechanismus vorstellen, der ähnlich ist wie der zum Einlesen von msd-Dateien verwendete. Man hätte also als Rückgabewert des Parsens eine Reihe modellierter Objekte in einem Szenengraphen, ebenso wie bei der Schnittstelle von interaktiver Modellierung und Rendering. Umgekehrt gilt, daß man beide Möglichkeiten der Eingabe von modellierten Objekten, Parser und Benutzerinteraktion, nachträglich anfügen kann, wenn erst einmal die Schnittstelle in Form einer Klassenhierarchie für die Generative Modellierung spezifiziert ist. Die Ausdruckskraft eines Modellierparadigmas wird aber vornehmlich von der internen Organisation eines Modellers bestimmt und erst in zweiter Linie von den Eingabemöglichkeiten, denn jedes Modell muß diese Schnittstelle zum Rendering passieren. Es geht bei einer Implementierung der Generativen Modellierung also vordringlich darum, eine entsprechende Klassenstruktur zu entwerfen. Was man zusätzlich noch erreichen möchte, ist die Erweiterbarkeit einer solchen Klassenstruktur. Es soll also nicht jeweils ein fester Typ für Zahlen, Vektoren, Kurven und Flächen vorgegeben werden, sondern die Objekte, mit denen generativ modelliert wird, sollen austausch- und erweiterbar sein. Das bedeutet, daß zunächst ein ganz abstrakter Kern von Klassen implementiert wird, auf dem der Rest des Paketes beruht, und außerhalb definierte Klassen durch sogenannte WrapperKlassen in das System aufgenommen werden können. 4.1.1 Abbildungen als Objekte In Verallgemeinerung der Modifier, die bei 3D-Studio Max in einem Stack gehalten werden, wird beim Genmod-Paket, der Erweiterung des MRT zum Generativen Modellieren innerhalb des bestehenden objektorientierten Paradigmas – wie in 2.5 auf Seite 23 ausgeführt –, mit Abbildungsobjekten gearbeitet. Grundlage von Genmod bilden zwei Basisklassen für Metaobjekte, zum einen die Basisklasse t_GmMAP für Abbildungsobjekte oder Maps (für englisch “Abbildung”), zum anderen für abzubildende Objekte die Variablen-Basisklasse t_GmVar. Die Ableitungen von t_GmVar heißen Variablenklassen, das sind die eigentlichen Objekte, mit denen modelliert wird, zum Beispiel also Zahlen und Vektoren. Daneben gibt es eine Kurven- und Flächenbibliothek, das cns-Paket (für ’curves and surfaces’), das als Beispiel einer Genmod-externen Klassenhierarchie durch Wrapper in das Genmod-Paket integriert wurde. Dieses Paket sowie die Realisierung der Einbindung wird im Kapitel 5.1 über die Einzelheiten der C++-Implementation im einzelnen vorgestellt. Hat man nun beispielsweise eine Real-Klasse als Ableitung der Variablenbasisklasse und die arithmetischen Grundoperationen durch Ableitungen der Abbildungs-Basisklasse realisiert, so 4.1. GENERATIVE MODELLIERUNG IN C++ 45 wird der Ausdruck (3 + 4) + 5 durch fünf Objekte repräsentiert, drei Konstanten, modelliert als konstante Abbildungen, die den Rückgabetyp Real haben, sowie zwei Addier-Abbildungen, die als Eingabe zwei und als Ausgabe einen Real-Wert besitzen. Um die Typsicherheit zu gewährleisten, hat jede Abbildung eine Signatur, das ist eine Liste von Typ-Identifiern, die angeben, von welchem Typ die Werte sind, die von der Abbildung verarbeitet werden, und was zurückgegeben wird. Jedes Abbildungsobjekt legt lokal eine Rückgabevariable an, in der der Wert nach seiner Berechnung gespeichert wird. Ihrer Signatur entsprechend erhält eine Abbildung ihre Eingaben von Rückgabewerten anderer Abbildungen, enthält also Zeiger auf Eingabeabbildungen. Damit kann man eine Kette von Abbildungen aufbauen, wo die erste Abbildung einen Eingabewert verarbeitet, aus diesem einen neuen Wert berechnet, dieser ist die Eingabe der nächsten Abbildung, bis schließlich an der letzten Abbildung der Wert der Berechnungskette ausgelesen werden kann. Am Anfang der Kette stehen dabei konstante Abbildungen, die selber keine Eingaben brauchen und einen festen Rückgabewert haben. – Haben einzelne Abbildungen dabei mehr als eine Eingabe, so wird aus dieser Kette ein Berechnungsbaum, wie in dem Beispiel (3+4)+5. Zusätzlich soll gestattet sein, einzelne Zwischenergebnisse dabei mehrfach zu verwenden, sodaß bei (3+4)+5+2*(3+4) der Ausdruck (3+4) nur einmal ausgewertet werden muß. Auf diese Weise wird aus dem Berechnungsbaum ein gerichteter azyklischer Graph, ein DAG (directed acyclic graph). Um eine solche verkettete Abbildung auszuführen, wird von der zuletzt auszuführenden Abbildung eine Auswertungskaskade in Gang gesetzt, weil zuerst die Eingabedaten dieser Abbildung berechnet werden müssen, indem die Eingabeabbildungen ausgewertet werden. Auf diese Weise wird der Auswertungsprozeß also durch den gesamten DAG weiterpropagiert, und die Ergebnisse werden in der umgekehrten Richtung durchgereicht. Const_0 Op_0 Const_1 Op_1 Const_2 Zu diesem Zweck bietet die Basisklasse t_GmMAP zwei Funktionen an, die am Auswertungsprozeß beteiligt sind, eine public-Funktion execute() und eine rein virtuelle protected-Funktion evaluate(), die von jeder abgeleiteten Klasse überschrieben werden muß. Um eine gegebene gebundene Map map auszuführen, kann ein Benutzer map.execute() aufrufen. Diese Funktion ruft ihrerseits ein execute() für sämtliche Eingabeabbildungen auf und danach die eigene Memberfunktion evaluate(), die damit alle Rückgabewerte der ausgewerteten Eingabeabbildungen benutzen kann. Das bedeutet, execute()-Nachrichten werden vom Ende zum Anfang des DAGs gereicht, dann werden die Ergebnisse von evaluate() vom Anfang zum Ende hin verarbeitet. Ein DAG entspricht aber einer partiellen Ordnung. Wählt man eine totale Ordnung, die mit dieser partiellen Ordnung verträglich ist, so erhält man eine Sequenz von Abbildungen, die nur einmal nacheinander ausgeführt werden müssen, und es ist sichergestellt, daß die Eingaben jeder einzelnen Abbildung bereits berechnet sind. Das hat den Vorteil, daß Abbildungen nicht mehrfach ausgewertet werden wie Const_1 im Beispiel. Const_2 Const_1 Const_0 Op_0 Op_1 KAPITEL 4. GENERATIVE MODELLIERUNG 46 Um Abbildungen in einem Programm zu verwenden, muß nun ein Interface zwischen normalen C++-Variablen oder -Konstanten des Programms und den Ein- und Ausgabevariablen der Abbildung geschaffen werden. Dies wird im Genmod-Paket über konstante Abbildungen erreicht, die bei Konstruktion einen Zeiger auf einen C++-Wert oder eine C++-Konstante erhalten, die in ihre Rückgabevariable übertragen wird. Gleichzeitig bietet jede Abbildung eine Möglichkeit, den augenblicklichen Wert der Rückgabevariablen zu erfragen, sodaß man das Endergebnis der Berechnung an der letzten Abbildung auslesen kann. Diese zusammengesetzte Abbildung kann nun jedesmal neu ausgeführt werden, wenn sich die variablen C++-Eingabewerte ändern. Entscheidend für die Flexibilität dieses Ansatzes ist nun die Tatsache, daß Abbildungsobjekte zur Laufzeit des Programmes angelegt und miteinander verbunden werden können. So erreicht man ein ähnliches Verhalten wie bei einem Interpreter, wo Programmfragmente interaktiv eingegeben und überprüft werden können. Der Zweck dieses Generativen Modells ist aber die ’Berechnung’ von geometrischen Objekten, nicht die Ausführung eines allgemeinen Algorithmus’. Programme, die ohne Schleifen und if-Abfragen auskommen, nennt man auch ’straight-line-programs’, dies ist gerade die Klasse der hiermit realisierten Programme. Ähnlich wie in interaktiven Modellern werden Operationen damit entweder ausgeführt, oder sie werden nicht ausgeführt, ohne daß zunächst Verzweigungen notwendig wären. Ein straight-line-program, ein linearisierter DAG von Abbildungen, kann auch einzelne Abbildungen enthalten, die noch nicht vollständig gebunden sind, wo also Parameter noch nicht mit Eingabeabbildungen verbunden wurden. Eine solche Kette teilgebundener Abbildungen kann dann als eigenständiges Abbildungsobjekt behandelt werden, als ein Makro oder eine Gruppe von Abbildungen, die als solche Eingaben erhält, die an die ungebundenen Parameter weitergegeben werden. Die Signatur der zusammengesetzten Abbildung setzt sich damit aus Verweisen auf die ungebundenen Variablen der Einzelabbildungen zusammen. Op_0 Const_1 Op_1 Eine zusammengesetzte Abbildung dieser Art wird damit als Einheit behandelt, die in anderen Abbildungen verwendet werden kann, und die mittels ’deep copy’ vervielfältigbar ist. Es handelt sich unter anderem also um eine Kopiervorlage, ein Objekt im Speicher, das als Teil einer speicherinternen Werkzeugbibliothek aufbewahrt werden kann, um es kopieren und flexibel verwenden zu können. Hat man eine C++-Bibliothek von Modellierobjekten, Zahlen, Vektoren usw., so kann man diese in Variablen- und Abbildungsklassen verpacken, um damit generativ zu modellieren. Entscheidend ist dabei, daß eine solche Einbindung in Genmod flexibel und einfach bewerkstelligt werden kann. Der Genmod-Kern enthält daher neben der Variablen- und der Abbildungsbasisklasse und der zusammengesetzten Abbildung noch eine Variablen-Templateklasse, die einfach eine Instanz des übergebenen Typs enthält. Sind Variablenklassen durch Template-Instanziierung definiert, so kann man Abbildungen definieren. Dazu gibt es noch zwei Templates für konstante Abbildungen, die zusammen mit der Einführung eines Variablentyps instantiiert werden. Zum einen ist dies eine Abbildung, die bei Konstruktion einen Verweis auf eine Programmvariable erhält, aus der der Wert jeweils ausgelesen wird, und zum anderen eine Abbildung, die eine Konstante vom Template-Parametertyp enthält, die, einmal gesetzt, den Wert nicht mehr ändert. Der Kern des Genmod-Paketes besteht somit aus einer sehr überschaubaren Menge von Klassen und Templates, deren Funktionsweise, bis auf die Teile, die für den Anwender notwendig sind, 4.1. GENERATIVE MODELLIERUNG IN C++ 47 nach außen hin verborgen bleiben kann. Zudem ist die Abbildungs-Basisklasse so gemacht, daß eine Ableitung von ihr, die eine spezielle Abbildung implementiert, für den Benutzer sehr einfach zu schreiben ist, weil die gesamte Verwaltung von Signaturen und Rückgabewerten in Basisklassenfunktionen geschieht, sodaß ein Anwendungsprogrammierer nicht mit Verwaltungsaufgaben belastet wird. Vielmehr hat die Klasse t_GmMAP im wesentlichen zwei virtuelle protected-Funktionen, die vom Anwender überschrieben werden müssen. Das ist einmal fillSignature() zum Setzen der Signatur der Abbildung, damit das Binden typsicher abläuft und der Variablentyp einer Rückgabevariablen mit dem erwarteteten Typ übereinstimmt, der in der Signatur vereinbart wurde. Zudem wird dabei sichergestellt, daß keine un- oder teilgebundenen Abbildungen ausgewertet werden können. Zum anderen wird evaluate() überschrieben, die Funktion, in der die eigentliche Arbeit geleistet wird. Um zum Beispiel in Genmod das Kreuzprodukt zweier Vektoren bilden zu können, das durch den überladenen Multiplikationsoperator für Objekte vom Typ t_3DVector gegeben ist, definiert man eine Abbildungsklasse nach folgendem Schema: class t_GmMapPt3DCross : public t_GmMAP { ... protected: virtual t_Bool evaluate () ; virtual t_Bool fillSignature () ; private: t_3DVector* a; t_3DVector* b; t_3DVector* c; }; t_Bool t_GmMapPt3DCross::fillSignature () { signature(t_GmVar3DVectorPtr(a))(t_GmVar3DVectorPtr(b)) (t_GmVar3DVectorPtr(c)); return True; } t_Bool t_GmMapPt3DCross::evaluate() { (*a)=(*b)*(*c); return True; } Der Benutzer muß sich dabei nicht um das richtige Setzen der Zeiger kümmern, weil dies beim Binding durch die Basisklasse erledigt wird. – Diese Vorgehensweise funktioniert für alle Memberfunktionen von Variablenklassen in Genmod, die gewisse Voraussetzungen erfüllen: Jede Memberfunktion einer Variablenklasse, die – explizit oder implizit – nur einen Rückgabewert hat, kann in einer Genmod-Abbildung gekapselt werden. Für straight-line-programs solcher Funktionen läßt sich eine Normalform finden, in der C++Statements und Funktionsaufrufe dargestellt werden können. Dazu werden sämtliche Ausdrücke aufgelöst und explizit Variablen angelegt. KAPITEL 4. GENERATIVE MODELLIERUNG 48 So wird aus diesem Programmfragment: Class1 a; Class4 d=some_function(a,Class2(),a.member_function(Class2()); durch Normalisierung das folgende Programmfragment: Class1 Class2 Class3 Class4 a; b; c=a.member_function(b); d=some_function(a,b,c); Das bedeutet, C++-Programme, die nur aus Konstruktoren und Aufrufen von Funktionen bestehen, die keine destruktiven Updates von Objekten durchführen, lassen sich auf diese Weise linearisieren. Im Unterschied zu destruktiven gibt es auch nicht-destruktive Updates, die keine Information ändern und damit löschen, sondern Information hinzufügen, beispielsweise das Anhängen an eine Liste in LISP. — Für solche C++-Programme gilt dann, daß sie sich – falls die nötigen Wrapper eingebracht wurden – als zusammengesetzte Genmod-Abbildung, mithin in einem DAG darstellen lassen. Dabei werden in jeder Zeile neue Variablen eingeführt, und alle Statements, die danach folgen, dürfen auf diese Variable zugreifen, und diese Variable hat stets denselben Wert. Der Grund für das Verbot destruktiver Updates ist, daß Seiteneffekte ausgeschlossen werden sollen, die durch eine Verwendung derselben Variablen mit verschiedenen Werten eintreten können. Genmod-Abbildungen haben nur einen einzelnen Rückgabewert. Hat eine Memberfunktion, oder eine Funktion allgemein, mehrere Rückgabewerte, so kann nur einer davon von der Abbildung zurückgegeben werden. In Genmod gibt es nun aber keine Zeilennummern, keinen Hinweis darauf, in welcher Reihenfolge Funktionen abgearbeitet werden müssen, die Auswertungsreihenfolge wird allein durch die Propagierung der Evaluierung der Eingabewerte ausgehend von der Ergebnisabbildung eines DAGs bestimmt. Anders ausgedrückt, die Reihenfolge der Statements in einem Programm ist nur eine mögliche totale Ordnung, die zu dem gerichteten azyklischen Graphen der Aufrufabhängigkeiten paßt. Es ist in Genmod aber nicht definiert, welche totale Ordnung bei Linearisierung einer zusammengesetzten Abbildung gewählt wird. Mehrere Rückgabewerte besitzt beispielsweise eine Funktion, die als Parameter eine Referenz oder einen Zeiger auf ein weiteres Objekt erhält, das von der Funktion modifiziert wird. Insbesondere gilt dies für Memberfunktionen, die ihre Objektinstanz verändern, zum Beispiel setFunktionen, mit denen der innere Zustand eines Objektes verändert wird, die aber auch selber einen Wert zurückliefern, meist vom Typ Boolean. Memberfunktionen erhalten nämlich implizit einen weiteren Parameter, und zwar die Objektinstanz, zu der sie gehören. Solche Funktionen lassen sich nur schlecht in Genmod integrieren, denn damit das funktioniert, muß das veränderte Objekt in den Rückgabewert der Abbildung kopiert werden, und jede Veränderung erzeugt ein neues Objekt. Dies ist zu verschmerzen, wenn man beispielsweise 3D-Vektoren normalisieren möchte, der Rückgabewert der Abbildung ist eine normalisierte Kopie des Eingabevektors. Unpassend sind jedoch Funktionen, die ein destruktives Update von großen Datenstrukturen durchführen. Hat man eine Memberfunktion zum Verschieben einer Ecke eines Dreiecksnetzes, so wird daraus eine Abbildung, die ein Dreiecksnetz, eine Ecke des Netzes und einen Verschiebungsvektor erwartet und ein neues Dreiecksnetz zurückliefert; als Rückgabewert der Abbildung wird aber eine Kopie des gesamten Netzes mit einem verschobenen Knoten angelegt. 4.1. GENERATIVE MODELLIERUNG IN C++ 4.1.1.1 49 Integration von Kurven und Flächen Eine zusammengesetzte Abbildung mit der Signatur (Real,Real) ! Vector kann als parametrische Fläche aufgefaßt werden. Um das angestrebte Modellieren mit Abbildungen zu realisieren, ist daher ein spezielles Konzept von Kurven- und Flächenobjekten notwendig. Und zwar sollten die entsprechenden Klassen über eine parametrische Auswertungsfunktion verfügen, das bedeutet für Kurven, zu einem gegebenen t einen Punkt p auf der Kurve und für eine Fläche, zu (u; v) ein p auf der Fläche zu liefern. Das cns-Paket enthält daher eine Klassenhierarchie, die im wesentlichen auf drei Basisklassen aufbaut. Diese Klassen t_CurveParametric3D, t_SurfaceParam3D und t_Function3D haben virtuelle Memberfunktionen der folgenden Form: t_3DVector& t_CurveParametric3D::evaluate (t_Real t) t_3DVector& t_SurfaceParam3D ::evaluate (t_Real s, t_Real t) t_3DVector& t_Funct3D ::evaluate (const t_3DVector& p) Teil der Integration der Kurven- und Flächenbibliothek sind daher eine Kurven- und eine Flächenklasse, deren Konstruktoren Genmod-Abbildungen mit einer passenden Signatur erhalten, und deren überladene evaluate()-Funktionen nichts anderes machen, als ihre Abbildung auszuwerten. Auf diese Weise lassen sich also zusammengesetzte Abbildungen wiederum als Kurven und Flächen auffassen, aus denen dann wieder neue Abbildungen erzeugt werden können. Neben dieser Basisfähigkeit verfügen die Basisklassen über Funktionen, um differentialgeometrische Größen wie Tangente, Normale und Binormale einer Kurve, das Frenet’sche Dreibein, und die partiellen Ableitungen und die Normale einer Fläche an einer Stelle zu berechnen. Eine Abbildung zum Frenet’schen Dreibein findet man in Kapitel 5 auf Seite 66. Betrachtet man etwa die Memberfunktion zur Tangentenbestimmung der Kurvenklasse, so kann man diese in eine Abbildung verpacken, die als Eingabe eine Kurve und einen Parameter erhält und als Rückgabewert einen Vektor liefert. Auf diese Weise lassen sich Abbildungen für alle entsprechenden Memberfunktionen konstruieren, die dann der Werkzeugbibliothek als elementare Operatoren hinzugefügt werden. Die Verbindung zum MRT geschieht nun folgendermaßen. Die Kurven- und Flächenbibliothek ist durch Variablenobjekte in Genmod integriert, insbesondere sind die Kurven- und die Flächenbasisklasse gültige Genmod-Variablen. Nun gibt es im Genmod-Paket Abbildungen t_GmMapCurveMap und t_GmMapSurfaceMap, die eine zusammengesetzte Abbildung passender Signatur erwarten, und nicht nur deren Rückgabewert benutzen, sondern die gesamte Abbildung kopieren. Die kopierte Abbildung wird dann dem Konstruktor der abgeleiteten Kurvenklasse t_CurveMap bzw. der Flächenklasse t_SurfaceMap übergeben. Dort wird ein Rebinding der Eingabeparameter durchgeführt, sodaß die Memberfunktion evaluate auf die Auswertung der zusammengesetzten Abbildung umgeleitet wird. Diese Kurve oder Fläche ist dann der Rückgabewert der GenmodAbbildungen t_GmMapCurveMap bzw. t_GmMapSurfaceMap. Für parametrische Kurven- und Flächenobjekte gibt es wiederum – in Snyders Terminologie – eine Methode, die eine Umwandlung in ein anzeigbares Objekt vornimmt. Dies ist einmal eine von t_Object abgeleitet Klasse, deren Konstruktor eine parametrische Kurve bekommt und diese als einen Schlauch darstellt, denn Kurven haben als eindimensionale Punktmenge keine darstellbare Fläche. Parallel dazu gibt es eine Klasse, die eine parametrische Fläche erwartet und diese zum approximativen Rendering in ein anzeigbares BRep umwandelt, indem ein Sampling der Fläche auf einem regelmäßigen Gitter im Parameterraum durchgeführt wird. Zu jeder darzustellenden Genmod-Abbildung wird also ein parametrisches Objekt erzeugt, welches dann durch Sampling in ein MRT-Objekt umgewandelt wird, das in eine Szene eingebaut, angezeigt und interaktiv betrachtet und verfeinert werden kann. KAPITEL 4. GENERATIVE MODELLIERUNG 50 4.1.2 Vergleich des Genmod-Paketes mit GENMOD Das Zusammensetzen von Abbildungen geschieht bei Snyders GENMOD durch das Schreiben und Interpretieren von Metashapes, das sind Funktionen, die MAN-Objekte erhalten, neue aus ihnen erzeugen und ein MAN-Objekt zurückgeben. In ähnlicher Weise kann man Genmod benutzen, um C++-Funktionen schreiben, die aus Abbildungen neue Abbildungen machen. Allerdings ist man durch diese Arbeitsweise beim Modellieren auf die Metashapes angewiesen, die zur Compilezeit im System vorhanden sind, das bedeutet, ein interaktiver Interpreter ist nicht Teil des Genmod-Paketes. Es ist eher auf eine einfache Benutzungsweise und ein verständliches API für die Abbildungen selber Wert gelegt worden, um Anwendungsprogrammierern, die Metashapes entwerfen, die Arbeit zu erleichtern. Zudem sind Abbildungen auf diese Weise zu Objekten geworden, die zur Laufzeit erzeugt und verbunden werden können. Auf diese Weise ist erst die Voraussetzung geschaffen worden, die grundsätzliche Beschränkung auf festverdrahtete Werkzeuge eines Modellers einmal zu überschreiten. Skriptdateien und Interaktionen, die in welcher Weise auch immer Vorschriften zur Konstruktion von Abbildungen beschreiben, können nun zur Laufzeit eines Programmes ausgeführt werden und so parametrisierte dreidimensionale Modelle zusammensetzen, die interaktiv angezeigt werden. Ein grundsätzlicher Unterschied zwischen dem MAN-Datentyp von GENMOD und dem Abbildungskonzept von Genmod ist der, daß ein MAN stets eine Abbildung von Rn ! Rm ist, also zwischen reellen Vektorräumen. GENMOD besitzt als Programmiersprache aber auch die anderen Grunddatentypen wie Zahlen, Vektoren und Zeichenketten, die man bei der Programmierung verwenden kann. – In sofern ist das Genmod-Paket allgemeiner, weil die Art der verwendeten Datenstrukturen nur durch die Beschränkung auf Funktionen ohne destruktive Updates begrenzt ist. Andererseits beraubt man sich durch diese Allgemeinheit der Möglichkeit, Inklusionsfunktionen für sämtliche Abbildungen definieren zu können, diese rekursive Operatormethode von GENMOD fällt also weg. Das läßt sich zunächst damit rechtfertigen, daß das Thema von Gemod mehr die Modellierung an sich ist als Intervallarithmetik und das Lösen impliziter Gleichungssysteme, zu dem diese dient. Andererseits ist das Problem des Umganges mit impliziten Gleichungen durchaus ein Kernthema der Modellierung, verdient aber eine gesonderte Behandlung an anderer Stelle und wurde in Genmod daher ausgeklammert. Grundsätzlich besteht allerdings die Möglichkeit, rekursive Operatormethoden wie die Bestimmung der Ableitung oder der Inklusionsfunktion auch in Genmod durchzuführen. Dazu wären folgende beiden Punkte notwendig: Operatormethoden müssen Memberfunktionen sein Bei der Integration von Kurven und Flächen in Genmod wird die Operatormethode Auswertung für diese Objekte realisiert. Das gelingt dadurch, daß die cns-Klassen über evaluate()Memberfunktionen verfügen, um Parameter auszuwerten. – Um effizient eine Inklusionsfunktion für eine Bézierfläche zu finden, müßte die Bézierklasse eine entsprechende Memberfunktion zur Verfügung stellen, weil von außen kein Zugriff auf die internen Berechnungen der Klasse bei der Auswertung eines Flächenpunktes möglich ist: Nur die Bézierklasse selber kann bestimmen, wie weit ihre Fläche bei einem gegebenem Urbildintervall variiert. Jede Operatormethode muß für jeden elementaren Operator existieren Dieser Punkt ist von entscheidender Komplexität: Setzt man eine Fläche aus elementaren arithmetischen Ausdrücken zusammen und versucht dann, die Inklusionsfunktion dieser Fläche zu bestimmen, so muß dies rekursiv erfolgen. Das bedeutet, daß ähnlich wie in Snyders Implementation alle elementaren Operatoren um eine solche Methode ergänzt werden müssen. Ähnliches gilt für die Berechnung von Ableitungen als Operatormethode. 4.1. GENERATIVE MODELLIERUNG IN C++ 51 Der C++-Implementation von Genmod sind aber durch ihre Allgemeinheit Grenzen gesetzt, denn es gibt schwächere Voraussetzungen für Operatoren. Insbesondere sind beispielsweise BReps ein Variablentyp, der im Genmod-Paket enthalten ist, und für eine Abbildung, die eine Liste von Punkten in einen BRep verwandelt, macht die Berechnung einer Ableitung keinen Sinn. Um den Unterschied zwischen GENMOD und dem Genmod-Paket in bezug auf die Modellierung weiter zu charakterisieren, kann man feststellen, daß in Genmod die Abbildungsklassen implementiert sind, aus denen zusammengesetzte Abbildungen zusammengesetzt werden können. Was allerdings fehlt, ist ein Konzept für den Zusammenbau von Operatoren. Snyder benutzt dazu C-Funktionen, die Abbildungen erhalten und Abbildungen zurückgeben. Die zusammengesetzten Abbildungen selber sind der Inhalt des Genmod-Paketes, aber es gibt es keine Objekte, um vorzuschreiben, welche Abbildungen wie zusammengesetzt werden. In diesem Sinne sind Operatoren noch eine weitere Indirektion, die dazu dient, eine Vorschrift zu kodieren, wie eine Abbildungen zusammenzubauen ist. Inhalt des Genmod-Paketes ist ein flexibler Mechanismus, um Abbildungsobjekte zu definieren und zu Metashapes zusammenzusetzen, aber keine Klassen zur Definition, wie Metashapes zusammengesetzt werden sollen. Um diesen Unterschied zu verdeutlichen, kann man etwa den Plus-Operator in GENMOD betrachten, der dazu dient, zwei MAN-Abbildungen a und b, deren Resultat Vektoren sind, zu einer neuen Abbildung c zusammenzusetzen, deren Resultat die Summe dieser Vektoren ist. In Genmod lautet die entsprechende Zeile MAN c=a+b. Der +-Operator ist in diesem Fall also ein Metaobjekt, eine Funktion, wenn man so will, die eine zusammengesetzte Abbildung zurückliefert. Die Implementation für diesen Operator in Genmod lautet unter Benutzung von Reference-Pointern wie in [Fis95a] beschrieben daher: t_GmMapPt3DAddPtr add_vector(t_GmMAP& p, t_GmMAP& q) { t_GmMapPt3DAddPtr s = new t_GmMapPt3DAdd; (*s)(p)(q); return s; } Diese Funktion ist damit die Implementation eines elementaren Operators, letztendlich nur ein weiterer Wrapper zum Binden der Abbildungen, die die Implementation des Operators darstellt. Ein solcher Wrapper könnte direkt von einem Parser aufgerufen werden, der den Ausdruck a+b verarbeitet, und feststellt, daß irgendwelche zwei Abbildungen addiert werden, die 3D-Vektoren zurückliefern. Dieser Ausdruck wird dabei in der Definition eines höheren Operators benutzt, und man hätte die gleiche Funktionalität wie bei Snyder. Um ein Metashape zu definieren, das die Strecke s pq (t ) = (1 , t ) p + tq zwischen zwei Punkten p; q als eine Abbildung mit der Signatur Real! Vector realisiert, könnte der Code auf C++-Ebene folgendermaßen aussehen. Dabei verwendet man der Deutlichkeit halber noch keine überladenen C++-Operatoren, obwohl damit der Code noch GENMOD-ähnlicher werden würde. t_GmMapRealAdd interpolate(t_GmMAP& t, t_GmMAP& p, t_GmMAP& q) { t_GmMapRealMinus one_minus_t=real_minus(real_const(1.0),t); return add_vector(scale_vector(one_minus_t,p),scale_vector(t,q)); } Genmod besitzt jedoch keine solchen Operatoren, und zwar aus dem einfachen Grunde, daß man nicht mehr an das Programmieren von Abbildungen in C++ gebunden sein möchte. Vielmehr geht es in der zweiten Implementation von Genmod in Haskell darum, ein Sprachkonzept zu erarbeiten, das der generativen Idee besser angepaßt ist als das von Snyder verwendete imperative KAPITEL 4. GENERATIVE MODELLIERUNG 52 Sprachparadigma. In einer funktionalen Sprache kann einfach auf einer höheren Ebene modelliert werden. Genmod bietet also zwar keine Objekte, um Vorschriften für den Zusammenbau von Abbildungen zu formulieren, also keine Operatorfunktionen wie GENMOD, aber das Binden von Abbildungen kann – wie im Code zu add_vector() – mit Hilfe des ()-Operators durchgeführt werden, der in der Abbildungs-Basisklasse t_GmMAP definiert ist. Damit können auf C++-Ebene Operatoren programmiert werden, es müssen allerdings – wie in der Normalform für C++-Programme – sämtliche Statements einzeln behandelt werden, und dabei führt man stets neue Variablen ein, was zuweilen etwas unübersichtlich werden kann. Der Zweck von Genmod ist wie gesagt aber nicht vordringlich die leichte Definition von Operatoren in C++, sondern Laufzeitflexibilität. Die wirkliche Definition von interpolate() sieht in Genmod daher folgendermaßen aus: t_GmMapCompose interpolate(t_GmMAP& t, { t_Real realOne=1.0; t_GmMapRealConst one(&realOne); t_GmMapRealMinus one_m_t; t_GmMapPt3DScalMult scale_p; t_GmMapPt3DScalMult scale_q; t_GmMapPt3DAdd add_p_q; t_GmMapCompose segment; t_GmMAP& p, t_GmMAP& q) one_m_t scale_p scale_q add_p_q segment (one)(t); (one_m_t)(p); (t)(q); (scale_p)(scale_q); (add_p_q); return segment; } Für die zusammengesetzte Abbildung segment werden dabei die einzelnen Terme konstruiert, zunächst wird (1 , t ) berechnet, dann die beiden Vektoren skaliert, und diese schließlich summiert. Die zusammengesetzte Abbildung t_GmMapCompose linearisiert den DAG, es wird schließlich nur ein einziges Abbildungsobjekt zurückgegeben, das eine Sequenz von Abbildungen enthält, die von t_GmMapCompose als Kopien der automatischen Variablen angelegt worden sind, die ja mit Beendigung der Funktion – und vor allem nach Konstruktion des Rückgabewertes segment – wieder vom C++-Stack entfernt werden. Nun ist die Schreibweise im zweiten Beispiel zwar deutlich mühsamer und bewegt sich auf einem niedrigeren Level, andererseits ist es mit einer Einführung von Wrapper-Funktionen, die es ermöglichen, Funktionen wie im ersten Beispiel zu schreiben, eben nicht getan. Die Realisierung höherer Operatoren, von Metashapes also, durch C++-Funktionen, ist denkbar unbefriedigend, weil diese Funktionen compiliert werden, also nicht, wie angestrebt, während des Programmlaufes definiert werden können. Was stattdessen mit Genmod realisiert wurde, ist folglich der zugrundeliegende Kern einer Sprache: Erweiterbare Menge von “eingebauten” Datentypen Erweiterbare Menge von elementaren Operationen auf diesen Datentypen Damit hat man aber noch keine höheren Konstrukte, die zu einer interpretierten Programmiersprache gehören. Der erste Schritt zur Erweiterung des Mechanismus wäre die Einführung eines Funktionsobjektes, das ist eine Bindevorschrift für eine Sequenz von Abbildungen. Diese müßte das gleiche leisten wie obige Funktion interpolate(). Das heißt, man kann flexibel vorschreiben, wo die Eingabe-Maps t, p und q an andere Maps gebunden werden. In Genmod wurde dazu mit speziellen Aufrufabbildungen experimentiert, die ein Binding bzw. Rebinding durchführen können. Das bedeutet, eine Kopiervorlage, ein Werkzeug, wird zunächst kopiert, und dann mit neuen Parametern versehen. – Das bedeutet aber letztendlich auch, Abbildungen zu Objekten zu machen, die 4.1. GENERATIVE MODELLIERUNG IN C++ 53 das Objekt von Abbildungen sind. An dieser Stelle findet sich aber eine prinzipielle Grenze, denn die Memberfunktionen der Abbildungsbasisklasse führen destruktive Updates durch und können daher selber nicht in Abbildungen verpackt werden. An dieser Stelle wird deutlich, daß es nötig ist, tatsächlich eine neue Meta-Ebene oberhalb von Abbildungen einzuführen, die den Umgang mit Abbildungen in objektorientierter Weise realisiert. Um Genmod zu einer vollen Programmiersprache auszubauen, wäre es daher nötig, die folgenden Punkte zu realisieren: Funktionsobjekte Ein Funktionsobjekt ist eine Bindevorschrift, die es ermöglicht, eine Instanz eines Werkzeuges zu erzeugen und an spezielle Parameter zu binden. Es führt sämtliche Aktionen durch, die beim Aufruf obiger C++-Funktion interpolate ausgeführt werden. Kontrollstrukturen Um Strukturen zu realisieren wie eine Liste von Objekten, deren Parameter variiert werden, sind Schleifen oder – grundlegender – Verzweigungen und relationale Ausdrücke notwendig. Grundlage hierfür wäre eine branching-Abbildung wie in GENMOD Aufrufsemantik Es muß ein Stack-Konzept gefunden werden, wenn Funktionsaufrufe objektorientiert gekapselt sind, um “lokale Variablen” zu speichern. Dabei ist nicht klar, wann der Stack wieder zu löschen ist, weil keine Objekte verlorengehen dürfen, die noch referenziert werden. Es wäre dazu ein Mechanismus denkbar, der wie Reference-Objects [Fis95a] für Abbildungen funktioniert. Scoping Es muß ein Konzept für lokale Variablen bzw. lokale Definitionen von Funktionsobjekten gefunden werden, um temporäre Ausdrücke darstellen zu können. Dies ist wichtig, um die Flut von C++-Variablen bzw. Genmod-Variablenobjekten einzudämmen, die durch die Rückführung auf die objektorientierte Normalform entstanden ist. Die Mächtigkeit der Funktionsobjekte, die erforderlich sind, läßt sich aber an den umfangreichen Programmen ablesen, die bereits in Genmod realisiert sind, siehe die Beispiele in 6.1. Der Punkt der Aufrufsemantik ist dabei von besonderer Bedeutung. Bisher wird so vorgegangen, daß ein DAG von Abbildungen aufgebaut wird, dieser wird in einer zusammengesetzten Abbildung linearisiert. Das bedeutet, die Klasse t_GmMapCompose verfolgt die Eingaben einer übergebenen Abbildung zurück, bis sie auf konstante Abbildungen stößt, die keine weiteren Eingaben mehr benötigen, oder auf eine Abbildung, die der t_GmMapCompose explizit als Eingabeabbildung mitgeteilt wurde. Dazu wird der ()-Operator benutzt. – Alle entdeckten Abbildungen werden kopiert und die Kopien neu verbunden, die entstehende Sequenz kann in einem Durchgang abgearbeitet werden. Ist nun aber eine Teilabbildung dabei selber eine zusammengesetzte Abbildung, so wird diese bei der Kopie selber vollständig kopiert. Das bedeutet, wenn komplizierte Abbildungen über mehrere Stufen zusammengesetzt werden, so besteht die resultierende Abbildung aus einer unter Umständen sehr langen Sequenz von Elementaroperatoren. Aus diesem Grunde ist bei der gegenwärtigen Implementierung keine Aufrufsemantik vonnöten, weil stets mit einer ’deep copy’ einer Abbildung gearbeitet wird, die lokal gebunden ist und deren Bindung auch nicht wieder gelöst wird. Der Grund für diese Vorgehensweise sind Effizienzüberlegungen, denn das Linearisieren und Binden ist wegen der Typsicherheit ein verhältnismäßig aufwendiger Prozeß, der eben nicht zur Compilezeit, sondern zur Laufzeit stattfinden muß, KAPITEL 4. GENERATIVE MODELLIERUNG 54 dann, wenn Abbildungen verbunden werden. Um die Auswertungszeit einer zusammengesetzten Abbildung gering zu halten, wird also nur einmal gebunden. Dadurch haben zusammengesetzte Abbildungen in Genmod eine rasend hohe Ausführungsgeschwindigkeit, es bestehen praktisch keine Verwaltungs-Overheads, und die Rechenzeit ist allein die Rechenzeit für die jeweiligen Elementarabbildungen, die der Anwender voll unter Kontrolle hat, weil er sie selber schreibt. Eine kurze Ausführungszeit ist aber zum Beispiel beim Sampling von entscheidender Bedeutung, wenn die Abbildung, die eine Fläche parametrisiert, mehrere hundert Mal mit verschiedenen (u; v)Werten ausgewertet wird. Folgende Entsprechung gilt damit zwischen dem Genmod-Paket und der GENMOD-Sprache. Genmod-Paket Variablenklassen t_GmVar Abbildungsklassen t_GmMAP (C++-Funktionen) (C++-Funktionen) execute() Umwandlung in MRT-Objekt ! ! ! ! ! ! GENMOD-Programmiersprache reelle Vektoren Mannigfaltigkeit MAN elementare Operatoren Metashapes Operatormethode Auswertung Operatormethode Sampling Statt jedoch Genmod zu einer vollen Programmiersprache auszubauen, wurde zunächst das bestehende Paket untersucht, indem einige Standardbeispiele der Generativen Abbildung zu Testzwecken in Normalform überführt wurden, zusammen mit den nötigen Operatoren in Form von C++-Funktionen. Einige dieser Beispiele, die eine konkrete Vorstellung vom Nutzen der Generativen Modellierung vermitteln, werden in 6.1 besprochen. 4.2 Generative Modellierung in Haskell Um sich dem Problem zu nähern, welche Sprachklasse angemessen ist, um eine geometrische Programmiersprache für die Generative Modellierung zu realisieren, wurde eine Referenzimplementation im funktionalen Paradigma vorgenommen, weil dort das Prinzip des Rechnens mit Abbildungen am konsequentesten verfolgt wird. Dabei wurde deutlich, daß einige der Probleme, auf die man bei der Realisierung von Genmod gestoßen ist, prinzipielle Probleme der Generativen Modellierung sind, für die in funktionalen Sprachen zum Teil Lösungen existieren. Beispielsweise läßt sich argumentieren, daß die Wiederverwendbarkeit eines Werkzeugs genauso wie die Referenzierbarkeit eines Modells an verschiedenen Stellen in einer Szene Wertkonstanz von Variablen bzw. von Rückgabewerten von Abbildungen impliziert. Dies wiederum bedeutet, daß man zum Beispiel keine Schleifen wie in imperativen Sprachen schreiben kann, denn die Schleifenvariable verändert sich; die funktionale Programmierung beantwortet dies so, daß Rekursion anstelle von Schleifen verwendet wird. Die inhärente Ineffizienz der Rekursion im Vergleich zur Schleife kann in bestimmten Fällen aber dadurch aufgehoben werden, daß ein Compiler einer funktionalen Sprache erkennt, daß eine ’tail recursion’ intern wie eine Schleife behandelt wird und entprechend effizienter abläuft. Dies ist ein Beispiel, wo kontrolliert destruktive Updates, in diesem Falle der Schleifenvariablen, durchgeführt werden können, und im Bereich der Forschung auf dem Gebiet der funktionalen Sprachen werden komplizierte Algorithmen entwickelt, um Möglichkeiten für solche Optimierungen zu finden. Ein entscheidender anderer Punkt, der Beachtung verdient, ist das Fehlen höherer Funktionen sowohl in Snyders GENMOD-Ansatz als auch zunächst im Genmod-Paket. Das bedeutet, es ist in GENMOD keine Möglichkeit vorgesehen, Metashapes auszutauschen. Wird in einer Konstruktion der wire-product-Operator m_wire(MAN cross, MAN wire) wie in Abschnitt 3.1 definiert verwandt, 4.2. GENERATIVE MODELLIERUNG IN HASKELL 55 so steht der entsprechende Funktionsaufruf fest im Programmtext. Es ist nicht möglich, den Operator selber zu einem Parameter eines anderen Operators zu machen, das bedeutet, eine Konstruktion kann nicht in Hinsicht auf die verwendeten Operatoren parametrisiert werden. Um dies zu leisten, wäre in GENMOD eine weitere Indirektion erforderlich, eine Erweiterung des Abbildungsbegriffs von Funktionen Rm ! Rn auf Funktionen mit Signaturen wie surface_deformation: ( R2 ! R3 ) ! ( R2 ! R3 ) aus Abschnitt 2.5.2 mit einem entsprechend angepaßten Konzept von Operatormethoden. Das bedeutet aber gerade den Übergang zu höheren Funktionen, die aber ebenfalls ein Konzept aus funktionalen Sprachen darstellen. Der Nutzen solcher höherer Funktionen wird in in der HaskellImplementation dadurch deutlich, daß man einen einheitlichen Beschreibungsrahmen für Objekte, für Meta-Objekte, die auf Objekten operieren usw. formulieren kann. Dadurch kann es Klassen geben, die eine Funktion als Parameter erwarten, und aus ihr ein Flächenobjekt machen, ganz ähnlich wie t_GmMapSurfaceMap. Andererseits ist es möglich, daß es auch nicht nötig ist, eine komplette funktionale Sprache zu realisieren, um generativ modellieren zu können. Generatives Modellieren ist offensichtlich auch in einem C-Derivat wie GENMOD möglich, doch die damit realisierten Programme sind wie das Beispiel in 3.1 sehr schematisch aufgebaut und scheinen dem Sprachumfang von C nicht angemessen. Dies kann als ein Indiz dafür angesehen werden, daß ein anderes Sprachkonzept sinnvoller wäre. Um herauszufinden, in welcher Weise funktionale Konzepte bereits in Genmod teilimplementiert sind, und um herauszuarbeiten, welche weiteren Konzepte funktionaler Sprachen in der Modellierung von Nutzen sein können, scheint es gerechtfertigt, dem Genmod-Paket eine Implementation in einer funktionalen Sprache gegenüberzustellen. 4.2.1 Warum Haskell? Neben einer Implementation in C++ kann die Genmod-Idee ebenso im Rahmen einer funktionalen Programmiersprache formuliert werden. Dies bedeutet keine Aufgabe der objektorientierten Sichtweise; vielmehr ist Anfang der neunziger Jahre eine Synthese beider Konzepte in Form der Programmiersprache Haskell entstanden, die eine neuere Entwicklung im Bereich der funktionalen Programmierung darstellt. Haskell vereint eine ganze Reihe von Konzepten, die von verschiedenen funktionalen Programmiersprachen eingeführt worden sind. Auf einer Konferenz über funktionale Sprachen, der FPCA ’87 in Portland, Oregon, wurde von Sprachentwicklern beklagt, daß zwar viele Konzepte im Laufe der Jahre entwickelt und auch realisiert worden sind, daß aber ein Mangel an Einheitlichkeit herrscht, der die Forschung eher behindert als weiterbringt. Daher wurde dort Haskell, benannt nach Curry B. Haskell, einem führenden Logiker, als eine Sprache entworfen, die Einzelentwicklungen der funktionalen Programmierung in sich vereint und Objektorientierung als zusätzliches Konstrukt einführt. Haskell kann also als die funktionale Standardsprache angesehen werden und ist aus diesem Grund für eine funktionale Implementation ausgewählt worden. Haskell zeichnet sich durch die folgenden Merkmale aus [Hud89], [HJe92], deren Bedeutung für die Modellierung anschließend erläutert wird. Funktionen höherer Ordnung Funktionen sind ’first class citizens’ und können an Funktionen als Parameter übergeben, zusammengesetzt sowie als Rückgabewerte zurückgegeben werden. Curried functions sind teilgebundene Funktionen, ist add eine Funktion Float!Float!Float, so ist (add 5.0) eine Funktion Float!Float und (add 5.0 2.0) eine Zahl. Lambda-Ausdrücke sind anonyme, also unbenannte Funktionen, ( (n x y -> x+y ) 5 7 ) ist 12. KAPITEL 4. GENERATIVE MODELLIERUNG 56 Lazy evaluation Im Unterschied zum ’call by value’-Mechanismus prozeduraler Sprachen, bei dem Funktionsargumente vor Übergabe ausgewertet werden, wird bei ’call by need’ lazy evaluation eingesetzt. Dabei werden Argumente erst dann ausgewertet, wenn sie innerhalb einer Funktion auch tatsächlich benutzt werden. Dies hat den Vorteil, daß virtuell unendliche Datenstrukturen verwendet werden können. Statisches Typsystem Sämtliche Variablen und Funktionen sind statisch getypt, wodurch Typfehler bereits bei der Kompilation erkannt werden. Zum Typsystem gehören dabei auch Typvariablen sowie beliebig komplexe Funktionstypen. Polymorphismus Haskell unterstützt ad-hoc-Polymorphismus, Überladen, wo Funktionen gleichen Namens für unterschiedliche Typen unterschiedliche Wirkung haben, und parametrischen Polymorphismus, wo generische Funktionen mit Hilfe von Typvariablen geschrieben werden, sodaß über alle Typen hinweg die gleiche Funktionalität realisiert wird. Dies ist zu vergleichen mit C++, wo beides über virtuelle Funktionen und den Template-Mechanismus realisiert ist. Beispiel ist eine Funktion twice, die eine Funktion f zweimal hintereinander anwendet, (twice f) ist dann vom gleichen Typ wie (f): twice f x = f (f x) Pattern matching Funktionen können je nachdem, wie die übergebenen Variablen aussehen, durch Pattern matching verschiedenes Verhalten zeigen. List comprehensions Es gibt einen sehr flexiblen Mechanismus, wie aus Listen neue Listen erzeugt werden können, der an die mathematische Schreibweise angelehnt ist. Eine quasi-unendliche Liste aller Fibonaccizahlen erhält so man mit zip, das aus zwei Listen eine Liste von Paaren macht: fib = 1:1:[ a+b | (a,b) <- zip fib (tail fib) ] Rekursion Die Fibonaccizahlen sind ein Beispiel einer rekursiven Definition. In funktionalen Sprachen gibt es keine Schleifen; daher benutzt man stattdessen rekursive Definitionen, die deshalb von Compilern auch entsprechend effizient unterstützt werden. Klassen Haskell-Klassen sind lediglich Interface-Vereinbarungen, wo die Signaturen der Memberfunktionen definiert werden. Dies ist mit rein virtuellen Klassen von C++ vergleichbar. Ein Datentyp wird zu einer Instanz einer Klasse, indem erklärt wird, wie die Funktionen der Klasse auf diesen Datentyp wirken. Referentielle Transparenz Grundlage funktionaler Sprachen ist der Lambda-Kalkül. Dies ist ein reiner Ersetzungskalkül für typisierte Ausdrücke. Daraus leitet sich das funktionale Sprachparadigma ab: Equals can be replaced by equals. Das bedeutet, daß nach einer Zuweisung eines Ausdrucks an eine Variable an allen Stellen, wo diese Variable verwendet wird, genausogut der Ausdruck eingesetzt werden könnte. 4.2. GENERATIVE MODELLIERUNG IN HASKELL 57 Der Eigenschaft der referentiellen Transparenz kommt dabei im Rahmen der Modellierung eine besondere Bedeutung zu. Verwendet eine Haskell-Anweisung y=x+x eine Variable x und ist diese definiert als x=f 1, so entspricht die Definition von y dem Ausdruck y=(f 1)+(f 1). In prozeduralen Sprachen ist dies nicht unbedingt der Fall, weil es sein kann, daß zwischen einer Zuweisung x=f(1) und der Verwendung in y=x+x der Variablen x ein neuer Wert zugewiesen wird. Dieser Seiteneffekt ist in funktionalen Sprachen ausgeschlossen, weil eine zweite Zuweisung an x vom Compiler als Fehler erkannt würde. Daher ist herkömmliches sequentielles Programmieren in funktionalen Sprachen nicht möglich, und alle Definitionen, aus denen ein Programm besteht, gelten gleichberechtigt nebeneinander als Gleichungen. – Ein bemerkenswerter Gewinn ist dabei, daß funktionale Programme durch das Fehlen von Seiteneffekten wesentlich effizienter zu verifizieren sind als herkömmliche imperative. Umgekehrt bedeutet referentielle Transparenz aber auch Wertkonstanz, denn Objekte, sind sie einmal definiert, können ihren Wert nicht mehr ändern. Intern werden Objekte solange behalten, wie sie noch referenziert werden. Solange können sie auch als Parameter für Funktionsaufrufe eingesetzt werden. Diese Tatsache hat folgende Konsequenzen für die Modellierung. Wertkonstanz ermöglicht erst Wiederverwendbarkeit und Änderbarkeit. Jede Manipulation an einem Objekt erzeugt ein neues Objekt. 4.2.1.1 Referentielle Transparenz: Nutzen Um die Bedeutung einer referentiellen Transparenz zu erläutern, ist es zunächst nützlich, festzustellen, daß dies einer unbegrenzten Undo-Fähigkeit gleichkommt. Denn dadurch, daß jede Anwendung einer Operation auf ein Objekt ein neues Objekt erzeugt, wird keine Information zerstört, und die ursprünglichen Objekte können stets an anderer Stelle erneut verwendet werden. Zum einen ist damit also auf jeder Stufe eine Änderbarkeit gegeben, weil sämtliche Schritte des Konstruktionsprozesses nachträglich noch zugreifbar sind. Andererseits können alle Zwischenergebnisse eines abgeschlossenen Konstruktionsprozesses nachträglich neu und anders verwandt werden. Damit ist auf dieser Ebene eine Wiederverwendbarkeit aller Teilergebnisse der Konstruktion gegeben. Um ein Teilergebnis wiederzuverwenden, muß man es lediglich referenzieren, also benennen, in einer Liste speichern oder einer übergeordneten Funktion als Rückgabewert zur Verfügung stellen. 4.2.1.2 Referentielle Transparenz: Kosten In der Modellierung werden Objektstrukturen verwandt, die an Größe mehr und mehr zunehmen. Ändert man aber nur die Position eines Punktes in einem Dreiecksgitter mit vielen tausend Einzeldreiecken, so fordert die referentielle Transparenz, daß ein neues Objekt anzulegen ist, das sich an nur einer Stelle vom ursprünglichen Objekt unterscheidet. Und zwar ist dies nötig, wenn die Möglichkeit besteht, daß das ursprüngliche Objekt noch an anderer Stelle verwendet, das bedeutet, wenn es noch referenziert wird. Die Entscheidung, einer Sprache die Eigenschaft der referentiellen Transparenz zu geben, hat also weitreichende Konsequenzen. Die Unmöglichkeit destruktiver Updates, bei denen Speicherplatz bei Änderungen wiederverwendet werden kann, macht funktionale Programme sicher weniger effizient als sie es bei kontrollierter Wiederverwendung von Speicher wären. Dies ist einer der Gründe dafür, warum funktionale Sprachen als hoffnungslos ineffizient und nicht tauglich für reale Anwendungen gelten. Dies gilt wegen der Komplexität der Modelle in besonderer Weise für 58 KAPITEL 4. GENERATIVE MODELLIERUNG die Modellierung, wo die populären Dreieckskomplexe leicht mehrere Megabytes groß werden können. Andererseits betreiben die Produzenten von Modellierpaketen auch einen nicht unerheblichen Aufwand, um dem Benutzer möglichst viele ’Undo-Level’ anzubieten, und die Daten vor Änderung sind dabei auch zwischenzuspeichern. Ein typisches Beispiel, wie Benutzer mit diesem Mangel umgehen, ist die Technik der ’Sicherungspunkte’, wo jeweils nach einer Reihe von Arbeitsschritten das komplette Modell, mit Versionsnummern, versehen “zur Sicherheit” gespeichert wird, um später Zugriff auf die Ausgangselemente einer Konstruktion zu haben. – 3D-Studio Max etwa stellt bei einem Verschneiden von BReps bei Booleschen Operationen die Frage, ob die Ausgangskörper gelöscht werden sollen. Selbst wenn diese Frage verneint wird, hat der Modellierer aber keine Möglichkeit, im Modifier-Stack die ursprünglichen Elemente der Boole’schen Operation nachträglich zu modifizieren. Ein Objekt ’Boolean’ wird von 3D-Studio Max nämlich als ein neues Objekt angesehen und nicht als Modifikation bestehender Objekte. – Diese Unterscheidung wirkt künstlich und zeigt, wie beschränkt das Konzept des Modifier-Stack ist, wenn es in dieser Weise implementiert wird. Schließlich ist nicht nur das Verbiegen eines Objektes eine Modifikation, die würdig ist, referenziert zu werden und die nachträglich änderbar sein sollte, sondern jede Operation verändert ja die Szene als solche. Eine gesamte Szene hat aber keinen Modifier-Stack, und nicht jede beliebige durchgeführte Aktion läßt sich benennen und an anderer Stelle in gleicher Weise wiederholen. Würde ein Modeller dies alles anbieten, so wäre er de facto referentiell transparent und es ergäben sich die gleichen Effizienzprobleme wie jene, auf die die Forschung zur Implementierung funktionaler Sprachen bereits seit längerem gestoßen ist. Dies ist ein weiterer Beleg für die These, daß der Modellierung ein inhärent funktionales Paradigma zugrundeliegt. Gleichzeitig wird hier aber auch ein Grundwiderspruch deutlich zwischen der objektorientierten Sichtweise wie sie in Sprachen wie Smalltalk oder C++ realisiert ist und der funktionalen Sichtweise von Haskell-Klassen. C++ sieht ein Objekt als eine Datenstruktur, mit der über Nachrichten (Memberfunktionen) kommuniziert werden kann. Mit Hilfe von set- und get-Funktionen können Informationen über seinen inneren Zustand (Membervariablen) ausgetauscht werden, dieser Zustand kann aber auch geändert werden, was die Möglichkeit eines destruktiven Updates erfordert. In Haskell sind Klassen aber nichts als Signaturvereinbarungen, über eine neue Klasse führt man einen neuen Geltungsbereich für Memberfunktionen ein. Dann kann jeder Typ als zu dieser Klasse zugehörig erklärt werden indem man definiert, wie die nötigen Memberfunktionen der Klasse auf diesem Typ operieren. So kann man etwa binäre Bäume zu Integers machen, wenn nur die Grundrechenarten auf Bäumen erklärt werden, oder doppelt verkettete Listen zu Strings, wenn man die Konkatenationsoperation definiert, ohne daß man nachher allerdings Zeichenketten und doppelt verkettete Listen miteinander konkatenieren könnte. Typen sind nämlich nur als Implementation einer bestimmten Klasse anzusehen, nicht als Unterklassen einer gemeinsamen Oberklasse. Haskell unterscheidet klar zwischen Klassen und Typen. 4.2.2 Formulierung von Genmod in Haskell Die Haskell-Implementation von Genmod ist sehr knapp, alle nötigen Klassen finden in einer Datei von sechzehn Kilobytes Platz, siehe Anhang. Diese Referenzimplementation weist dabei starke Ähnlichkeiten mit einer frühen C++-Implementation von Genmod auf und ist nicht mit so ausgefeilten Verwaltungs- und Erweiterungsmechanismen versehen wie das beschriebene GenmodPaket. Einerseits ist diese Realisierung daher mehr als ein ’proof of concept’ zu sehen, andererseits erübrigen sich Optimierungen wie die Linearisierung eines DAGs von Abbildungen dadurch, daß das Rechnen mit Abbildungen integraler Bestandteil von Haskell ist. Haskell verfügt als echte 4.2. GENERATIVE MODELLIERUNG IN HASKELL 59 Programmiersprache zudem über Scoping, lokale Funktionsdefinitionen und ein statisches Typsystem, um einige der bei Genmod notwendigen Erweiterungen zu nennen. Die Implementation beschränkt sich auf die Basis-Typen für die Modellierung: Vektoren, Kurven und Flächen. Wie erwähnt werden dazu im wesentlichen die Signaturvereinbarungen benötigt, die eine Haskell-Klasse ausmachen. Kurven und Flächen sowie Transformationen sind in einer Weise definiert, daß nur die Auswertungsfunktion selber überschrieben werden muß, die anderen Funktionen besitzen eine Default-Implementation, die auf dieser Auswertungsfunktion aufsetzt, indem Ableitungen durch Differenzenquotienten bestimmt werden. Dieses Vorgehen ist vollkommen analog zur Vorgehensweise beim cns-Paket der Kurven und Flächen in Genmod. class Curve c tangent mainNormal biNormal evalCurve where :: c -> :: c -> :: c -> :: c -> Float Float Float Float -> -> -> -> Vector3 Vector3 Vector3 Vector3 class Surface tangentU tangentV normal evalSurf s where :: s -> :: s -> :: s -> :: s -> Float Float Float Float -> -> -> -> Float Float Float Float -> -> -> -> Vector3 Vector3 Vector3 Vector3 class Transform t where evalTrans :: t -> Vector3 -> Vector3 Diese Deklaration ist so zu lesen, daß eine Typ c eine Kurve ist, falls es eine Funktion evalCurve für diesen Typ gibt. Ist crv_0 eine Variable dieses Typs, so erfolgt die Auswerung durch (evalCurve crv_0 0.5), dieser Ausdruck ist damit vom Typ Vector3. Eine erste Version des Genmod-Paketes in C++, die ohne Trennung von Kern und cns-Bibliothek arbeitete, hat Klassen nach dem folgenden Muster verwandt, um etwa eine Kurve zu gewinnen, die zwischen zwei anderen Kurven liegt: class CrvInterpol : public Curve { public: CrvInterpol(Curve& c1, Curve& c2, Real t) : crv1(c1), crv2(c2), t0(t) { } virtual Pt3 eval(Real t) { return line(crv1.eval(t),crv2.eval(t),t0); } private: Curve& crv1; Curve& crv2; Real t0; }; Diese Implementation wurde zugunsten einer Trennung von Genmod-Kern und geometrischer Bibliothek aufgegeben, um eine Optimierung des Aufrufgraphen vornehmen zu können. – Die Funktion line berechnet dabei (1 , t ) p + tq, die entsprechende Definition in Haskell benutzt ebenfalls eine Funktion line und sieht folgendermaßen aus: line p q t = add (mult (1-t) p) (mult t q) KAPITEL 4. GENERATIVE MODELLIERUNG 60 data (Curve c, Curve d) => CurveInterpol c d = CrvInterpol c d Float instance (Curve c, Curve d) => Curve (CurveInterpol c d) where evalCurve (CrvInterpol crv1 crv2 t0) t = line (evalCurve crv1 t) (evalCurve crv2 t) t0 Das ist so zu lesen, daß mit der data-Deklaration ein neuer Typ eingeführt wird, ein Metashape, und zwar CurveInterpol mit dem Konstruktor CrvInterpol1, der drei Argmente erwartet, nämlich zwei Kurven und einen Interpolationsparameter. Um diesen neuen Typ zur Klasse Curve zugehörig zu erklären, wird in der instance-Deklaration nur definiert, was die Auswertungsfunktion evalCurve tut. – CurveInterpol ist eine lineare Interpolation zwischen korrespondierenden Kurvenpunkten zweier Kurven. Für t0=0 entspricht sie crv1, für t0=1 crv2, und die Gesamtheit aller Kurven für t0 aus [0; 1] entspricht der linearen Interpolationsfläche zwischen korrespondierenden Kurvenpunkten. Diese Fläche kann unter Benutzung von CrvInterpol folgendermaßen definiert werden: data (Curve c, Curve d) => SurfInterpolCrv c d = SrfInterpolCrv c d instance (Curve c, Curve d) => Surface (SurfInterpolCrv c d) where evalSurf (SrfInterpolCrv crv1 crv2) u v = evalCurve (CrvMorph crv1 crv2 u) v Dies entspricht der Definition, die Snyder für die Generative Modellierung gegeben hat: Ein Generatives Modell ist eine Form, die durch die kontinuierliche Transformation einer anderen Form, des Generators, entsteht. In Haskell-Genmod gibt es nun eine ganze Reihe von Kurvenund Flächentypen, deren Instanzen wie CrvInterpol aus anderen Kurven und Flächen entstehen. Darunter sind Reparametrisierung, Umkehrung der Parameterrichtung, Projektion einer Kurven in den Parameterbereich einer Fläche, bilineare Coons-Interpolation von Randkurven zu einer Fläche und weitere, die in der Implementation in Kapitel 5.2 vorgestellt werden, Beispiele dazu findet man in 6.2. Entscheidend bei der Benutzung von Haskell ist aber, daß man ebenfalls Kurven, Flächen und Transformationen definieren kann, wenn man nur eine Funktion hat, deren Signatur der jeweiligen Auswertungsfunktion entspricht. – Die allgemeinste Definition einer parametrischen Fläche ist beispielsweise eine, wo einfach eine beliebige gegebene Funktion benutzt wird, die aus zwei FloatWerten einen Vektor macht. In Haskell sind dies Dreizeiler: data SurfFct = SrfFct (Float->Float->Vector3) instance Surface SurfFct where evalSurf (SrfFct f) u v = f u v data CurveFct = CrvFct (Float->Vector3) instance Curve CurveFct where evalCurve (CrvFct f) t = f t data TransformFct = TrnFct (Vector3->Vector3) instance Transform TransformFct where evalTrans (TrnFct f) p = f p Der Anwender modelliert nun, indem er diese vorgefertigten Kurven, Flächen und Transformationen benutzt und durch explizite Komposition zusammensetzt. Auf diese Weise erhält man 1 Namenskonvention: Typen und Konstruktoren haben bis auf das Präfix identische Namen, Curve, Surface und Transform sind die Präfixe für Typen, Crv, Srf und Trn jene für Konstruktoren 4.2. GENERATIVE MODELLIERUNG IN HASKELL 61 eine Trennung der Werkzeugbibliothek mit den Operatorklassen von der Anwendung. In der Anwendung, in der eine Szene mit ihren Gegenständen definiert wird, sieht man einmal die Kompositionsoperationen, die in einem interaktiven Modeller den Werkzeugaufrufen entsprechen, und andererseits enthält eine Anwendung natürlich Konstanten, die den normalerweise interaktiv eingegebenen Punkten und Zahlen entsprechen. Andererseits ist es möglich, flexibel Konstanten, die mehrfach auftreten, zu Parametern eines Modells zu erklären, sodaß man diese an zentraler Stelle ändern kann. Damit erübrigen sich Hilfskonstrukte wie Gitter, um auf bestimmte Punkte an verschiedenen Stellen bezug nehmen zu können. Andererseits hat der Benutzer die Möglichkeit, wiederkehrende Konstruktionen schließlich in Klassen zu verpacken, die seine eigene Werkzeugbibliothek ergänzen. Dies soll an einem Beispiel verdeutlicht werden, das demonstriert, wie die eingeführten Klassen benutzt werden. Um den Verlauf der Hauptnormalen einer Kurve zu zeigen, kann man diesen Richtungsvektor an jeder Stelle der Kurve auftragen, also zum Kurvenpunkt hinzuaddieren. Um dies für eine Bézierkurve mit bestimmten Kontrollpunkten durchzuführen, schreibt man die folgende Anwendung: p0 p1 p2 p3 = = = = V3 (-2.0) 0.0 (-1.0) V3 (-1.0) 0.8 0.0 V3 1.0 0.8 2.0 V3 2.0 0.0 (-1.0) path = CrvBezier p0 p1 p2 p3 pathNormal = CrvFct (\t -> add (evalCurve path t) (mainNormal path t)) srfNormal = SrfInterpolCrv path pathNormal Statt ein spezielles Werkzeug zu verwenden, das zwei Kurven addiert, kann man so in Haskell mit Hilfe einer anonymen Funktion t 7! path(t ) + pathnormal (t ) in Lambda-Schreibweise einfach die Klasse CrvFct benutzen; das ist die entscheidende Flexibilität, die dieser Ansatz bietet. Zudem ist es ganz einfach, eine “Haut” zwischen beide Kurven zu spannen, die Fläche SrfInterpolCrv ist das Werkzeug, das sich dazu anbietet. – Benutzt man diese Konstruktion nun mehrfach in verschiedenen Anwendungen, wobei man einmal den Verlauf der Hauptnormale, ein anderes Mal den der Binormale oder der Tangente zeigen möchte, so bietet es sich an, aus dieser Konstruktion ein Werkzeug zu machen, das der Anwenderbibliothek hinzugefügt wird. data (Curve c)=> SurfOffsetIntp c = SrfOffsetIntp c (c->Float->Vector3) instance (Curve c) => Surface (SurfOffsetIntp c) where evalSurf (SrfOffsetIntp path fct) u v = evalSurf fctSrf u v where fctSrf = SrfInterpolCrv path fctPath fctPath = CrvFct (\t-> add (evalCurve path t) (fct path t)) Mit der zweiten where-Klausel werden hier lokale Variablen für den evalSurf-Ausdruck vereinbart, so werden in Haskell Mehrfachauswertungen von Termen vermieden. Damit vereinfacht sich eine Anwendung, die Hauptnormale und Binormale zeigt, auf die folgenden beiden Zeilen: srfBiNormal = SrfOffsetIntp path biNormal srfNormal = SrfOffsetIntp path mainNormal An dieser Stelle wird die Bedeutung höherer Funktionen für die Modellierung deutlich: biNormal und mainNormal sind Memberfunktionen der Klasse Curve, die selber als Parameter der Anwenderklasse SrfOffsetIntp dienen können. Gleichzeitig kann aber auch jede andere Funktion KAPITEL 4. GENERATIVE MODELLIERUNG 62 benutzt werden, die aus einer Kurve und einem Parameter einen 3D-Vektor macht, um eine Instanz von SrfOffsetIntp zu erzeugen. Die innere Struktur dieser Klasse kann aber definiert werden, ohne zu wissen, welche Funktion übergeben werden wird. Man hat also durch die Verwendung höherer Funktionen bei der Generativen Modellierung die Möglichkeit, Konstruktionen auf einem noch höheren Niveau zu beschreiben als dies in GENMOD möglich ist, wo zum Beispiel in der Definition von m_wire in 3.1 der Aufruf von derivative nicht parametrisiert werden kann sondern fest verdrahtet ist. Dies ist aber kein Mängel des Abbildungsbegriffs, sondern nur der speziellen Implementierung in einem C-Derivat. Um diese Kurven und Flächen nun anzuzeigen, existiert eine MRT-Anbindung eher provisorischer Natur auf dem Umweg über Dateien, die Kurven- und Flächenpunkte enthalten. Der MRT wurde dazu um zwei t_SurfaceObjekt-Klassen ergänzt, die Samplingdaten aus Dateien lesen können. Die Operatormethode Sampling wird dazu in der main-Funktion von Haskell aufgerufen, durch die die Auswertung der komponierten parametrischen Objekte angestoßen wird. Dabei kann angegeben werden, in wieviele Einzelintervalle [0; 1] für Kurven und [0; 1] [0; 1] für Flächen aufgeteilt wird. Der Dateiname sollte dem Variablennamen entsprechen: main = putStr $ ++ (showCurve "path" (showSurface "srfNormal" 10 8 60 path) srfNormal) Der $-Operator von Haskell ist eine Abkürzung für die Klammerung sämtlicher folgender Terme, f $ g x ist gleichbedeutend mit f(g x) und bedeutet f (g(x)). Im Gegensatz dazu steht f g x im allgemeinen fehlerhafterweise für (f g) x, denn Funktionsapplikation ist in Haskell linksassoziativ. – Kurven werden in Form von Schläuchen gerendert, deren Radius in der msd-Datei angegeben werden kann. Die entsprechenden Zeilen lauten dann folgendermaßen: inputcurve 0 "path" (rad) inputsurface 0 "srfNormal" 4.3 Zusammenfassung Zum einen ist nun festzustellen, daß mit dieser minimalen Haskell-Referenzimplementation der Generativen Modellierung mit kleinem Aufwand eine Ausdruckskraft erreicht wurde, die es ermöglicht, die Beispiele für Metashapes aus elementaren Operatoren in GENMOD direkt zu übertragen. Dazu soll noch einmal das ’wire product’ aus Abschnitt 3.1 betrachtet werden: MAN m_wire(MAN cross, MAN wire) { MAN that = m_derivative(wire,wire->input_list[0]); MAN t = m_normalize(that); MAN n = @(-t[1], t[0]); return @(n*cross[0]+wire, cross[1]); } MAN cross = m_crv("cross.crv",m_x(0)); MAN wire = m_crv("wire.crv", m_x(1)); MAN racket_frame = m_wire(cross,wire); Das Resultat dieser Berechnung, racket_frame, ist eine zusammengesetzte Abbildung. Wertet man diese – zum Beispiel in der Operatormethode “Sampling” – aus, so werden Schritt für Schritt sämtliche elementaren Operationen durchgeführt, die bei Ausführung von m_wire zusammengesetzt werden. 4.3. ZUSAMMENFASSUNG 63 Genau diese zusammengesetzte Abbildung kann außerdem in der C++-Implementation, dem Genmod-Paket, zur Laufzeit angelegt werden, also das Resultat der Ausführung der Funktion – des Metashapes – m_wire. GENMOD-Programme sind letztlich aber wie funktionale Programme aufgebaut, in sofern, als keine destruktiven Updates von Objekten benutzt werden. In keinem Beispiel findet sich ein Ausdruck wie MAN a=b+c; a=a+d;, auf GENMOD-Objekte werden nach der Konstruktion nur noch Operatormethoden wie Auswertung oder Bestimmung der Ableitung angewandt. Das Verbot destruktiver Updates in funktionalen Sprachen ist also nur konsequent. – Gleichzeitig ist die Haskell-Implementation zunächst weniger aufwendig, weil Funktionen bequem als Parameter benutzt werden können, die Operatormethode “Auswertung” ist Sprachbestandteil. Damit läßt sich das GENMOD-Beispiel eins zu eins nach Haskell übertragen: m_wire_fct cross wire u v = q where that = derivOne wire u t = normalize that n = V3 (-(y t)) (x t) 0.0 wire_u = evalCurve wire u crss_v = evalCurve cross v p = add (mult (x crss_v) n) wire_u q = V3 (x p) (y p) (y crss_v) m_wire cross wire = SrfFct (\u v->m_wire_fct cross wire u v) racket_frame = m_wire cross_crv wire_crv Gleichzeitig muß bemerkt werden, daß die anderen Operatormethoden aber nicht Sprachbestandteil von Haskell sind. Für eine tatsächliche genaue Übertragung müßte eine Funktion derivOne angewandt auf eine Kurve wieder eine Kurve zurückgeben; tatsächlich ist das für eine Klasse SrfFct nicht effizient zu implementieren, insbesondere was die für die implizite Modellierung so wichtige Inklusionsfunktion angeht, denn Operatormethoden sind lokal rekursiv, und die Effizienz der Implementierung von GENMOD beruht darauf, daß ein MAN-Objekt weiß, wie es die rekursiven Aufrufe der Operatormethode zu verknüpfen hat. Auswertung ist aber zunächst für die parametrische Modellierung die wichtigste Operatormethode, daher kann man in obigem Beispiel in aller Kürze SrfFct eine anonyme Lambda-Funktion zuweisen. - Noch knapper läßt sich das als Instanz der Flächenklasse formulieren wie in Beispiel 6.2.7. Die Haskell-Implementation bewegt sich also auf der gleichen Ebene wie eine Ableitung von t_SurfaceParam des cns-Paketes, deren evaluate()-Aufruf mit obiger Funktion überschrieben wurde, die auf C++-Ebene zum GenmodPaket hinzugelinkt wird, bedeutet also zunächst keinen Vorteil, solange kein Haskell-Interpreter verwendet wird. Zum anderen ist zu bemerken, daß das Modellieren mit Abbildungen eine abstraktere Modellierung als in interaktiven Modellern ermöglicht, aber auch erfordert. Jede Abbildung erzeugt ein neues Objekt, dies ist in GENMOD wie im Genmod-Paket wie in Haskell-Genmod der Fall, ebenso wie in interaktiven Modellern, die ein unbegrenztes Undo ermöglichen. Konsequenz dieser Tatsache ist, daß Modelle möglichst lange in nicht-approximierter Form vorliegen sollten. Um dies zu verdeutlichen, soll eine einfache Translation einer Kugel als Beispiel herangezogen werden. Generativ bedeutet das, auf ein Kugelobjekt ein Transformationsobjekt anzuwenden, alles, was passiert, ist die Komposition zweier Abbildungen zu einer neuen zusammengesetzten Abbildung. Um die bewegte Kugel zu verwenden, wendet man eine Operatormethode an; beim Sampling beispielsweise werden stets zwei Abbildungen ausgewertet, erst ein Punkt auf der Kugel berechnet, dann dieser bewegt. 64 KAPITEL 4. GENERATIVE MODELLIERUNG Stellt man sich das gleiche für die Kugel in approximierter Form vor, so kann man zum einen eine – interaktive oder skriptgesteuerte – Abbildung anwenden, die einzelne Ecken eines Polyeders bewegt, oder zum anderen eine, die ein Polyeder im Ganzen bewegt. – Die erste Lösung wäre aufwandsmäßig eine Katastrophe, die zweite Methode würde zumindest ein zweites, verschobenes Objekt erzeugen. Eine dritte Möglichkeit, die Veränderung der ersten Kugelapproximation durch Verschieben von Knoten, existiert zunächst einmal wegen des Verbotes destruktiver Updates nicht, die unverschobene Kugel könnte nicht anderweitig referenziert werden. Konsequenz dieser Überlegung ist also ein völliges Abgehen vom approximativen Modellieren wie in der ersten Modellergeneration. Der Schritt vom parametrischen zum Generativen Modellieren bedeutet also aus Effizienzgründen, daß möglichst lange mit der unausgewerteten Form von Modellen alleine als Komposition von Werkzeugen gearbeitet wird. Gleichzeitig ist dies nicht eine Not, sondern eine Tugend, weil es so endlich möglich ist, jeden Punkt einer Oberfläche mit Maschinengenauigkeit zu berechnen, es geht also an keinem Punkt des Modellierprozesses Information über die Konstruktion verloren. Kapitel 5 Implementationen von Genmod in C++ und Haskell Wie im vorigen Kapitel 4 über die Grundideen einer Realisierung der Generativen Modellierung in C++ und Haskell beschrieben, ähneln sich Grundkonzepte in beiden Paketen stark. Dazu gehört einerseits das Konzept der Kurven und Flächen als parametrische Abbildungen, andererseits die Idee, eine Menge von Basisklassen als Kern für eine erweiterbare objektorientierte Klassenhierarchie zugrundezulegen. Im Genmod-Paket des MRT geht es dabei aber nicht nur um die Klassen, mit denen modelliert wird, sondern auch um die Implementation eines Abbildungskonzeptes, das C++ als imperative Sprache so nicht kennt. Die C++-Implementation ist dabei recht umfangreich, es entfallen inklusive der benötigten Hilfsklassen dabei auf das Genmod-Paket 84 Klassen, auf die Bibliothek der Kurven- und Flächen, das cns-Paket, noch weitere 68. In diesem Kapitel sollen daher lediglich die wesentlichen Konzepte und der Umfang der implementierten Verfahren vorgestellt werden. Neben dem Generativen Kern ist bei der C++-Implementierung insbesondere Interesse, in welcher Weise Kurven und Flächen des cns-Paketes realisiert sind. C++- und Haskell-Implementation benutzen dabei die gleiche Grundidee, daß “abstrakte” Basisklassen t_ParametricCurve und t_ParamSurface in C++ und Curve und Surface in Haskell bereits die notwendigen differentialgeometrischen Größen berechnen können, wenn in einer abgeleiteten Klasse bzw. einer Instanz die virtuelle evaluate() bzw. evalCurve oder evalSurf-Funktionen überschrieben werden. Im folgenden werden die differentialgeometrischen Grundbegriffe für Kurven und Flächen nur in soweit eingeführt, wie sie für die Implementation von Relevanz sind. Der interessierte Leser sei auf [do 93] und auf die Kapitel 11 und 22 in [Far90] verwiesen. Zur Berechnung von Tangenten etc. bedient man sich der näherungsweisen Ableitung über Differenzenquotienten. Nach der Taylorschen Formel gilt für eine Funktion f , die in der Umgebung von t unendlich oft differenzierbar ist: 1 1 1 f (t + h) = f (t ) + h f 0 (t ) + h2 f 00 (t ) + h3 f (3) (t ) + h4 f (4)(t ) + ::: 2 6 4! Die erste bzw. die zweite Ableitung erhält man dann als Grenzwert des ersten und zweiten Differenzenquotienten für h ,! 0. ∆ f (t ) = f (t + h) , f (t , h) ; 2h ∆2 f (t ) = f (t + h) + f (t , h) , 2 f (t ) h2 Die Berechnung des Frenet’schen Dreibeins von Kurven und Flächen läßt sich dann mit Differenzenquotienten auf die Auswertungsfunktion zurückführen. 65 66 KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL Die erste Ableitung einer Raumkurve entspricht ihrer Tangente, der Differenzenquotient zeigt in Richtung zweier benachbarter Kurvenpunkte. Die zweite Ableitung steht im Grenzwert senkrecht dazu und zeigt in Richtung auf den Mittelpunkt des Krümmungskreises, das ist der Kreis, der die Kurven in einem bestimmten Punkt in zweiter Näherung berührt. Die euklidische Länge der zweiten Ableitung ist die Krümmung, für Kurven, die nach der Bogenlänge parametrisiert sind, ist das κ = j f 00 (t )j, ihre Inverse ρ = κ1 ist im Falle nichtverschwindender Krümmung gerade der Radius des Krümmungskreises.Für Kurven, die nicht nach der Bogenlänge parametrisiert sind, j f (t ) f (t )j erhält man die Krümmung durch κ(t ) = j f (t )j3 , ist dabei das Kreuzprodukt. 0 00 0 t t m m Die ersten beiden Ableitungen einer Kurve mit nichtverschwindender Krümmung sind linear unabhängig, durch Orthonormalisierung nach Gram-Schmidt erhält man die Basisvektoren des Frenet’schen Dreibeins in einem Kurvenpunkt. Das sind die Tangente t, der Hauptnormalvektor oder die Normale m, und der Binormalvektor b von f in t: t= f 0 (t ) j f 0(t )j ; m = bt; b= f 0 (t ) f 00 (t ) j f 0(t ) f 00(t )j Dabei ist zu beachten, daß in einem Punkt, wo eine zweidimensionale Kurve ihre Krümmungsrichtung ändert, die Krümmung null ist und der normalisierte Normalvektor schlagartig die Seite der Kurve wechselt. In solchen singulären Punkten der Ordnung 1 ist die Normale als Funktion von t also unstetig. Tangente und Normale spannen die Schmiegeebene der Kurve auf, bei planaren Kurven ist das gerade die Ebene. In Flächenpunkten kann man ebenfalls ein lokales Koordinatensystem definieren, allerdings im allgemeinen kein Orthonormalsystem. Und zwar bedient man sich hier der partiellen Ableitungen der Fläche in die Parameterrichtungen zusammen mit dem Normalvektor der Fläche. tu(u; v) = ∂s(u; v) ; ∂u tv (u; v) = ∂s(u; v) ; ∂v n(u; v) = tu tv In diese Definitionen werden in der Implementation in den Basisklassen die ersten bzw. zweiten Differenzenquotienten eingesetzt, um das Frenet’sche Dreibein zu berechnen. Für Kurven und Flächen erhält man damit insgesamt die gezeigten lokalen Richtungsvektoren. Koordinatensysteme sind hier stets rot, grün, blau für x-, y-, z-Achse, bzw. für Tangente, Hauptnormale, Binormale und für tu , tv , n dargestellt. Der Flächenpunkt in der Abbildung hat die Koordinaten (0:8; 0:1). 5.1. IMPLEMENTATION IN C++ 67 5.1 Implementation in C++ In diesem Kapitel werden die C++-Klassen im einzelnen beschrieben, die den Kern der GenmodImplementation bilden. Des weiteren wird die Kurven- und Flächenbibliothek vorgestellt und schließlich ihre Integration in Genmod mit Hilfe von Wrapper-Klassen vorgeführt. Es gilt dabei die MRT-Namenskonvention, abgeleitete Klassen verlängern die Namen ihrer Basisklassen. Das hat den Grund, daß OOP-gemäß abgeleitete Klassen Spezialisierungen ihrer Basisklassen darstellen. Dies gilt gleichermaßen für Namen von Template-Instantiierungen, so wird aus einer Basisklasse t_GmVar und einer abgeleiteten Templateklasse t_GmVarTemplate durch Instantiierung eine Klasse t_GmVarReal. 5.1.1 Der Genmod-Kern t_GmRegister t_GmAdmin t_GmAdminMap t_GmAdminType t_GmVarPtr t_GmMAP t_GmVar t_GmMapRebind t_GmMapCompose _DagOrder t_GmSignature t_GmMapConstTemplate<C> t_GmMapVarTemplate<C> t_GmVarTemplate<C> t_GmVarPtrTemplate<C,V,P> Dies sind die Kernklassen von Genmod. Sie zerfallen in verschiedene Kategorien, einmal Verwaltungsklassen, dann Klassen für Variablen und Klassen für Abbildungen. Die zentrale Klasse ist t_GmMAP, deren wichtigste Funktionen kurz vorgestellt werden sollen. class t_GmMAP : public t_GmVar { public: t_GmSignature& getBoundSignature(); // set return value, get input-signature to append input vals to t_GmMAP& operator() (t_GmMAP& in); // bind and check if everything is ready for execution t_Bool checkBinding (); // do what you are made for t_Bool execute (); // get return value, may be NULL if function has no return value const t_GmVarPtr& result (); // get rid of all input vars and maps void unbind () ; protected: virtual t_Bool bind (); virtual t_Bool evaluate () ; // =0; virtual t_Bool fillSignature () ; // =0; t_GmSignature& signature(const t_GmVarPtr& p); t_GmMapList evalMaps; t_GmSignature localVars; t_Bool bindingIsDone; }; Diese Klasse ist dazu entworfen, daß ein Anwendungsprogrammierer leicht eigene Abbildungen durch Ableitung erzeugen kann. Daher ist es wichtig, zu verstehen, wie das Zusammenspiel von t_GmMAP und einer abgeleiteten Klasse funktioniert. Als Beispiel soll die in 4.1.1 gezeigte Ableitung t_GmMapPt3DCross dienen. 68 KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL Abbildungsobjekte werden in der Regel mit Hilfe des Standardkonstruktors erzeugt. Als nächstes werden mit dem ()-Operator die Eingabeabbildungen gesetzt. Dabei wird erst sichergestellt, daß diese gebunden sind, weil sie dann erst Rückgabevariablen angelegt haben, Zeiger auf diese Abbildungen an die dynamische Liste der Eingabeabbildungen angefügt. Der nächste Schritt, das Binden, erfolgt, wenn es explizit durch checkBinding() angefordert wird, oder wenn ein execute()-Aufruf geschieht. Das Binden selber erfolgt ein einziges Mal in der zentralen Funktion bind(), die nur in Ausnahmefällen in abgeleiteten Klassen überschrieben wird, etwa bei t_GmMapCompose. In bind() wird die virtuelle Funktion fillSignature() aufgerufen, die, wie in 4.1.1 für t_GmMapPt3DCross vorgeführt, die Verbindung zwischen den privaten Variablen der abgeleiteten Klasse und dem Verwaltungsmechanismus der Basisklasse herstellt. Innerhalb von bind() wird nun ein Typabgleich zwischen der Liste der über operator() eingesammelten Eingabeabbildungen und der in fillSignature gesetzten Signatur durchgeführt. Dabei wird einerseits eine Rückgabevariable vom spezifizierten Typ gesetzt, der immer als erstes mit signature(Rückgabe) angegeben wird, andererseits werden die übergebenen lokalen Zeiger der abgeleiteten Klasse auf sinnvolle Werte gesetzt, nämlich auf die Inhalte der Rückgabevariablen der Eingabeabbildungen. Ist dies erfolgt, so wird bindingIsDone auf Wahr gesetzt, und fortan kann die Abbildung per execute() ausgeführt werden. Über result() kann der Wert der Berechnung dann ausgelesen werden, wenn das zurückgegebene Referenzpointer-Objekt dereferenziert wird. – Dadurch, daß die Verwaltungsarbeit automatisch beim Anfügen von Eingaben oder beim Ausführen selber erledigt wird, ist nicht nur das Schreiben von Ableitungen recht einfach, sondern auch im Rahmen des Möglichen die Benutzung: t_Real a=3,b=4; t_GmMapRealVar a0(&a); t_GmMapRealVar b0(&b); t_GmMapRealAdd p; p(a0)(b0); p.execute(); cout << "3+4=" << p.result() << endl; Bei der Einführung einer neuen Variablenklasse werden dann sechs Templates instantiiert, das Variablentemplate selber und zwei konstante Abbildungen zur Kommunikation mit C++Variablen, sowie drei Zeiger-Templates für die drei ersten Klassen mit Hilfe der von Martin Fischer in [Fis95a] vorgestellten Reference-Pointer. Das ermöglicht der Klasse t_GmSignature, die eigentlich nur eine Liste von Variablen hält, sich nicht um das Löschen angelegter Variablen oder Abbildungen kümmern zu müssen; solange diese noch referenziert werden, werden sie nicht gelöscht. Schließlich ist die Grundidee der Generativen Modellierung, daß sich ein Anwender eine Bibliothek eigener Werkzeuge definieren kann. Um dies zu unterstützen, können sich Abbildungen einen Namen geben, der an zentraler Stelle von t_GmRegister verwaltet wird. Man könnte sich vorstellen, daß ein interaktiver Modeller die Liste dieser Namen anbietet, aus denen sich ein Modellierer eine passende Abbildung sucht. Hat er eine eigene Abbildung zusammengestellt, wird eine Kopie dieser Verwaltungsinstanz zurückgegeben und gehört ebenfalls zum Repertoire, das man bei Bedarf benutzen kann. Dort werden neben Abbildungen aber auch Typen gehalten, beide Administrator-Klassen sind abgeleitet von der Basisklasse t_GmAdmin. 5.1. IMPLEMENTATION IN C++ 5.1.2 69 Die Kurven- und Flächenbibliothek Das cns-Paket ist unabhängig von Genmod entstanden, um eine Hierarchie von Kurven- und Flächenklassen zur allgemeinen Verwendung im MRT zu haben. Beispielsweise sind die parametrischen biquadratische Bézierflächen dazu verwendet worden, um ein MRT-Objekt zu definieren, das in msd-Dateien verwendet werden kann. Dazu wurde die allgemeine parametrische Fläche des cns-Paketes um einen Algorithmus zur Berechnung von Strahlschnitten ergänzt, das Bézierclipping-Verfahren [CS96], um die Strahlschnittanforderung von t_Object zu beantworten. Obwohl beide Pakete, Genmod und cns, aufeinander aufbauen, ist Genmod zur Übersetzung von cns nicht notwendig. Einerseits ist eine Trennung im Sinne einer Modularisierung, andererseits mußte im Rahmen dieser Arbeit eine Hierarchie gefunden werden, um eine wohldefinierte Basis zur Integration der vielen Repräsentationen von Kurven, Flächen und allgemeinen Transformationen der Computergrafik zu besitzen. Die vorhandenen Klassen sind daher mehr als repräsentativer Querschnitt denn als erschöpfende Implementation zu verstehen. Zudem ist analog zur Klasse t_3DVector des MRT-Paketes innerhalb des cns-Paketes eine Klasse t_PtRb entstanden, die auf balancierten Rot-Schwarz-Bäumen [CLR90] beruht. Damit ist es möglich, n-dimensionale Vektoren zu implementieren, die auch für große n günstiges Laufzeitverhalten zeigen, denn Zugriff auf eine Komponente kostet Zeit O(logk), k Anzahl der nichtverschwindenden Komponenten. n-dimensionale Vektoren sind so definiert, daß der Eintrag ungleich null mit dem größten Index die Dimension n des Vektors angibt, wobei ein Vektor niedrigerer Dimension als Teil des kanonischen Unterraums des Rn für n beliebig betrachtet wird. Praktisch heißt das, daß alle Komponenten xi mit i > n gleich null sind. Für diesen Vektortyp sind alle Operationen wie Addition, skalare Multiplikation etc. mit Ausnahme des dreidimensionalen Kreuzproduktes wohldefiniert. Dieses wurde nur für die ersten drei Komponenten zweier n-dimensionaler Vektoren definiert, damit eine Template-Kompatibilität zwischen t_PtRb und t_3DVector hergestellt werden konnte. Dadurch lassen sich Vektoren beiden Typs als Parameter in Templateklassen einsetzen, und je nach Anwendung kann man so einmal recht effizient in 3D rechnen, andererseits besitzt man aber auch die Möglichkeit, Kurven und Flächen im n-Dimensionalen zu betrachten. Differentialgeometrisch machen dort allerdings dreidimensionale lokale Koordinatensysteme wenig Sinn, immerhin funktioniert die Differentiation jedoch wie in der Einleitung beschrieben nach wie vor in n Dimensionen. Dazu sind die Kurven und Flächen zu Templateklassen geworden. Im folgenden wird dabei der für die 3D-Modellierung relevante Fall m; n = 3 betrachtet, also für Pt in diesem Fall t_3DVector eingesetzt. Ähnlich wie in der Haskell-Implementation hat man somit Kurven, Flächen und Funktionen als Abbildungen R ! R3, R2 ! R3 und R3 ! R3. Die Basisklassen von Kurven und Flächen benutzen die virtuelle Auswertungsfunktion evaluate() (siehe 4.1.1.1), um Ableitungen in numerischer Näherung zu berechnen. Die Funktionen zur Berechnung der ersten und zweiten Ableitung sind aber ebenfalls virtuell, um effizientere Ableitungsberechnungen in abgeleiteten Klassen zuzulassen. Insgesamt besitzt die Kurvenklasse die folgenden Memberfunktionen: evaluate deriv deriv2nd tangent mainNormal biNormal curvature length domain Die Funktion domain liefert ein Objekt vom Typ t_Interval, das ist der Parameterraum der Kurve. Die Krümmung wird nach der Formel in der Einleitung aus erster und zweiter Ableitung berechnet, die Länge einer Kurve c wird berechnet, indem über die Länge der Tangente integriert wird, falls l (t0) = t0, ist c nach der Bogenlänge parametrisiert: l (t0) = Z t0 0 jc0(t )j dt 70 KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL Die Flächenklasse enthält folgende Memberfunktionen. evaluate deriv deriv2nd gradient isTriangle normal tangent dirTangent twist area domain normCurvat gaussCurvat meanCurvat Die Ableitungsfunktion bekommt einen Boole’schen Wert, der sagt, ob in u oder in v-Richtung ∂s (u v) ∂s (u v) abgeleitet werden soll, der Gradient ist eine Zeile der Ableitungsmatrix ( i∂u ; i∂v ), dirTangent berechnet die Richtungsableitung, und der Twist ist die gemischte zweite Ableitung suv einer Fläche s. Die Fläche über einem Intervall I J ist: ; Area(I J ) = Z Z I J q ; susu sv sv , (susv )2 du dv Gauß’sche, mittlere und Normalkrümmung können mit Hilfe der zweiten Ableitungen einer Fläche berechnet werden. — Zusätzlich besitzen Flächen eine virtuelle Funktion, die angibt, ob ihr Parameterbereich drei- oder viereckig ist. Das zweidimensionale Standardintervall ist [0; 1] [0; 1], bei dreieckigen Flächen ist zusätzlich u + v 1. Die folgende Übersicht gibt einen Eindruck von der Hierarchie der implementierten Kurven und Flächen. t_CurveParametric<Pt> t_Function<Pt> t_SurfaceParam<Pt> t_CurveLine<Pt> t_FunctRotQuat<Pt> t_SurfaceBicubicBezier<Pt> t_FunctionTurbulence<Pt> t_SurfaceBezierTri<Pt> t_FunctTensor<Pt> t_SurfaceCoons<Pt> t_CurveTensor<Pt> t_CurveBSpline<Pt> t_CurveBezier<Pt> t_SurfaceCoonsTri<Pt> t_SurfaceSphere<Pt> t_SurfaceTensor<Pt> Eine besondere Rolle in der Geometrischen Modellierung spielen Tensorprodukt-Kurven und -Flächen. Bézierkurven und B-Splines beispielsweise fallen in die Kategorie der Tensorkurven. Diese zeichnen sich dadurch aus, daß man skalare Basisfunktionen A0(t ); :::; An(t ) über einem reellen Intervall I und Kontrollpunkte p0 ; ::: pn 2 R3 hat, sodaß sich die Kurve in parametrischer Form als ein Skalarprodukt eines reellen mit einem Punktvektor darstellen läßt: c(t ) = ∑ni=0 Ai (t ) pi Damit sich der Verlauf der Kurve intuitiv steuern läßt, verlangt man, daß Kurvenpunkte affine Konvexkombinationen der Kontrollpunkte sind. Das bedeutet, die Basisfunktionen sind stets positiv und eine Partition der Eins: Für alle t 2 I gilt, daß Ai (t ) 0 für i = 1; :::; n ist und ∑ni=0 Ai (t ) = 1. Verallgemeinert man diese univariate auf bi- oder trivariate Darstellungen, so erhält man das Tensorprodukt als verallgemeinertes Skalarprodukt. Das bedeutet, man hat im bivariaten Fall ein zweidimensionales und im trivariaten Fall ein dreidimensionales regelmäßiges Gitter von Kontrollpunkten. Man kann Tensorproduktflächen folgendermaßen mit einer Matrix von Kontrollpunkten 5.1. IMPLEMENTATION IN C++ 71 darstellen: n m s(u; v) = ∑ ∑ Ai (u) B j (v) pi j = (B0(v); :::; i=0 j=0 0 B Bm(v)) @ p00 .. . pm0 .. . p0n .. . 1 0 A (u) 1 0 C AB @ ... C A pmn An(u) Gemischte Tensorproduktflächen können dabei als Ai die Bernsteinpolynome der Bézierkurven und als Bi die B-Spline-Basisfunktionen benutzen. – Ein trikubisches Tensorproduktvolumen mit Basisfunktionen Ai (x); B j (y); Ck(z) dagegen sieht in Summenschreibweise so aus: l m v(x; y; z) = ∑ ∑ n ∑ Ai(x) B j (y) Ck (z) pi jk i=0 j=0 k =0 Die Auswertung einer bikubischen Funktion kann man so interpretieren, daß zunächst vier Kurven in u ausgewertet werden, diese vier Punkte nimmt man als Kontrollpunkte für die kubische v-Kurve. Entsprechend kann man eine trikubische Funktion so interpretieren, daß zunächst die Kontrollpunkte einer bivariaten Fläche berechnet werden. Die Auswertung kann so visualisiert werden. Die Auswertung einer k-variaten Tensorproduktfunktion kann man folglich auf die Auswertung einer (k , 1)-variaten Funktion zurückführen. Genau diese Rekursion wird nun bei t_FunctTensor benutzt, es gibt einen Konstruktor, der ebenfalls eine Tensorfunktion erhält, zusammen mit einer Tensorproduktkurve, die für die Auswertung der Kontrollpunkte benutzt wird, und einer Information über den Rekursionsindex, damit die zur Interpolation zu benutzende Koordinate aus dem Parametervektor herausprojiziert werden kann. class t_FunctTensor : public t_Function<Pt> { public: t_FunctTensor (t_CurveTensor<Pt>& evalCrv, t_FunctTensor<Pt>& changeTensor, t_PtrList* contrPt); virtual Pt& evaluate (const Pt& t) ; virtual t_Bool setControlPoints (t_PtrList* contrPt); protected: t_ListDl<Pt*>* cp; int parameterIndex; t_CurveTensor<Pt>* curve; t_FunctTensor<Pt>* tensor; t_PtrList* cPt; }; Auf diese Weise genügt es, eine virtuelle Basisklasse für Tensorprodukte zu definieren, um die Verwaltung des Kontrollpunktvektors an zentraler Stelle zu erledigen. Eine Tensorfunktion hat KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL 72 im univariaten Fall Zugriff auf eine Liste von Punkten, im bivariaten auf eine Liste von Liste von Punkten und so weiter. Eine Tensorproduktfläche ist daher eine Spezialisierung einer allgemeineren Funktion und erhält im Konstruktor zwei Tensorproduktkurven, die im wesentlichen nur die zu benutzenden Basisfunktionen repräsentieren. Neben den beiden implementierten Kurven gibt es noch eine ganze Reihe weiterer Verallgemeinerungen von Splines, wie Beta-Splines [BBB87], ν-Splines und τ-Splines [HL92]. Eine B-Spline-Kurve ist nicht nur durch den Kontrollvektor gegeben, sondern es gehört auch eine Aufteilung des der Kurve zugrundeliegenden Parameterinterintervalles I = [t0; tn+k] zur Definition, der Knotenvektor T = (t0; :::; tn+k) (nicht streng) aufsteigender reeller Zahlen. Dabei ist k der im allgemeinen niedrige Grad (bei kubischen Kurven 4), n die Anzahl der Punkte des Kontrollpolygons. Die Definition der Basisfunktionen geschieht über die bekannte Boehm-Rekursion: Nik (t ) = t , ti ti+k,1 , ti Nik,1(t ) + ti+k , t k,1 N (t ) ; Ni1 (t ) = χ[ti ti+1 ] ; i = 0; :::; n + k , 1 ti+k , ti+1 i+1 ; Eine Basisfunktion Nik hat dabei als Träger das Teilintervall [ti; ti+k ], daher haben für ein t 2 [ti; ti+1] nur die Kontrollpunkte pi,k+1; :::; pi Einfluß auf die Form der Kurve (Lokalitätseigenschaft von B-Splines). Diese Kontrollpunkte werden dann im de-Boor-Algorithmus benutzt, um die affine Kombination dieser Punkte zu bilden. Der Aufwand dieses Algorithmus’ ist O(k2), also quadratisch im i. a. niedrigen Grad. Ein Problem besteht bei der Verwendung dynamischer Datenstrukturen wie verketteter Listen daher im schnellen Finden des entsprechenden Index. In der vorliegenden Implementierung ist dies dadurch gelöst, daß die gleichen Rot-Schwarz-Bäume für die Knotenvektoren benutzt werden, wie sie bei den n-dimensionalen Punkten verwandt werden. Diese gestatten das Finden des richtigen Intervalls in logarithmischer Zeit. Zudem enthalten sie im gefundenen Intervall nicht den Parameterindex, sondern einen Listeniterator [SL95], der in die doppelt verkettete Liste der Kontrollpunkte zeigt, sodaß der de-Boor-Algorithmus gleich an der richtigen Stelle pi,k+1 aufsetzt. Damit erhält man insgesamt die sehr guten Auswertungszeiten, die B-Splines mit vielen Kontrollpunkten schneller, beherrschbarer und im allgemeinen auch genauer als Bézierkurven machen. Die Verwendung eines dynamischen Knotenvektors ist wichtig, um ein schnelles Einfügen von Kontrollpunkten etwa mit Hilfe des Oslo-Algorithmus [Far90] zu ermöglichen, wobei auch im Knotenvektor lokale Einfügungen entstehen. Zusätzlich zur beschriebenen allgemeinen Implementation von Tensorproduktflächen mit Tensorkurven beliebigen Grades ist ebenfalls eine fest bikubische Version der Bézierflächen implementiert, die einen Vektor von sechzehn Kontrollpunkten erhält und deren Auswertungsfunktion stark optimiert ist, indem sämtliche Schleifen “ausgerollt” wurden. Diese Klasse enthält zusätzlich Funktionen, um die Fläche an einem u bzw. an einem v-Wert in zwei oder vier Einzelflächen aufzuteilen und die entstehenden Kontrollpunkte zurückzugeben. Diese Funktionen wurden ebenfalls optimiert, da sie die Grundoperation im erwähnten Bézierclipping-Algorithmus zum Strahlschnitt darstellen. Die Basisfunktionen n-ten Grades Bnij der Bézierkurven, die Bernsteinpolynome, haben eine Verallgemeinerung auf baryzentrische Dreieckskoordinaten. Neben v = 1 , u und i + j = n kann man nämlich auch w = 1 , u , v und i + j + k = n zerlegen: Bnij (u; v) = n! i j uv ; i! j! Bnijk (u; v; w) = n! ui v j wk i! j!k! Betrachtet man positive (u; v) mit u + v 1, so ist w 0 und man kann eine Dreiecksfläche parametrisieren, [HL92], Kapitel 6.3.2, mit Kontrollpunkten pi jk mit i + j + k = n. Neben diesen Bézierdreiecken ist der andere implementierte dreieckige Flächentyp das Dreiecks-Coons-Patch. 5.1. IMPLEMENTATION IN C++ 73 Coons-Patches existieren ebenfalls in der viereckigen Form und zeichnen sich dadurch aus, daß diese Flächen alleine durch die Angabe von Randkurven schon definiert werden, [Far90], Kap. 21. Es wird dabei ein bi- bzw. trilineares Blending durchgeführt, das sich durch sogenannte Boole’sche Summen ausdrücken läßt, wenn man die Randkurven einer Fläche p(u; v) in der Form p(u; 0); p(u; 1); p(0; v) und p(1; v) bzw. p(0; t ; 1 , t ) etc. ausdrückt. Die Randkurven müssen allerdings die naheliegende Forderung erfüllen, daß sie sich in den Ecken treffen, damit man von Eckpunkten wie p(0; 0) sprechen kann. Die Abbildungen in der Einführung zu diesem Kapitel auf Seite 66 stellen beispielsweise ein viereckiges Coonspatch dar. Zu guter Letzt existieren in cns noch eine Kugel, parametrisiert in Polarkoordinaten, eine Strecke zwischen zwei Punkten und eine Turbulenzfunktion nach der üblichen Formel [WW92], 7.2.2: k turbulence(u; v) = ∑ abs i=0 1 2 noise(2iu; 2i v) i : Die periodische noise-Funktion ist in diesem Fall über eine Tensorproduktfläche als zufälliges Höhenfeld einstellbaren Grades implementiert. 5.1.3 Integration von Kurven und Flächen in Genmod Um mit Kurven und Flächen generativ modellieren zu können, werden nun der Genmod-Kern einerseits und die Kurven und Flächen andererseits mit Hilfe der vorgestellten Templateklassen kombiniert. Zunächst werden dazu die Variablenklassen definiert, dann die wesentlichen Memberfunktionen in Abbildungen verpackt. Aus diesen lassen sich Anwendungen gewinnen, die im abschließenden Bericht über Erfahrungen mit Genmod in Kapitel 6 besprochen werden. 5.1.3.1 Variableninstanzen long t_Real t_3DVector t_PtRb t_BRep t_CurveParametric3D t_CurveBezier3D t_CurveMap3D t_CurveBSpline3D t_CurveLine3D t_SurfaceParam3D t_SurfaceTensor3D t_SurfaceMap3D t_SurfaceSphere3D t_SurfaceCoons3D Für diese Klassen werden sechs Template-Instanzen erzeugt, die durch typedef nach folgendem Muster umbenannt werden: typedef t_GmVarTemplate<t_Real> t_GmVarReal; typedef t_GmMapVarTemplate<t_Real> t_GmMapRealVar; typedef t_GmMapConstTemplate<t_Real> t_GmMapRealConst; Insbesondere ist hier von Bedeutung, daß die Ableitungshierarchie zwischen Variablenklassen ebenfalls spezifiziert werden kann. Mit Hilfe der Runtime-Type-Information [Fis95b] kann die Typüberprüfung beim Binden von Abbildungen in der Weise erfolgen, daß eine Abbildung, die beispielsweise eine parametrische Kurve auswertet, eine Variable vom syntaktisch unterschiedlichen Typ t_CurveBezier3D erhalten kann. Variablenklassen müssen den Anforderungen genügen, sich per <<-Operator auf einen Stream ausgeben zu können, sie müssen über einen Standard- und einen Copykonstruktor verfügen und Zuweisung per =-Operator zulassen. Die im vorigen Abschnitt vorgestellten Kurven und Flächen sind hier in ihrer dreidimensionalen Version enthalten. Daneben gibt es zwei weitere Typen, um Abbildungen der entsprechenden 74 KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL Signaturen in eine Kurve oder Fläche zu verpacken, t_CurveMap ist von t_CurveParametric und t_SurfaceMap ist von t_SurfaceParam abgeleitet, die jeweiligen Konstruktoren erhalten Objekte vom Typ t_GmMAP. Ableitungen werden für diese Klassen näherungsweise bestimmt. Für das einfache Rechnen mit Fließkommazahlen wurden die üblichen arithmetischen Verknüpfungen und die Funktionen der Mathematikbibliothek in Abbildungen übersetzt. t_GmMapRealAdd t_GmMapRealAssign t_GmMapRealCeil t_GmMapRealCos t_GmMapRealDiv t_GmMapRealExp t_GmMapRealFabs t_GmMapRealFloor t_GmMapRealInverse t_GmMapRealLog t_GmMapRealMinus t_GmMapRealMult t_GmMapRealNegate t_GmMapRealSin t_GmMapRealTan Zum Umgang mit dreidimensionale und n-dimensionalen Vektoren existieren neben Addition, Subtraktion, Skalarprodukt usw. auch Funktionen, um reelle Zahlen zu Vektoren zusammenzusetzen und sie aus Vektoren herauszuprojizieren. Erwähnenswert wäre noch, daß auch die Projektion als Operation implementiert ist, die zu v, w die Projektion von w auf v berechnet und ein Vielfaches von v zurückgibt: proj(v; w) = v; w > v; < v; v > < t_GmMapPt3DAdd t_GmMapPt3DConcatReal t_GmMapPt3DCoord t_GmMapPt3DCross t_GmMapPt3DDot t_GmMapPtRbAdd t_GmMapPtRbConcatPtRb t_GmMapPtRbConcatReal t_GmMapPtRbCoord t_GmMapPtRbCross wegen v; w > v,w v = 0 < v; v > < t_GmMapPt3DLength t_GmMapPt3DMinus t_GmMapPt3DNegate t_GmMapPt3DNormalize t_GmMapPt3DProject t_GmMapPtRbDot t_GmMapPtRbLength t_GmMapPtRbMinus t_GmMapPtRbNegate t_GmMapPtRbNormalize t_GmMapPt3DScalDiv t_GmMapPt3DScalMult t_GmMapPt3DSet t_GmMapPt3DSpat t_GmMapPtRbProject t_GmMapPtRbScalDiv t_GmMapPtRbScalMult t_GmMapPtRbSet t_GmMapPtRbSpat Die wesentlichen Memberfunktionen von Kurven und Flächen bilden reelle Parameter auf Vektoren ab, insbesondere existieren Abbildungen, die statt eine Abbildung auszuwerten diese Abbildung in ein t_CurveMap bzw. ein SurfaceMap-Objekt verwandeln und dieses als Rückgabewert liefern. Um aus Randkurven eine Fläche zu bilden, kann man Coons-Flächen verwenden, die Sitzfläche des Stuhles aus Beispiel 6.1.7 ist beispielsweise auf diese Weise entstanden. Weiterhin existiert ein Werkzeug, um zwischen Kurven linear zu interpolieren, und um eine Liste von Punkten einer Bézierkurve als Kontrollpolygon zu übergeben. t_GmMapCurveEval t_GmMapCurveMap t_GmMapCurveTangent t_GmMapCurveXNormal t_GmMapCurveYNormal t_GmMapSurfaceEval t_GmMapSurfaceMap t_GmMapSurfaceNormal t_GmMapSurfaceSTangent t_GmMapSurfaceTTangent t_GmMapCurvesToCoonsSurf t_GmMapInterpolateCurves t_GmMapPointsToBezier t_CurveMap t_SurfaceMap Um Kurven und Flächen im interaktiven Viewer zu betrachten, gibt es die folgenden Abbildungen, die ein Sampling durchführen und deren Rückgabewert ein BRep ist. Dies ist als ein Beispiel für beliebige Tesselierungsklassen, auch eine adaptive Triangulierung einer Oberfläche läßt sich als Abbildung auffassen. t_GmMapSampleCurveBRep t_GmMapSampleSurfBRep t_GmMapShowSceneSimple t_GmObject 5.2. IMPLEMENTATION IN HASKELL 75 Das MRT-Objekt t_GmObject kann die approximative Darstellung auf das Sampling zurückführen. Eine Lösung für das Strahlschnittproblem mit allgemeinen parametrischen Flächen, so wie sie beim Generativen Modellieren entstehen, ist ein wesentlich härteres Problem. Obwohl im Laufe der Entwicklung verschiedene Algorithmen getestet wurden, ist keine in allen Fällen zufriedenstellende Lösung gefunden worden. – Das Grundproblem ist das Lösen einer impliziten Gleichung durch ein Iterationsverfahren. Dies wurde zwar erfolgreich implementiert, das hauptsächliche Problem ist aber das Finden eines geeigneten Startpunktes. Es muß ein Punkt im Parameterraum (ustart ; vstart ; tstart ) gefunden werden, der durch Iteration garantiert zu dem Schnittpunkt von Fläche und Strahl (uschnitt; vschnitt; tschnitt) konvergiert. Es wurden eine Reihe von Startpunktheuristiken ausprobiert, doch keine lieferte ein robustes Lösungsverhalten. Um aber sicherzugehen, daß sich ein bestimmter Startpunkt nicht in einem lokalen Minimum der Zielfunktion verfängt, benötigt man letztendlich Inklusionsfunktionen wie bei Snyders Ansatz. 5.2 Implementation in Haskell 5.2.1 Basisklassen Vector3 Curve Surface Transform Dies sind alle Klassen, die notwendig sind, um in Haskell generativ zu modellieren. Spezielle Implementationen von Kurven oder Flächen sind dagegen in Haskell-Terminologie Instanzen. Diese müssen jedoch nur die jeweiligen Auswertungsfunktionen überschreiben, die restliche Funktionalität kann mit Defaultimplementationen bereits von der Klasse generisch erledigt werden. Dazu zählt insbesondere die Ableitungsberechnung, die nach dem eingangs geschilderten Verfahren von der Basisklasse in numerischer Näherung durchgeführt wird. Ähnlich wie in C++ kann eine Instanz diese Funktionen aber auch selber überschreiben, wenn es eine effizientere oder genauere Möglichkeit dazu in einem Spezialfall gibt. In diesem Sinne sind sämtliche Funktionen, die in einer Haskell-Klasse definiert werden, in OOP-Terminologie virtuelle Funktionen. Von einem Überschreiben von Ableitungen wurde für die Instanzen in der vorliegenden Implementation allerdings Abstand genommen, vielmehr wurde der Schwerpunkt auf eine reichhaltige Bibliothek von Operatoren gelegt. Bei der Vorstellung von Haskell in 4.2.1 wurde deutlich, warum die Implementation der Generativen Modellierung in einer funktionalen Sprache wesentlich knapper ausfallen kann als in C++: Die Grundkonzepte, die bei der C++-Implementation den Kern von Genmod ausmachen, sind bereits integraler Bestandteil der Sprache. – Der Genmod-Kern in C++ ist auf große Allgemeinheit ausgelegt, um den elementaren Anforderungen des objektorientierten Designs zu genügen, insbesondere mußte auf die Implementation der Snyderschen Operatormethode zur Bestimmung einer Inklusionsfunktion eines Operators verzichtet werden. - Die Haskell-Implementation kann aber gerade dies zum Beispiel für die Operatormethode “Bestimmung der Ableitungsfunktion” leisten. Anders als in GENMOD ist dazu keine Unterscheidung zwischen Operatoren und Operatormethoden notwendig! - In einer funktionalen Sprache ist es durchaus möglich, daß ein Objekt eine Funktion liefert, die dynamisch zusammengebaut wird, zum Beispiel eine Ableitungsfunktion. Für die Bestimmung der Ableitung der Summe zweier Kurven wäre daher folgendes denkbar: data (Curve c, Curve d) => CurveAdd c d = CrvAdd c d instance (Curve c, Curve d) => Curve (CurveAdd c d) where derivative (CrvAdd crv1 crv2) = CrvAdd (derivative crv1) (derivative crv2) KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL 76 Der Rückgabewert von derivative ist also selber wieder eine Kurve. – Grenzen findet ein solcher Mechanismus allerdings in der Verwendung von allgemeinen Funktionen als Kurven, Flächen und Transformationen wie weiter unten in CrvFct, SrfFct und TrnFct vorgestellt. Daher müßte bei der Einführung weiterer Operatormethoden untersucht werden, ob es entweder möglich ist, ohne diese Instanzen auszukommen, oder eine Operatormethode für diese Instanzen ebenfalls generisch zu implementieren. In den folgenden Unterabschnitten werden zunächst die Klassen im einzelnen vorgestellt und dann die implementierten Instanzen aufgezählt. Die Implementation dieser Instanzen besteht jeweils nur aus wenigen Zeilen Haskell-Code, daher wurde es möglich, die gesamte Implementation im Anhang A auf neun Seiten unterzubringen. An dieser Stelle soll im wesentlichen ein Überblick gegeben werden. 5.2.1.1 Vektoren add sub prod cross mult x y z normalize absLen Die Funktionen prod und mult sind Skalarprodukt und skalare Multiplikation. Der Vollständigkeit halber sei noch der Konstruktor erwähnt: data Vector3 = V3 Float Float Float 5.2.1.2 Kurvenklasse class Curve c tangent mainNormal biNormal evalCurve derivOne derivTwo diff_1 diff_2 where :: c -> :: c -> :: c -> :: c -> :: c -> :: c -> :: c -> :: c -> Float Float Float Float Float Float Float Float -> -> -> -> -> -> -> -> Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 diff_1 crv t = sub (evalCurve crv (t+diffQuot_h)) (evalCurve crv (t-diffQuot_h)) diff_2 crv t = sub (add (evalCurve crv (t+diffQuot_h)) (evalCurve crv (t-diffQuot_h))) (add p p) where p=evalCurve crv t derivOne crv t = mult diffQuot_2h (diff_1 crv t) derivTwo crv t = mult diffQuot_h2 (diff_2 crv t) tangent crv t = normalize (diff_1 crv t) biNormal crv t = normalize (cross (diff_1 crv t) (diff_2 crv t)) mainNormal crv t = normalize (cross (cross d1 d2) d1) where d1 = diff_1 crv t d2 = diff_2 crv t Die Funktionen diff_1 und diff_2 sind dabei aus Effizienzgründen vorhanden, tangent spart durch die Verwendung von diff_1 eine skalare Multiplikation. Die Konstanten diffQuot_h, diffQuot_2h und diffQuot_h2 für die Differenzenquotienten stehen 1 für die Zahlen h, 2h und h12 . 5.2. IMPLEMENTATION IN HASKELL 77 Es wird angenommen, daß die Auswertung einer Kurve über dem Standardintervall [0; 1] stattfindet, eine explizite Überprüfung wird nicht vorgenommen. Zudem gilt für alle implementierten Kurven, daß sie auch außerhalb des Standardintervalls definiert sind. 5.2.1.3 Flächenklasse class Surface tangentU tangentV normal evalSurf tangentU tangentV normal s where :: s -> :: s -> :: s -> :: s -> srf u v srf u v srf u v Float -> Float -> Vector3 Float -> Float -> Vector3 Float -> Float -> Vector3 Float -> Float -> Vector3 = tangent (CrvKeepV srf v) u = tangent (CrvKeepU srf u) v = normalize $ cross (tangentU srf u v) (tangentV srf u v) Die Implementation greift hier auf zwei Instanzen der Kurvenklasse zurück, die einen Parameter fixieren; die Ableitungsvektoren dieser Kurven sind dann gerade die partiellen Ableitungen dieser Kurven. Die Auswertung findet auch hier über dem Standardintervall [0; 1] [0; 1] statt, ebenfalls ohne Überprüfung. Auch hier können die implementierten Klassen auch außerhalb des Standardintervalls benutzt werden. 5.2.1.4 Transformationsklasse class Transform t where evalTrans :: t -> Vector3 -> Vector3 Transformationen besitzen die allgemeinste Definition. Auf die Berechnung der Ableitungen wurde hier verzichtet, weil Transformationen im allgemeinen nur in statischer Weise benutzt werden. – Transformationen sind für alle dreidimensionalen Vektoren definiert. 5.2.2 Instanzen der Basisklassen CrvX CrvSegment CrvEllipse CrvCircle CrvBezier CrvReparam CrvReverse CrvKeepU CrvKeepV CrvFct CrvTransform CrvInSurf CrvInterpol Identische X-Kurve, Strecke, Ellipse gegeben durch Mittelpunkt und Richtungsvektoren, Standardkreis und kubische Bézierkurve sind übliche Kurvenprimitive. Die Auswertungsrichtung einer Kurve kann durch die Transformation t 7! (1 , t ) umgedreht werden, Reparametrisierung einer Kurve durch eine andere bedeutet: evalCurve (CrvReparam f g) t = evalCurve f (x (evalCurve g t)) Zudem kann eine beliebige Funktion der Signatur R ! R3 in eine Kurve umgewandelt werden, und eine Transformation angewandt auf eine Kurve ergibt wieder eine Kurve. Die letzten beiden Klassen dienen dazu, eine Kurve in den Parameterraum einer Fläche zu legen, und um die lineare Interpolation zu einem festen Interpolationswert zwischen zwei gegebenen Kurven als neue Kurve zu gewinnen. 78 KAPITEL 5. IMPLEMENTATIONEN VON GENMOD IN C++ UND HASKELL SrfXY SrfSphere SrfSphereSegment SrfBezier SrfInterpolCrv SrfCoons SrfRailProd SrfWireProd SrfFct SrfTransform SrfParamCrv Identische xy-Ebene, Kugel in Polarkoordinaten und viereckiges Kugelsechstel sind übliche Primitive; eine Abbildung mehrerer Kugelsechstel findet man in Beispiel 6.2.4 auf Seite 90. Bikubische Bézier-Flächen werden in eleganter Weise definiert, indem man vier kubische Bézierkurven direkt aus den Kontrollpunkten gewinnt, diese auswertet und daraus eine neue Bézierkurve konstruiert. Dieses Vorgehen ist vergleichbar mit den Tensorflächen im cns-Paket, aber nicht so allgemein. Lineare Interpolationsflächen zwischen Kurven und Coons-Patches sind Werkzeuge, um Kurven zu Flächen zu kombinieren. Dazu gehören auch rail product und wire product, die in Beispiel 6.2.7 dargestellt werden. Schließlich existieren noch die Darstellung einer Funktion R2 ! R3 als Fläche und die Transformation einer Fläche, die wieder eine Fläche ist. Zudem kann man auch eine Kurve, die zusätzlich von einem reellen Parameter abhängig ist, als Fläche auffassen. Der Konstruktor erhält also eine Funktion: data (Curve c) => SurfParamCrv c = SrfParamCrv (Float->c) TrnCmps TrnFct TrnScale TrnTranslate TrnRotXY TrnRotYZ TrnRotZX Neben den Standardtransformationen existiert die Darstellung einer Funktion R3 ! R3 als Transformation, und die Komposition zweier Transformationen liefert wieder eine Transformation. Transformationen können also zunächst nicht per Infix-Operator komponiert werden, sondern werden hintereinandergeschrieben: evalTrans (TrnCmps trn2 trn1) p = evalTrans trn2 (evalTrans trn1 p) Die Rotationstransformationen erhalten einen Drehwinkel nicht in Grad oder Radian, sondern ebenfalls aus [0; 1], dem allgegenwärtigen reellen Standardintervall. Die Skalierung erhält einen dreidimensionalen Vektor, der die Streckung in die Koordinatenrichtungen beschreibt, man erhält die identische Skalierung also folgendermaßen: trnId = TrnTranslate (V3 1 1 1) line heightfield showSurface showCurve showSurfaces showCurves Neben der bereits in 4.2.2 gezeigten Strecke gibt es als Hilfsfunktion das Höhenfeld, das ein regelmäßiges Gitter von 4 4 Punkten auf der xy-Ebene verteilt und nur die sechzehn Höhenwerte übergeben bekommt, die es braucht, um daraus eine bikubische Bézierfläche zu machen. Das Höhenfeld existiert in zwei Versionen, einmal bedeckt es [,1; 1] [,1; 1] auf der Grundebene und als heightfieldNrm das Standardintervall [0; 1] [0; 1]. Die Funktionen showCurve und showSurface führen ein regelmäßiges Sampling durch, führte man eine echten MRT-Integration von Haskell-Genmod durch, wäre denkbar, daß sie zum Überführen einer Kurve oder einer Fläche in ein MRT-Objekt dienen, das ähnlich wie beim GenmodPaket nach wie vor Zugang zu der Auswertungsfunktion besitzt, um Approximationen beliebig zu verfeinern und Strahlschnitte durchführen zu können. – Die zweiten Versionen, showCurves und showSurfaces bekommen jeweils eine Kurve bzw. eine Fläche, die noch von einem zusätzlichen reellen Parameter abhängig sind. Für diesen Parameter erfolgt ebenfalls ein Sampling, das ermöglicht die Ausgabe einer Schar von Kurven oder Flächen wie beim Zaun-Beispiel 6.2.5. Kapitel 6 Beispiele und Erfahrungsbericht In diesem Kapitel sollen konkrete Beispiele für Generatives Modellieren vorgestellt werden. Es ist für das Verständnis der theoretischen Überlegungen in den vorangegangenen Kapiteln wichtig, deutlich zu machen, daß hier ein praktischer Modellieransatz erarbeitet wurde, der beim Modellieren im Großen einen wirklichen Fortschritt gegenüber der existierenden Technologie darstellt. Man muß von den einfachen Beispielen einzelner Flächen abstrahieren, um eine Vorstellung davon zu bekommen, wie ein Modellierer bei realistischen Problemstellungen von einem generativen Modeller profitiert. Ein Stuhl, ein Lattenzaun, eine Wendeltreppe werden nicht konkret modelliert und dann nur noch per cut&paste weiterverwendet, sondern ein Modellierer macht sich den Aufbau eines Modells klar, überlegt sich, welche Größen variabel und welche fest sind, um daraus schließlich ein Werkzeug machen, das fortan in konkreten Konstruktionen als parametrisiertes Modell verwendet werden kann. Die Genmod-Beispiele zeigen die Tragfähigkeit der C++-Implementierung, das Funktionieren des implementierten Abbildungsmodells und der parametrischen Kurven und Flächen, auch wenn hier nur auf C++-Ebene modelliert worden ist. Die Haskell-Beispiele geben einen Eindruck davon, wie eine Beschreibung in einer Datei für ein gegebenes Beispiel aussehen könnte. Gleichzeitig vermittelt die knappe textuelle Beschreibung, die in Haskell möglich ist, einen Eindruck davon, welche Schritte in einem interaktiven Modeller denkbar sind, um eines der Beispielmodelle zu konstruieren, deren Beschreibung hier gegeben wird. 6.1 Genmod-Beispiele Das Vorgehen bei den Beispielen ist so, daß C++-Funktionen geschrieben wurden, die Operatoren repräsentieren. Die Abbildungen in diesen Funktionen produzieren Objekte vom Typ parametrische Kurve bzw. Fläche. Um diese darzustellen, werden daraus t_GmObject-Objekte gemacht, die von der MRT-Basisklasse t_SurfaceObject abgeleitet sind und folglich in einen Szenengraphen eingetragen und dargestellt werden können. Diese Funktionen wurden dann in einem zweiten Schritt in Klassen verpackt, die man von der Basisklasse t_GmDemo ableitet. Diese stellt Funktionen checkinCurve() und checkinSurface() für die produzierten Kurven und Flächen zur Verfügung, eine bestimmte Demo besteht dann aus den dazu erzeugten MRT-Objekten. Die entsprechenden Zeilen in einer msd-Datei lauten dann zum Beispiel: GENMOD GENMOD 2 2 "Sinwave" 0 // spiral "Sinwave" 1 // wave 79 KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT 80 6.1.1 Sinusfläche mit Spirale Hier wird einmal eine Sinusfläche und zum anderen eine Spirale dargestellt, nach diesen Formeln: 0 1 4 (s , 0 5) sinwave(s t ) = @ sin(2 π (1 , s)) + cos(2 π t ) A 0 cos(12 t ) 1 A spiral(t ) = @ t ; ; 4 (t , 0; 5) ; sin(12 t ) Wie erläutert erhält eine zusammengesetzte Abbildung als ersten Parameter die letzte auszuwertende Abbildung eines DAGs. Dann werden deren Eingabeabbildungen zurückverfolgt und so kann der gesamte DAG kopiert werden. – Dieser Prozeß kommt dadurch zum Halten, daß der Beginn des DAG aus konstanten Abbildungen besteht, die selber keine Eingabeabbildungen haben. Das bedeutet aber, daß die zusammengesetzte Abbildung keine eigenen Parameter mehr hat. Um dies zu verhindern, kann man einer t_GmMapCompose noch weitere Parameter anfügen. Diese werden dann als die Eingaben einer zusammengesetzten Abbildung benutzt. Im abgedruckten Beispiel übernehmen die reellen konstanten Abbildungen s und t die Rolle der Parameter der Fläche sinwave, sie werden compose als Eingabeparameter übergeben. Die zusammengesetzte Abbildung bildet somit zwei reelle Zahlen auf einen Vektor ab und kann als solche in eine t_SurfaceMap verpackt werden. Dabei wird ein Rebinding durchgeführt, denn die Memberfunktion Pt& evaluate(t_Real s, t_Real t) der parametrischen Fläche muß die Eingabe der zusammengesetzten Abbildung auf die eigenen Parameter umlenken. Die ursprünglichen Parameter s und t spielen also die Rolle von Dummy-Variablen, die nur dem Zweck dienen, eine korrekt gebundene zusammengesetzte Abbildung zu erzeugen. void t_GmDemoSinwave::execute() { t_Real realFour=4.0, realMtwo=-2.0, t_GmMapRealConst s,t; t_GmMapRealConst t_GmMapRealConst t_GmMapRealConst t_GmMapRealMult t_GmMapRealMult t_GmMapRealMult t_GmMapRealMult t_GmMapRealAdd t_GmMapRealAdd t_GmMapRealMinus t_GmMapRealSin realTwopi=2.0*M_PI; four(&realFour); mtwo(&realMtwo); twopi(&realTwopi); fourS; fourS fourT; fourT s2pi; s2pi t2pi; t2pi x; x z; z tpiMs2pi; tpiMs2pi sinus; sinus (t)(four); (s)(four); (t)(twopi); (s)(twopi); (mtwo)(fourS); (mtwo)(fourT); (twopi)(s2pi); (tpiMs2pi); 6.1. GENMOD-BEISPIELE 81 t_GmMapRealCos t_GmMapRealAdd t_GmMapPt3DConcatReal t_GmMapCompose cosinus; y; con; compose; cosinus y con compose (t2pi); (sinus)(cosinus); (x)(y)(z); (con)(s)(t); t_GmMapSurfaceMap sinwave; sinwave (compose); t_Real realCrvFakt = 12.0; t_GmMapRealConst t_GmMapRealMult t_GmMapRealSin t_GmMapRealCos t_GmMapPt3DConcatReal t_GmMapCompose t_GmMapCurveMap crvFakt(&realCrvFakt); tScal; tScal (t)(crvFakt); sint; sint (tScal); cost; cost (tScal); point; point (cost)(t)(sint); cmp2; cmp2 (point)(t); spiral; spiral (cmp2); checkinCurve(curveset); checkinSurface(sinwave); } 6.1.2 Morphing Das Grundwerkzeug, auf dem viele Ideen in der Geometrischen Modellierung beruhen, ist die Strecke als affine Konvexkombination zweier Raumpunkte: strecke pq (t ) = (1 , t ) p + t q Die Bedeutung dieses Konzeptes wird dadurch deutlich, daß sowohl Bézierkurven als auch Splines, wie auch jede lineare Iterpolation zwischen Kurven oder Flächen bis hin zur vierdimensionalen Quaternioneninterpolation letztendlich auf dieser Formel beruhen. – Die Strecke ermöglicht sowohl ein dreidimensionales wie auch ein zweidimensionales Morphing zwischen Flächen. Beides wird in diesem Beispiel demonstriert. Zum einen ist ein Morph zwischen der Kugel in Polarkoordinaten und einer bikubischen Freiformfläche abgebildet. Die Kugel wird dabei durch Sinus- und Kosinusausdrücke definiert, das Bézierpatch dagegen ist eine einfache Polynomfläche im Raum, benötigt also keinerlei transzendente Funktionen. Die Generative Modellierung ermöglicht es aber, beide Flächen in gleicher Weise zu behandeln, und zwar dadurch, daß die Information vorhanden ist, daß beide Objekte von t_SurfaceParam3D abgeleitet sind. Die Interpolationsfläche kann nun mit einem Parameter c 2 [0; 1] so ausgedrückt werden: morphc(u; v) = (1 , c) bezier(u; v) + c sphere(u; v) KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT 82 Zum anderen sind Momentaufnahmen eines zweidimensionalen Morphings abgebildet. Die eine Fläche ist eine bikubische Bézierfläche mit quadratischem Rand, deren innere vier Kontrollpunkte in charakteristischer Weise verdreht sind. Die andere Fläche ist ein einfaches Quadrat in der xz-Ebene. Interpoliert man nun linear zwischen beiden Flächen, so erhält man zu jedem Interpolationsparameter eine quadratische Fläche. Die äußere Form bleibt also gleich, aber Punkte für einen bestimmten (u; v)-Wert wandern innerhalb der Fläche. Klebt man ein Bild auf diese Fläche, so kann man die Verformung des Bildes bei der Interpolation beobachten. Nach diesem Prinzip funktioniert auch das wirkliche Morphing in kommerziellen Systemen, wobei ein dichteres Netz von Kontrollpunkten benutzt wird. – Das einzige Werkzeug, das man zur Definition der Morphs braucht, ist die C++-Funktion interpolate, die in 4.1.2 gezeigt wurde. 6.1.3 Sweep-Werkzeug Dieses Beispiel realisiert eine skalierte Profilsweepfläche und ist ein erstes Beispiel für die Definition eines Metashapes in Genmod. Es wird dabei eine Sehne, eine Skalierkurve und eine Profilkurve verwandt. Die Skalierkurve f (s) ist eine ebene Kurve, die zu s 2 [0; 1] einen Punkt ( f x (s); f y(s)) 2 R2 liefert. Die Profilkurve p(t ) ist eine beliebige ebene Kurve, die mit f (s) skaliert wird. Zu jedem s erhält man so eine skalierte Profilkurve ps (t ) = ( fx(s) px (t ); fy(s) py (t )). Die Sehne c(s) ist eine beliebige Raumkurve. Die Haupt- und die Binormale des Frenet’schen Dreibeins spannen an jedem Kurvenpunkt die Normalebene der Kurve auf und können als Basisvektoren dieser Ebene betrachtet werden. In diese Ebene wird nun die skalierte Profilkurve ps (t ) gelegt, die Gesamtheit dieser Kurven für alle s ergibt damit eine Fläche a. Betrachtet man Hauptund Binormale als Funktionale, die angewendet auf eine Kurve wiederum eine Kurve ergeben, so erhält man: a(s; t ) = c(s) + fx (s) px(t ) hauptnormale(c)(s) + fy (s) py(t ) binormale(c)(s) Die Haupt- und Binormale werden von Abbildungen mit den suggestiven Namen t_GmMapCurveXNormal bzw. t_GmMapCurveYNormal erzeugt. t_GmMapCompose t_GmDemoScaledProfileSweep::sweep(t_GmMAP& s, t_GmMAP& t, t_GmMAP& sehne, t_GmMAP& scaler, t_GmMAP& prof) { long c0=0,c1=1; t_GmMapLongConst cX(&c0); t_GmMapLongConst cY(&c1); t_GmMapCurveEval sehnS; sehnS (sehne)(s); t_GmMapCurveXNormal sehnXNormS; sehnXNormS (sehne)(s); t_GmMapCurveYNormal sehnYNormS; sehnYNormS (sehne)(s); 6.1. GENMOD-BEISPIELE 83 t_GmMapCurveEval t_GmMapCurveEval scalerS; profT; scalerS profT (scaler)(s); (prof)(t); t_GmMapPt3DCoord t_GmMapPt3DCoord t_GmMapPt3DCoord t_GmMapPt3DCoord scalerSX; scalerSY; profTX; profTY; scalerSX scalerSY profTX profTY (scalerS)(cX); (scalerS)(cY); (profT)(cX); (profT)(cY); t_GmMapRealMult t_GmMapRealMult t_GmMapPt3DScalMult t_GmMapPt3DScalMult factorX; factorY; scalXNormS; scalYNormS; factorX factorY scalXNormS scalYNormS (scalerSX)(profTX); (scalerSY)(profyTY); (factorX)(sehnXNormS); (factorY)(sehnYNormS); t_GmMapPt3DAdd t_GmMapPt3DAdd t_GmMapCompose sehnSPlsX; sehnSPlsX (sehnS)(scalXNormS); sehnSPlsXY; sehnSPlsXY (sehnSPlsX)(scalYNormS); result; result (sehnSPlsXY)(s)(t); return result; } 6.1.4 Bézier-Blending Das Werkzeug zum Bézier-Blending ist ein weiteres Beispiel für einen Metashape. Es werden dabei vier Raumkurven b0 , r0, r1 und b1 in dieser Reihenfolge erwartet. Wertet man alle vier Kurven am gleichen Parameter t aus, so erhält man vier Punkte im Raum. Diese vier Punkte werden dann als Kontrollpunkte einer kubischen Bézierkurve aufgefaßt, die Gesamtheit der Kurven ergibt auch hier eine Fläche: a(s; t ) = bezier[b0(s); b0(s) + r0(s); b1(s) + r1(s); b1(s)] (t ) Der Grund für die Benennung ist die Eigenschaft von Bézierkurven, daß zum einen die Endpunkte interpoliert werden und zum anderen die Richtung der Tangente in diesen Punkten von den benachbarten inneren Kontrollpunkten vorgegeben wird. Das bedeutet, das Bézier-BlendingWerkzeug ist eine Fläche, wo zwei Randkurven vorgegeben werden und zwei innere Kurven, mit denen man die Richtung einer partiellen Ableitung der Fläche spezifizieren kann. In [Far90] wird daher von tangent ribbon curves gesprochen, also Gummibändern, die die Fläche intuitiv gesprochen in eine Richtung zerren. KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT 84 t_GmMapCompose t_GmDemoBlendTool::blend(t_GmMAP& s, t_GmMAP& t, t_GmMAP& border0,t_GmMAP& ribbon0,t_GmMAP& ribbon1,t_GmMAP& border1) { t_GmMapCurveEval b0Eval; b0Eval (border0)(s); t_GmMapCurveEval b1Eval; b1Eval (border1)(s); t_GmMapCurveEval r0Eval; r0Eval (ribbon0)(s); t_GmMapCurveEval r1Eval; r1Eval (ribbon1)(s); t_GmMapPt3DAdd b0r0; b0r0 (b0Eval)(r0Eval); t_GmMapPt3DAdd b1r1; b1r1 (b1Eval)(r1Eval); t_GmMapPointsToBezier mybez; mybez (b0Eval)(b0r0)(b1r1)(b1Eval); t_GmMapCurveEval bezEval; bezEval (mybez)(t); t_GmMapCompose return result; result; result (bezEval)(s)(t); } 6.1.5 Bézier-Blending mit Kugelnormale Dies ist ein Beispiel für die Anwendung des Bézier-Blending-Werkzeuges, und ein Beispiel für ein parametrisiertes Modell. Spezifiziert man eine eben Kurve c(t ), die sich im Standardintervall [0; 1] [0; 1] bewegt, so kann man dieses als das Parameterintervall einer parametrischen Fläche auffassen, durch Komposition erhält man eine Raumkurve. Diese Kurve kann man zusätzlich von einem Parameter abhängig machen, in diesem Fall der “Höhe” der ebenen Kurve im Parameterintervall. Im Beispiel wird als Fläche eine Kugel in Polarkoordinaten verwandt, insgesamt erhält man: bh (t ) = sphere(cx (t ); cy(t ) + h) Diese Kurve dient dann als eine Randkurve für das Blending-Werkzeug, die andere Randkurve wird erzeugt, indem dieselbe Konstruktion noch einmal gespiegelt durchgeführt wird. Um die inneren Kurven, die tangent ribbon curves, zu erzeugen, wird an am Kurvenpunkt c(t ) zusätzlich die Normale der Kugel bestimmt, die inneren Kurven liegen damit auf einer Offsetfläche der Kugel, also auf einer Kugel mit größerem Radius. rh (t ) = (normale(sphere))(cx (t ); cy(t ) + h) Betrachtet man die resultierende Fläche, so kann man sie als ein relativ kompliziertes Modell betrachten, das über eine einzelne reelle Zahl parametrisiert ist, und zwar die Höhe der Kurve im Parameterintervall der Kugel: ah(s; t ) = bezier[b0h (s); b0h(s) + r0h (t ); b1h(s) + r1h (t ); b1h(s)](t ) Damit ist demonstriert, auf welche Weise die Trennung von Modell und Eingabedaten in Genmod realisiert werden kann. 6.1. GENMOD-BEISPIELE 6.1.6 85 Bézier-Blending mit Kugeltangente Das vorige Beispiel kann man variieren, indem die inneren Kurven des Bézier-Blending-Werkzeuges nicht durch einen Offset in Normalrichtung der Kugel erzeugt werden, sondern in der Tangentialebene am Kurvenpunkt liegen. Das erreicht man, indem man die Binormale der komponierten Raumkurve betrachtet, die Basisvektoren des Tangentialraums der Kugel berechnet, und die Binormale in diese Ebene projiziert und schließlich normiert. Dies kann man sich so vorstellen, als liefe man eine Kurve auf einer Kugel entlang und schaute die ganze Zeit genau nach links, respektive rechts auf der gespiegelten Kugel. Einen bestimmten Abstand in dieser Richtung entfernt findet man dann den entsprechenden Kurvenpunkt der zugehörigen tangent ribbon curve. Man erhält die inneren Kurven des Bézier-Blending-Werkzeuges also auf folgende Weise: rh (t ) = bnh (t ) , projekt(nh (t ); bnh (t )) bnh(t ) = binormale(sphere(cx (t ); cy(t ) + h)) nh (t ) = normale(sphere)(cx(t ); cy(t ) + h) mit Diese Berechnung wird im folgenden Programmsegment durchgeführt, curve0 ist dabei die ebene Kurve, p_curve0 die parameterabhängige Raumkurve nach Komposition mit der Kugel, im Code mit sphere bezeichnet. Das Modell besitzt als Eingabeparameter immernoch die Höhe der Kurve im Parameterraum. long t0_coord0Long=0; long t0_coord1Long=1; t_Real t0_scalReal=1.9; t_GmMapLongConst t_GmMapLongConst t_GmMapRealConst t_GmMapCurveEval t_GmMapPt3DCoord t_GmMapPt3DCoord t_GmMapSurfaceNormal t_GmMapCurveYNormal t_GmMapPt3DProject t_GmMapPt3DMinus t_GmMapPt3DNormalize t_GmMapPt3DScalMult t_GmMapCompose t_GmMapCurveMap t0_c0(&t0_coord0Long); // t0_c0=0 t0_c1(&t0_coord1Long); // t0_c1=1 t0_scal(&t0_scalReal); // t0_scal=1.9 t0_pEval; t0_pEval(p_curve0)(t); t0_pX; t0_pX(t0_pEval)(t0_c0); t0_pY; t0_pY(t0_pEval)(t0_c1); t0_sphN; t0_sphN(sphere0)(t0_pX)(t0_pY); t0_cLeft; t0_cLeft(curve0)(t); t0_prj; t0_prj(t0_sphN)(t0_cLeft); t0_leftV; t0_leftV(t0_cLeft)(t0_prj); t0_left; t0_left(t0_leftV); t0_leftS; t0_leftS(t0_scal)(t0_left); t0_map; t0_map(t0_leftS)(t); tangent0; tangent0(t0_map); KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT 86 6.1.7 Stuhl Als abschließendes Beispiel für die Verwendung von Genmod ist mit dem Stuhl ein komplexes parametrisiertes Modell entstanden. Der Stuhl wird dabei als Metashape modelliert, das als Eingabe die abgebildeten Skelettkurven erhält. Als “Konstanten” enthält der Stuhl-Operator dabei Profilund Skalierkurven für die Lehne, für die Beine und für die Seiten der Sitzfläche. Diese Kurven werden mit den Eingabekurven kombiniert und zum einen dem scaled profile sweep-Werkzeug übergeben, das daraus die Lehne erzeugt. Die Sitzseiten werden durch einen Sweep ohne Transformation des Profils erzeugt, einfach durch Entlangziehen an der Randkurve, und die Stuhlbeine werden duch einen skalierten Sweep ohne Ebenentransformation generiert, die Konstruktionsebene ist parallel zum Boden. Für die Konstruktion der Sitzfläche selber werden zunächst aus den Sitzseitenflächen die Randkurven gewonnen, was man durch Festhalten eines Parameters der Fläche erreicht. Diese Randkurven werden schließlich einer Coons-Fläche übergeben, die sie bilinear verblendet. Damit ist gezeigt, daß mit Hilfe des Genmod-Paketes die wesentlichen Ziele der Generativen Modellierung erreicht wurden. Der Genmod-Ansatz ist tragfähig Es können nicht nur kleine Demonstrationsbeispiele modelliert werden, sondern die implementierten Datenstrukturen ermöglichen bereits die Modellierung von praktisch relevanten Modellen. Parametrisierbarkeit ermöglicht Modelliereffizienz Ein Benutzer kann allein durch Auswechseln der definierenden Kurven eine ganze Schar von Stühlen erzeugen. Kurven können selber wieder parameterabhängig sein, so kann man wie im Beispiel 6.1.2 ein kontinuierliches Morphing zwischen verschiedenen Stuhlklassen durchführen. Des weiteren kann man einen Stuhl in den charakteristischen Maßen parametrisieren, die Höhe der Lehne, des Sitzes, die Breite der Sitzfläche und so weiter. Damit ist der Nutzen der Isolierbarkeit von Freiheitsgraden, die mit der Generativen Modellierung möglich geworden ist, an einem praktischen Beispiel demonstriert. 6.2. HASKELL-BEISPIELE 6.2 87 Haskell-Beispiele Im folgenden wird eine Reihe von Beispielen für die Benutzung der vorgestellten Haskell-Klassen und -Instanzen näher besprochen. Diese Beispiele stellen im Grunde die beste Motivation für die Realisierung der Generativen Modellierung in einer funktionalen Sprache dar, denn bei der Betrachtung der wenigen Zeilen Code wird deutlich, wie einfach die Beschreibung komplexer Modelle sein kann. Zudem erfüllen sie dabei durch die Übersichtlichkeit jede Forderung nach Änderbarkeit; es wird auch darauf eingegangen, wie ein Benutzer die Größen, die er nachträglich ändern können möchte, zu Parametern eines Modells machen kann. In manchen Beispielen entfällt der Abdruck der länglichen main-Funktion einer Anwendung, wenn sie nichts Erhellendes beiträgt. 6.2.1 Frenet-Rahmen als Kurven Dieses Beispiel demonstriert, wie in einem Anwenderprogrammen eine neue Flächenklasse definiert wird, um die Werkzeugbibliothek um eine oft gebrauchte Konstruktione zu ergänzen. Die Klasse SrfOffsetIntp wurde bereits in Kapitel 4.2.2 vorgestellt, zudem wird hier eine parametrisierte Konstruktion verwendet: Die Funktion frenet_m t gibt für jedes t 2 [0; 1] eine Strecke zwischen der Kurve und ihrer Hauptnormalenkurve zurück. Mit dem showCurves-Aufruf wird diese Funktion an elf Stellen ausgewertet und die entstehenden Kurven – Strecken – ausgegeben. p0 p1 p2 p3 = = = = V3 (-2.0) 0.0 (-1.0) V3 (-1.0) 0.8 0.0 V3 1.0 0.8 2.0 V3 2.0 0.0 (-1.0) data (Curve c)=> SurfOffsetIntp c = SrfOffsetIntp c (c->Float->Vector3) instance (Curve c) => Surface (SurfOffsetIntp c) where evalSurf (SrfOffsetIntp path fct) u v = evalSurf fctSrf u v where fctSrf = SrfInterpolCrv path fctPath fctPath = CrvFct (\t -> add (evalCurve path t) (fct path t)) path = CrvBezier p0 p1 p2 p3 pathNormal = CrvFct (\t-> add (evalCurve path t) (mainNormal path t)) pathBiNormal = CrvFct (\t-> add (evalCurve path t) (biNormal path t)) srfNormal = SrfInterpolCrv path pathNormal srfBiNormal = SrfInterpolCrv path pathBiNormal frenet_m t = CrvSegment (evalCurve path t) (evalCurve pathNormal t) frenet_b t = CrvSegment (evalCurve path t) (evalCurve pathBiNormal t) KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT 88 main = putStr $ (showCurve ++ (showCurve ++ (showCurve ++ (showSurface ++ (showSurface ++ (showCurves ++ (showCurves 6.2.2 "path" 10 "pathNormal" 60 "pathBiNormal" 60 "srfNormal" 8 "srfBiNormal" 8 "frenet_b" 4 "frenet_m" 4 60 60 11 11 path) pathNormal) pathBiNormal) srfNormal) srfBiNormal) frenet_b) frenet_m) Parametrisierte Transformation Dieses Beispiel zeigt eine parametrisierte zusammengesetzte Transformation. Transformiert man mit dieser eine Kurve, einen Kreisbogen in diesem Fall, so ist diese Kurve von einem zusätzlichen Parameter abhängig, interpoliert man zwischen der parametrisierten Kurve und einer anderen Kurve, so hängt die Interpolationsfläche ebenfalls von einem Parameter ab. – Man sieht damit, daß die Konstruktion transparent bleibt in sofern, als Parameter, die in einer Konstruktion auftauchen, seien es nun Zahlen, Vektoren oder ganze Flächen, bis zum Schluß durchgereicht werden können, um sie in dieser Form in die Werkzeugbibliothek des Anwenders aufzunehmen. Diese Beispielszene kann man insbesondere auch als Animation auffassen. Die Interpolationsfläche surf hängt von einem reellen Parameter ab. Setzt man dort die Zeit als skalare Größe ein, so sieht man in der Abbildungen drei Momentaufnahmen einer Animation, in der die Zeit von 0 bis 0.2 läuft. Zusätzlich wird noch vorgeführt, wie aus der parameterabhängigen Kurve crv_1 eine weitere Kurve gewonnen werden kann, indem crv_1 an der festen Stelle 0.4 ausgewertet wird und der Transformationsparameter t zum neuen Kurvenparameter wird. Durch Reparametrisierung benutzt die neue Kurve crvTrn die Transformation für t 2 [0; 14 ]. crv_0 trn t = CrvBezier (V3 (V3 (V3 (V3 ( 1.0) ( 0.3) (-0.3) (-1.0) (-0.2) (-0.2) (-0.2) (-0.2) ( 1.0)) (-1.0)) (-1.0)) ( 1.0)) = TrnCmps (TrnRotYZ t) (TrnCmps (TrnTranslate (V3 0 0.9 0)) (TrnRotYZ 0.25) ) crv = CrvReparam CrvCircle (CrvSegment (V3 1 0 0) (V3 0.5 0 0)) crv_1 t = CrvTransform (trn t) (crv ) surf t = SrfInterpolCrv (crv_1 t) (crv_0) 6.2. HASKELL-BEISPIELE 89 crvTrn main ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ 6.2.3 = CrvReparam (CrvFct (\t -> evalCurve (crv_1 t) 0.4)) (CrvSegment (V3 0 0 0) (V3 0.25 0 0)) = putStr $ (showCurve "bezier" 20 (crv_0 )) (showCurve "crvTrn" 20 (crvTrn )) (showCurve "kreis0" 20 (crv_1 0.00)) (showCurve "kreis1" 20 (crv_1 0.05)) (showCurve "kreis2" 20 (crv_1 0.10)) (showCurve "kreis3" 20 (crv_1 0.15)) (showCurve "kreis4" 20 (crv_1 0.20)) (showCurve "kreis5" 20 (crv_1 0.25)) (showSurface "surf1" 10 20 (surf 0.00)) (showSurface "surf2" 10 20 (surf 0.10)) (showSurface "surf3" 10 20 (surf 0.20)) Funktion als Fläche und patch trimming Hier wird eine Rechteckfläche deformiert, sodaß zwei der Ränder Parabeln bilden. Diese Fläche liegt im Standard-2D-Intervall [0; 1] [0; 1] und kann daher in den Parameterbereich einer Kugel abgebildet werden, die in Polarkoordinaten parametrisiert wird. Dies ergibt ein trimmed patch, einen Teil einer Freiformfläche. Man sieht auch sehr schön, wo die Parameter-Naht der Kugel verläuft, wobei die Parameterlinien (0; v) und (1; v) miteinander identifiziert werden. Dies führt dazu, daß sich die Spitzen der Parabelränder berühren. – Auch hier wurde wieder eine parametrisierte Transformation benutzt, eine Skalierung der verschobenen Kugel, sodaß mehrere Instanzen ineinander erscheinen. surf0 = SrfTransform (TrnTranslate (V3 0 (-0.5) surf1 t = SrfTransform (TrnScale (V3 1 p 1)) where p=0.5+2.0*r*r r=t-0.5 surf2 = SrfFct (\u v->evalSurf (surf1 u) u v) surf3 = SrfTransform (TrnCmps (TrnScale (V3 (TrnTranslate (V3 surf4 t = SrfTransform (TrnCmps (TrnTranslate (V3 (TrnScale (V3 surf5 t = SrfFct (\u v-> let p=evalSurf surf3 u v evalSurf (surf4 t) (x p) 0)) SrfXY surf0 0.5 1 1)) 0.5 0.5 0))) surf2 0 0 2)) t t t))) SrfSphere in (y p) ) 90 6.2.4 KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT Offset einer Fläche in Richtung der Normalen Eine Kugel kann in sechs gleiche Viereckspatches zerlegt werden, analog zu den Seiten eines Würfels. In diesem Beispiel wurde die abgebildete bikubische Bézierfläche benutzt, um ein KugelSechstel in Richtung der Normalenvektoren zu verschieben. Die Randkurve des Höhenfeldes liegt dabei in der xy-Ebene, daher ist es möglich, mehrere Instanzen zu erzeugen, die um neunzig Grad um die Koordinatenrichtungen gedreht wurden und an den Randkurven aneinander anschließen. Somit realisiert dieses Beispiel einen stetigen Patchkomplex mit Hollow-Kugelsechstel, allerdings ohne Zusammenhangsinformationen. – Die sechs Großkreise dienen zur Verdeutlichung der Grenzen zwischen den Sechsteln. Es ist im übrigen interessant, zu verfolgen, wie der Datenfluß in diesem Programm verläuft. Der Konstruktor eines Kugelsegmentes steht schließlich nur ein einziges Mal im Code! An dieser Stelle wird also der Nutzen der referentiellen Transparenz für die Modellierung im besonderen deutlich; durch die Abwesenheit von Nebeneffekten sind auch die Flächen 4 und 5 wohldefiniert und müssen nicht befürchten, daß die zugrundeliegende Fläche, das Kugelsechstel, verändert wird, bevor sie selber ausgegeben sind. Andererseits kann dieses Sechstel auch nicht gelöscht werden, bevor alle fünf Flächen in der Szene ausgegeben sind, dies sind die Kosten dieses Ansatzes. Weiter von Bedeutung ist die Tatsache, daß die der Konstruktion zugrundeliegende Fläche an einer einzigen Stelle definiert wird – und dort geändert werden kann –, beziehungsweise verläuft die Konstruktion der Fläche 5 aus der Fläche 1 über drei Zwischenkonstruktionen, die ihrerseits referenzierbar sind. Haskell muß alle drei Zwischenergebnisse aber anlegen, zunächst nicht absehbar ist, ob diese Zwischenergebnisse an anderer Stelle noch benötigt werden, das heißt, die deformierte Fläche surf_N muß als solche erhalten bleiben, auch wenn in der Folge nur Fläche 5 für den Anwender von Interesse ist. – Ist man aber nur an dem Endresultat einer Konstruktion interessiert, so bietet es sich an, die Zwischenergebnisse mittels where zu lokalen Variablen zu machen, um dem Compiler eine Chance zur Optimierung zu geben. – Solche Effizienzüberlegungen sind insbesondere bei größeren Konstruktionen von Bedeutung. surf_0 = heightfield ( 0.0) ( 0.0) ( 0.0) ( 0.0) ( 0.0) ( 2.2) (-2.2) ( 0.0) ( 0.0) (-2.2) ( 2.2) ( 0.0) ( 0.0) ( 0.0) ( 0.0) ( 0.0) surf_1 = SrfSphereSegment surf_2 = SrfTransform (TrnRotXY 0.25) surf_1 surf_N = SrfFct $ \u v -> add (evalSurf surf_1 u v) (mult (z (evalSurf surf_0 u v)) (normal surf_1 u v)) surf_3 = SrfTransform (TrnRotXY 0.5 ) surf_N surf_4 = SrfTransform (TrnRotXY 0.25) surf_3 6.2. HASKELL-BEISPIELE 91 surf_5 = SrfTransform (TrnRotZX 0.25) surf_3 liftS srf = SrfTransform (TrnTranslate (V3 0 0 2)) srf liftC crv = CrvTransform (TrnTranslate (V3 0 0 2)) crv c0=CrvTransform c1=CrvTransform c2=CrvTransform c3=CrvTransform c4=CrvTransform c5=CrvTransform 6.2.5 (TrnRotZX (TrnRotZX (TrnRotYZ (TrnRotYZ (TrnRotXY (TrnRotXY 0.125) (-0.125)) 0.25) 0.25) 0.25) 0.25) CrvCircle CrvCircle c0 c1 c0 c1 Parametrisierte Konstruktion: Zaun Dies ist eine Lösung zu einer Variation der in Kapitel 2.5.1 auf Seite 24 gestellten Aufgabe, einen Zaun in einer Weise zu modellieren, daß die Lösung änderbar ist und an anderer Stelle wiederverwendet werden kann. Dies wird wieder über eine parametrisierte Konstruktion gelöst, die einzelnen Sprossen des Zaunes werden durch Sampling einer parametrisierten Funktion erzeugt, die zu t2 [0; 1] eine Sprosse liefert, die die obere und untere Verlaufskurve des Zaunes an dieser Stelle regelgesteuert miteinander verbinden. Dazu wird die Tangente und die Hauptnormale einer der Verlaufskurven benutzt, um eine Ellipse im Raum zu definieren. Dann wird eine Kopie dieser Ellipse nach ’unten’ (in Richtung der Binormalen der Ellipse!) verschoben. Das gleiche Werkzeug wird benutzt, um für die untere Kurve am gleichen Parameterwert eine ähnliche Ellipse zu erzeugen, zusammen mit einer nach ’oben’ verschobenen Kopie. Damit hat man vier Raumkurven. Aus diesen wird mit dem bezierSweep-Werkzeug eine Fläche definiert, indem – wie bereits beschrieben – für jeden u-Wert die vier Ellipsen ausgewertet werden und die entstehenden Punkte als Kontrollpolygon einer kubischen Bézierkurve genommen werden. Wertet man diese in v aus, entsteht eine deformierte zylinderförmige Hülle. Betrachtet man die Verlaufskurven und den Radius der Sprossen als Konstante, so ist die gesamte Konstruktion eines Zaunpfostens nur von der Stelle auf der Kurve abhängig, somit kann die Funktion showSurfaces zum Sampling von pfost, mithin zur Produktion von zehn Zaunpfosten benutzt werden. zaunBase=CrvBezier (V3 (-1.0) (V3 (-0.333) (V3 ( 0.333) (V3 1.0 (-0.3) ( 0.3) ( 0.3) (-0.3) 0) 0) 0) 0) zaunKopf=CrvBezier (V3 (-1.0) (-0.1) 0.5) (V3 (-0.333) ( 0.1) 0.5) KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT 92 (V3 ( 0.333) ( 0.1) 0.5) (V3 1.0 (-0.1) 0.5) sprossenRadius=0.03 kreis crv t = CrvEllipse (evalCurve crv t) (mult (sprossenRadius) (mainNormal crv t)) (mult (sprossenRadius*0.5) (tangent crv t)) biNormalOffset crv d = CrvFct $ \t -> add (evalCurve crv t) (mult (d) (biNormal crv t)) bezierSweep crv0 crv1 d = SrfFct $ (\u v->evalCurve (CrvBezier (evalCurve crv0 u) (evalCurve (biNormalOffset crv0 ( d)) u) (evalCurve (biNormalOffset crv1 (-d)) u) (evalCurve crv1 u)) v) bcurv_0 t = kreis zaunBase t bcurv_1 t = biNormalOffset (kreis zaunBase t) (-0.15) bcurv_2 t = biNormalOffset (kreis zaunKopf t) 0.15 bcurv_3 t = kreis zaunKopf t pfosten t = bezierSweep (kreis zaunBase t) (kreis zaunKopf t) 0.15 main = putStr $ (showCurve "s0" ++ (showCurve "s2" ++ (showSurfaces "pfost_" 6.2.6 40 40 8 10 10 zaunBase) zaunKopf) pfosten ) Parametrisierte Konstruktion: Wendeltreppe Ergebnis der folgenden Konstruktion ist eine Wendeltreppe, die im Seminarpapier von Jucknath [Juc95] als Paradebeispiel eines Modells genannt wird, das offensichtlich mit wenigen Parametern beschrieben ist, und dessen Konstruktion in einem konventionellen Modeller dennoch zum einen größere Mühe bedeutet, zum anderen empfindlich auf die Änderung der definierenden Parameter reagiert. Die Wendeltreppe wird dazu im wesentlichen in vier Teile zerlegt. Das eine sind die Trittflächen, gegeben durch die Funktion stair n, die für ein n eine Stufen-Grundform rotieren läßt. Die Form dieser Stufe ist in diesem Fall als Eingabeparameter vorgestellt, die undeformierte Grundform ist in der Abbildung exemplarisch als stairExample auf der xy-Ebene dargestellt. Der zweite Teil der Treppe sind die Frontflächen der Stufen, diese werden durch Interpolation der Randkurven der eigentlichen Stufen erzeugt, sind also auch als Funktion gegeben, und zwar stepVert n. Am Anfang und am Ende der Treppe ist zudem je ein kleines Podest angebracht, das 6.2. HASKELL-BEISPIELE 93 obere Podest wird entsprechend der Anzahl der Stufen mitrotiert. Der Deutlichkeit halber sind die Konstanten, die als Eingabeparameter des Modells dienen sollen, im Vorspann des Programmtextes angeordnet. – An dieser Stelle sieht man deutlich die Grenzen der vorliegenden Implementation von Genmod, denn es ist zwar möglich, eine Schar von Objekten zu erzeugen, aber der offensichtlichte Zusammenhang zwischen den einzelnen Bauteilen der Wendeltreppe kann nicht ausgedrückt werden. Das bedeutet, es wird hier offensichtlich, daß weitere integrierende Datenstrukturen wie Patchkomplexe und Szenengraphen notwendig sind, um Konstruktionen, die noch komplexer sind als die hier vorliegende, strukturieren zu können. – Die Notwendigkeit von Zusammenhangsinformationen wird zum Beispiel deutlich, wenn man sich überlegt, die Wendeltreppe noch mit einem Geländer zu versehen, zum Beispiel eine Interpolationsfläche zwischen einem kontinuerlichen Handlauf rechts und links und den Seiten der Trittund der Frontflächen. An dieser Stelle hat es der Modellierer schwer, eine allgemeine Lösung zu finden, die kein spezielles Wissen über den Aufbau der Wendeltreppe erfordert. stairDegree stairHeight stairRad stairSteps stairForm = = = = = 1.0/8.0 0.4 0.2 6 heightfieldNrm ( 0.2) (-0.2) ( 0.1) (-0.1) ( ( ( ( 0.0) 0.0) 0.0) 0.0) ( ( ( ( 0.0) 0.0) 0.0) 0.0) ( 0.2) (-0.2) ( 0.1) (-0.1) -- *********** spiral staircase object ******************** inner = stairRad outer = inner+1.0 stepHori = SrfFct $ \u v -> let p=evalSurf stairForm u v in evalTrans (TrnRotXY ((y p)*stairDegree)) (V3 ((x p)+stairRad) 0 (z p)) stair n = SrfTransform (TrnCmps (TrnRotXY degree) (TrnTranslate (V3 0 0 height))) stepHori where degree = n*stairDegree height = n*stairHeight stepVert n = SrfInterpolCrv (CrvKeepV (stair (n-1)) 0.0) (CrvKeepV (stair (n )) 1.0) upperFlat = SrfInterpolCrv (CrvKeepV (stair stairSteps) 0.0) (CrvSegment p q) where p = tr (V3 outer ( outer) (stairSteps*stairHeight)) q = tr (V3 inner ( outer) (stairSteps*stairHeight)) tr = evalTrans (TrnRotXY ((stairSteps+1)*stairDegree)) lowerFlat = SrfInterpolCrv (CrvReverse (CrvKeepV (stair 0) 0.0)) (CrvSegment p q) where p = tr (V3 inner (-outer) 0.0) q = tr (V3 outer (-outer) 0.0) tr = evalTrans (TrnRotXY stairDegree) 94 6.2.7 KAPITEL 6. BEISPIELE UND ERFAHRUNGSBERICHT rail product und wire product Als abschließendes Beispiel soll die Definition von wire product und rail product vorgestellt werden. Beide Metashapes sind dem Buch von Snyder entnommen, und so bietet sich eine Möglichkeit zum direkten Vergleich der verwendeten Sprachen. Die hier gegebene Definition des wire product läßt sich ohne Schwierigkeiten mit der in 3.1 auf Seite 31 gezeigten Definition in GENMOD identifizieren. Entsprechendes gilt für sämtliche Metashapes, die in GENMOD implementiert sind, mit Ausnahme derjenigen, die komplexe Operatoren benutzen, wie den Integrationsoperator, das Minimieren unter Nebenbedingungen, constraint solving oder das Lösen von Anfangswertproblemen. – Kurz gesagt, in bezug auf das elementare Modellieren hat diese Implementation mindestens die Ausdruckskraft wie GENMOD, verfügt aber nicht über ein Konzept wie Intervallarithmetik und die damit verbundenen Algorithmen und Datenstrukturen. In obiger Abbildung ist ein wannenförmiges wire product zu sehen, das über eine planare Verlaufskurve und eine ebenfalls planare Profilkurve in der xy-Ebene definiert wird. Das Profil ist im Bereich der negativen (grünen) y-Achse zu sehen, daher liegt die wire product-Fläche unterhalb der Verlaufskurve. Zudem sind zwei rail product-Flächen abgebildet, bei denen die Profile, die entlang der (roten) x-Achse im Bereich von ,1 bis 1 ausgestreckt sind, zwischen zwei Verlaufskurven gespannt werden. Beide Werkzeuge sind typische Variationen der Sweep-Idee und verkörpern als solche das Prinzip der Generativen Modellierung, weil sie demonstrieren, wie ein Generator kontinuierlich transformiert wird, um eine Fläche zu konstruieren. -- Wire Product operator from Snyder, 3.2.1.2, p. 64 data (Curve a, Curve b) => SurfWireProd a b = SrfWireProd a b instance (Curve a, Curve b) => Surface (SurfWireProd a b) where evalSurf (SrfWireProd cross wire) u v = V3 (x p) (y p) (y crss_v) where tang = tangent wire u wire_u = evalCurve wire u crss_v = evalCurve cross v left = V3 (-(y tang)) (x tang) 0.0 p = add (mult (x crss_v) left) wire_u -- Rail Product operator from Snyder, 3.2.1.3, p. 66 data (Curve a,Curve b,Curve c)=> SurfRailProd a b c = SrfRailProd a b c instance (Curve a,Curve b,Curve c)=> Surface (SurfRailProd a b c) where evalSurf (SrfRailProd crss rail1 rail2) u v = V3 (x p) (y p) (y c_u) where c_u = evalCurve crss u rail1_v = evalCurve rail1 v rail2_v = evalCurve rail2 v t = sub rail2_v rail1_v m = mult 0.5 (add rail2_v rail1_v) p = add m (mult (0.5*(x c_u)) t) Kapitel 7 Zusammenfassung 7.1 Modellierung ist funktional objektorientiert Es wurden viele Belege für die Eingangsthese gefunden, daß die Modellierung inhärent ein funktionaler Prozeß ist. Interaktives Modellieren mit konventionellen Modellern, seien sie approximationsbasiert, rein objektorientiert oder parametrisch, bedeutet die Definition von Objekten in einer Szene, sowie die Anwendung von Werkzeugen, um die Objekte der Szene sukzessive zu transformieren und zu verknüpfen. Die grundsätzlichen Operationen dabei sind die Objektkonstruktion und die Abbildung. Beides sind ebenfalls die grundlegenden Operationen in funktionalen Sprachen. Funktionale Sprachen sind daher grundsätzlich geeignet, um den Prozeß der Modellierung zu beschreiben. Protokolliert man die Aktionen, die ein Modellierer bei seiner Arbeit ausführt, so erhält man einen Abhängigkeitsgraphen, wo interaktiv eingegebene Konstanten, Skalare, Punkte, Kurven und Flächen sowie Gegenstände in Form von Patchkomplexen als Eingaben am Anfang stehen und sämtliche Werkzeuganwendungen die – implizite oder explizite – Abhängigkeiten zwischen diesen Objekten zu neuen Objekten führen. Veränderung von Objekten findet im eigentlichen Sinne nicht statt, entscheidend ist, daß die Möglichkeit besteht, Teilkonstruktionen an mehr als einer Stelle in einer Szene zu verwenden. Mehrfachreferenzierung von Objekten bedeutet, daß ein Abhängigkeitsbaum zu einem DAG wird. Es wurde eine C++-Implementation von Abbildungsobjekten vorgestellt, die eine Auswertung eines Abbildungsgraphen effizient unterstützt und der Art der verwendeten Datenstrukturen keine Beschränkungen auferlegt. Jede extern definierte C++-Klasse, die für die Modellierung wichtig erscheint, kann mit Hilfe des Template-Mechanismus als Variablenobjekt in das Modelliersystem aufgenommen werden. Es lassen sich Abbildungen beliebiger – auch variabler – Signatur konstruieren, die eine Liste von Objekten auf ein neues Objekt abbilden. Insbesondere sind solche Memberfunktionen von Variablenklassen zur Definition von Abbildungen geeignet, deren Parameter ebenfalls in das System in Form von Variablenklassen integriert sind. Die einzige Einschränkung für Abbildungen ist, daß sie die Eingangsparameter nicht verändern, sondern sich nur auf das Erzeugen eines neuen Objektes beschränken. Dies ist aber konsistent mit dem Vorgehen in einem interaktiven Modeller, wo Objekte mit Hilfe von Handles zwar scheinbar verändert werden, in Wirklichkeit handelt es sich dabei aber nur darum, die richtigen skalaren Parameter für eine Form zu finden. Ist ein gültiges Objekt erzeugt, wird es nicht weiter interaktiv bearbeitet und in der definierten Form gerendert. Das Verbot destruktiver Updates einerseits ist andererseits gleichbedeutend mit der Abwesenheit von Seiteneffekten. Für die Modellierung bedeutet das, daß sich der Modellierer darauf verlassen kann, daß sich ein referenziertes Objekt nicht plötzlich verändert. 95 96 KAPITEL 7. ZUSAMMENFASSUNG Bedeutet geometrische Modellierung die Anwendung von Abbildungen auf Objekte, so bedeutet funktionale Programmierung das Rechnen mit Abbildungen. Im funktionalen Programmierparadigma finden sich daher viele Konzepte zum Umgang mit der Beschränkung, die die Abwesenheit von Seiteneffekten bedeutet. Sei es, daß statt Schleifen Rekursion verwendet werden, sei es, daß Funktionen ’first class citizens’ sind. Gleichzeitig zeigt das Beispiel von Haskell, daß das nicht bedeutet, die Vorteile eines statischen Typsystems und der objektorientierten Programmierung aufzugeben. Es ist nur notwendig, den Signaturbegriff und den Begriff des Objektes in anderer – letztendlich auch der Modellierung angepaßter – Weise zu fassen. Durch die Verwendung funktionaler Prinzipien als Grundlage für die Beschreibung generativ modellierter Objekte wird daher das Optimum an Ausdrucksfähigkeit erreicht: Generative Modelle lassen sich in der kürzesten und verständlichsten Weise beschreiben. 7.2 Modellieren mit Abbildungen ist effizient Neben den theoretischen Überlegungen hat die Generative Modellierung ganz klar eine praktische Relevanz. Modellierung bedeutet nicht mehr, Szenen nach dem ’trial and error’-Verfahren zu konstruieren, sondern der Modellierer hat die Möglichkeit, den inneren Aufbau eines Modells zu explizieren. Präzises Wissen über den inneren Aufbau modellierter Objekte kann in Form anwenderdefinierter Werkzeuge zum Aufbau einer Bibliothek von Modelliervorschriften verwendet werden, es ist nicht mehr allein Programmierern von ’Plugins’ vorbehalten, Modeller um Makro-Objekte zu erweitern. Es kann unterschieden werden zwischen den speziellen Skalaren und Kontrollpunkten, die zu einer Objektinstanz gehören, und der abstrakten Verknüpfung dieser Eingabewerte zu einem Modell. Die Verwendung höherer Funktionen gestattet es zusätzlich, die verwendeten Werkzeuge selber zu Parametern des Modells zu machen, also auf noch höherem, abstrakteren und damit mächtigerem Niveau zu modellieren: Zur Beschreibung wissenschaftlicher Visualisierungen beispielsweise muß man Zugriff auf die vielen unterschiedlichen Darstellungsmittel der Computergrafik haben, die eingangs in Kapitel zwei in Abschnitt 2.2 geschildert wurden. Viele der benutzten Konzepte finden darüberhinaus einen ganz natürlichen Ausdruck in der funktionalen Beschreibung. Aufruf eines Objektkonstruktors und interaktive Eingabe von Parametern bedeutet einfach die Definition einer Variablen. Anwendung einer beliebigen Transformation oder eines Werkzeugs bedeutet einen Funktionsaufruf. Eine Szenehierarchie entsteht ganz natürlich aus den Transformationen im Abhängigkeitsgraphen, insbesondere kann man die Konstruktion von Objekten grundsätzlich im LKS vornehmen. Referenzobjekte entstehen durch Anwendung einer Funktion auf eine bereits existierende Variable, es ergibt sich eine neue Variable. Alle Größen einer Szene sind parametrisierbar, insbesondere also die verwendeten Zahlen, eine Animation erhält man wie in Beispiel 6.2.2 indem man die Zeit als skalaren Parameter verwendet, der an jeder gewünschten Stelle Einfluß nehmen darf. Materialien kann man in der allgemeinsten Form durch Angabe des Reflexionsverhaltens mit einer ’bidirectional reflection distribution function’, einer BRDF, ausdrücken, das Färben eines Flächenteils geschieht durch Auswertung der BRDF für den dortigen Ein- und Ausfallwinkel des Lichts. Eine Lichtquelle ist eine Abbildung, die jedem Raumpunkt eine Lichtintensität zuordnet. Eine Kamera ist eine umkehrbare Abbildung zwischen einem Punkt einer Projektionsfläche und einem Strahl im Raum. Schatten werden appoximativ berechnet, indem eine Szene und eine Reihe von Lichtquellen auf eine beleuchtete Approximation der Szene abgebildet werden. Ein Renderingverfahren ist letztendlich nichts als eine Abbildung einer beleuchteten Szene auf ein Bild, eine Abbildung einer Liste von Szenen ergibt eine Bildsequenz, eine Animation. – Die Realisierung von Freiformkurven und -Flächen, einzelnen Patches, und von vielen Werkzeugen wurde bereits detailliert vorgeführt. Dabei wurde im Rahmen des cns- 7.3. MÖGLICHE REALISIERUNGEN 97 Paketes vor allem beschrieben, wie eine Klassenhierarchie für parametrische Freiformkurven und -Flächen aussehen kann, und wie numerisch approximierte Ableitungen eingesetzt werden. Partikelsysteme kann man als Menge von Trajektorien beschreiben, ist etwa ein kontinuierliches Kraftfeld aus überlagerten Einzelkräften vorgegeben, so sind die Trajektorienkurven Ergebnis eines Anfangswertproblems, also abhängig von Startpunkt und Startgeschwindigkeit eines Partikels. Ein bestimmtes Partikelsystem kann als stochastisches Sampling der Trajektorienfunktion gesehen werden, eine Animation ergibt sich durch regelmäßiges Sampling der Trajektorien. Interagierende Partikel erhält man durch Angabe einer Liste von Verbindungen, ein neuer Zustand entsteht durch Abbildung eines alten Zustands durch eine Übergangsfunktion. Den gleichen Ansatz einer Zustands-Übergangsfunktion kann man für physikbasiertes Modellieren verwenden, dies ist auch das Verfahren für die Ein/Ausgabe in funktionalen Sprachen. Der große Vorteil einer vereinheitlichten Beschreibung liegt auf der Hand: Auf alle Aspekte eines Modellierprozesses kann mit demselben Mechanismus zugegriffen werden. Eine wissenschaftliche Visualisierung erfolgt, indem Dateien mit Rechenergebnissen regelgesteuert zu Szenen und Animationen verarbeitet werden. Eine einzelne Beschreibungsdatei, ein geometrisches Programm, wird von einem Renderer abgearbeitet, so entsteht eine komplette Animation alleine aus der Verknüfung von Eingabedaten und Abbildungsregeln, wenn die Beschreibungssprache für diese Regeln mächtig genug ist. Modellierung im Großen erfordert daher eine geometrische Programmiersprache, und die Beispiele in Kapitel sechs zeigen, wie dies realisiert werden kann. 7.3 Mögliche Realisierungen Generative Modellierung geschieht auf einem höheren Abstraktionsgrad. Dies beinhaltet einerseits den Abschied vom gewohnten Herumstochern in der zweidimensionalen Projektion einer dreidimensionalen Szene, andererseits zwingt es in der einen oder anderen Form zum Programmieren. Programmsysteme wie Geomview werden ferngesteuert, indem man ihnen einen Strom von low-level-Kommandos schickt. Einen objektorientierten Modeller kann man sich so vorstellen, daß er eine Schnittstelle zwischen dem Anwender und einer solchen ’geometry engine’ definiert, also Benutzerinteraktion in Kommandos für den Aufbau einer Geometrie umwandelt. Auf diese Weise trennt man das schwer formalisierbare Benutzerinterface von dem zugrundeliegenden Sprachkonzept. Ändert man die Kommandosprache und benutzt eine Beschreibung von Modellen auf einem höheren Niveau, so kann man beispielsweise ein GENMOD- oder Haskell-gesteuertes Geometriemodul konzipieren. Snyders Modellieransatz kann also als eine Trennung an einer Stelle verstanden werden, die einen Vergleich von Modellern ermöglicht, eine Beschreibungssprache, die die gesamten Benutzermanipulationen in eine “lebendige”, das heißt tatsächlich ausführbare Szenenbeschreibung umwandelt. Die Rolle eines skriptgesteuerten Geometriemoduls fällt im vorliegenden Fall dem MRT zu. Es stellt sich also das Problem, zum einen eine generative Programmiersprache zu definieren, zum anderen den MRT auf die Verarbeitung generativer Modelle vorzubereiten. Antworten auf beide Fragen wurden in dieser Arbeit vorgeschlagen. Die C++-Implementation von Genmod ermöglicht es, die zusammengesetzten Abbildungen zur Laufzeit zu konstruieren, die bei Aufruf eines Metashapes wie m_wire zurückgegeben werden. Überdies lassen sich für sämtliche Kurven und Flächen, die in den vorangegangenen HaskellBeispielen per show ausgegeben wurden, zusammengesetzte C++-Genmod-Abbildungen angeben, die genau diese Objekte repräsentieren. Das bedeutet, die innere Organisation des MRT ist auf generative Modelle vorbereitet. Zum anderen ist es offensichtlich nicht nötig, eine funktionale Sprache als geometrische Pro- KAPITEL 7. ZUSAMMENFASSUNG 98 grammiersprache zu verwenden. Ein wesentliches Ergebnis dieser Arbeit ist zwar die Erkenntnis, daß funktionale Sprachen dem Wesen der Modellierung am besten angepaßt sind, doch Beispiele wie TBag oder GENMOD zeigen, daß auch in normalerweise prozedural orientierten Sprachen ein funktionaler Programmierstil möglich ist. Im Prinzip läßt sich jede C-erweiterbare, interpretierte, bereits verbreitete Kommandosprache wie Python [Lut96], Tcl [Ous95], Perl [WS91], oder auch eine Minimalimplementation eines C-Interpreters wie Slang [Dav96] verwenden, um ihr die generative Struktur “unterzuschieben”. Das wire product könnte man sich in einer ersten PythonImplementation zum Beispiel folgendermaßen vorstellen: def m_wire(cross, wire, operation): that = operation(wire,m_real(0)) t = m_normalize(that) n = m_vector(m_minus(m_y(t)),m_x(t),m_real(0)) p = m_add(m_mult(m_x(cross),n) q = m_add(p,vector(m_real(0),m_real(0),m_y(cross) return q mywire = m_wire(mycross, mywire, m_derivative) Python hat den Vorteil, auch das Überladen von Operatoren zuzulassen, wodurch der Code entsprechend knapper und lesbarer würde. Entscheidend ist jedoch, daß hier ein Abbildungsgraph definiert wird, die Funktionsaufrufe würden in einer solchen Implementation dazu dienen, darunterliegende Genmod-Abbildungen zusammenzusetzen, wo die Typüberprüfung und das Binden in der gewohnten Weise stattfinden kann. Python gestattet als Interpreter überdies die Übergabe von Funktionen wie m_derivative in obigem Beispiel, die Typüberprüfung findet erst bei Ausführung statt. Nichtsdestotrotz ist mywire als Resultat einer Python-Auswertung dieses Programmteils wie gehabt ein MRT-Objekt, das gerendert werden kann. Haskell-Genmod ist langsam. Die Auswertung der Beispiele, das Sampling von Kurven und Flächen, zeichnet sich nicht durch eine hohe Ausführungsgeschwindigkeit aus, obwohl nicht interpretierter, sondern compilierter Code verwendet wurde. Dies hängt damit zusammen, daß Haskell als allgemeine Programmiersprache konzipiert wurde. Erweitert man dagegen eine interpretierte Standardsprache durch Wrapper so, daß das Resultat einer Ausführung C++-GenmodAbbildungen sind, ist die Ausführungsgeschwindigkeit mit hoher Wahrscheinlichkeit höher als bei Verwendung einer funktionalen Standardsprache oder bei reiner Implementation in einer interpretierten Sprache. – Eine weitere Möglichkeit ist die Verbindung von interpretiertem und vorkompiliertem Code, wie er von Haskell-Compilern wie HBC der Chalmers University of Technology erlaubt wird; die meisten Haskell-Compiler haben zudem eine Schnittstelle zu C, bzw. sind selber in C geschrieben. – Ein ideales Konzept zur Realisierung eines generativen Modellers wäre daher, zum einen den Aufbau einer Werkzeugbibliothek durch eine leicht zu erlernende Skriptsprache zu erleichtern, andererseits Standard-Handles zur Parameterauswahl zur Verfügung zu stellen, um für eigene Werkzeuge Gizmos zu schreiben, die Kontrollpunkte und Skalare als Parameter für aufgerufene Werkzeuge liefern. Denn benutzt man keine höheren Funktionen, kommen zur Auswahl von Parametern nur Zahlen, Punkte, Kurven und Patches in Frage. Dann genügt es – auch für die Mehrzahl der Haskell-Beispiele –, die folgenden interaktiven Operationen zu unterstützen und “mitzuschreiben”: Werkzeuganwendung bzw. Konstruktion eines Objektes Auswahl eines Objektes in der Szene als Werkzeugparameter oder Leerlassen eines Parameters, dieser wird Parameter des Modells Funktionsdefinition: Zusammengesetzte Abbildung als neues Werkzeug 7.4. AUSBLICK 99 In der Grundform der Generativen Modellierung werden also – wie in GENMOD – syntaktisch gesehen nur ganz wenige Sprachkonstrukte benutzt, die kein echtes Programmieren erfordern und interaktiv ausgelöst werden können. Entscheidend ist jedoch, daß ein sauberes, durchdachtes Sprachkonzept im Hintergrund steht, dessen Ausdrucksfähigkeit man formal untersuchen kann; mit zunehmender Erfahrung auf dem Gebiet der “interaktiven Programmierung” wird sich herauskristallisieren, wie weit man diesen Ansatz ausbauen kann. Klar ist jedoch, daß ab einem bestimmten Abstraktionsgrad die textuelle Eingabe, wirkliches Programmieren, wesentlich einfacher und transparenter ist. Variablen erhalten sprechende Bezeichner, man sieht auf einen Blick, welche Variable wo referenziert wird, und der innere Aufbau, der Datenfluß innerhalb eines Modells läßt sich bei disziplinierter Programmierung oftmals alleine durch das Programmlayout auf einen Blick erfassen. Durch einen hybriden Modellieransatz läßt sich schließlich möglicherweise ein fließender, schmerzfreier Übergang zwischen beiden Welten erreichen. 7.4 Ausblick Rein geometrisch ist der entscheidende Vorteil der Generativen gegenüber der konventionellen Modellierung die Tatsache, daß ein Modell schließlich nicht approximativ vorliegt, sondern als Abbildung, die jederzeit an jeder Stelle ausgewertet werden kann. Um vom Modellieren mit BReps aber gänzlich Abschied nehmen zu können, ist es entscheidend, die aufwands- und platzmäßig harten Probleme zu lösen, die beim Lösen impliziter Gleichungssystemen auftreten, um schließlich CSG auch auf Freiformflächen zu realisieren, Strahlschnitte effizient zu berechnen und Isoflächen extrahieren zu können. Snyder hat viele dieser Probleme mit Hilfe von Inklusionsfunktionen in den Griff bekommen, was nur möglich war, weil in Generativen Modellen das Konzept der Operatormethode konsequent realisiert werden kann. Berechnung der Inklusionsfunktion führt jedoch zwangsläufig zur Benutzung von Methoden der Intervallrechnung. Diese ist in den vorliegenden Implementationen von Genmod noch in keiner Weise realisiert, die verschiedenen Ideen, die Snyder in seinem Buch vorlegt, lassen aber vermuten, daß sich dort ein ganz eigenes Arbeitsgebiet ergeben wird. Intervallrechnung führt zu Methoden wie ’merging’ von kd-Bäumen und linearer Programmierung, die auf den ersten Blick nicht viel mit kontinuierlichen Abbildungen zu tun haben. Das zweite wichtige Problem, das sich aus dieser Arbeit ergibt, ist die Frage der Realisierung von Patchkomplexen. Interessant hierbei ist vor allem die Frage, wie kontinuierliche Strukturen wie Freiformflächen und -kurven mit der diskreten Struktur eines eingebetteten Graphen in Einklang gebracht werden können. Es ist klar, daß sich die Oberfläche zusammengesetzter, geschlossener Gegenstände als Ansammlung von Flächenstücken auffassen läßt, die entlang von Raumkurven verklebt werden. Alles andere als klar ist jedoch, wie sich der Konstruktionsprozeß eines solchen Gegenstands beschreiben läßt. Zum einen liegt dies in der Vielfalt der Verfahren begründet. Coons- oder Hermite-Patches gibt es in drei-, vier- und n-eckiger Form, diese lassen beispielsweise zu, daß man einfach ein hinreichend dichtes, irreguläres Kurvennetz vorgibt, zwischen das Flächen gespannt werden, ohne daß es zu Knicken kommt. Bei diesem Ansatz ergeben sich jedoch Probleme wie die Spezifikation von Twist-Vektoren, um ’flat spots’ zu vermeiden [Far90], und die praktisch begrenzte Ordnung der Stetigkeit entlang der Ränder. Eine andere Möglichkeit ist das Vorgeben einzelner Flächenstücke, die dann verblendet werden, um die Lücken auszufüllen. Hierbei aber ergeben sich zumeist zusätzliche Freiheitsgrade, die eine sehr unanschauliche geometrische Bedeutung haben und für die ein sinnvoller Wert schwer heuristisch abzuschätzen ist. Die Isolierung der Freiheits- 100 KAPITEL 7. ZUSAMMENFASSUNG grade selber ist ebenfalls schwierig, wenn man die Bedingung der Stetigkeit auf die Stetigkeit der Form reduziert, also Geometrische Stetigkeit vorschreibt, die unabhängig ist von der Parametrisierung der einzelnen Flächen. Und schließlich möchte man natürlich in der Lage sein, heterogene Patchkomplexe aus all den verschiedenen Typen von Flächenelementen zu bauen, die das Füllhorn der Geometrischen Modellierung bereithält. Dabei kommt es zu dem Problem, einheitliche Übergangsbedingungen für die verschiedensten Flächentypen zu formulieren. Ein weiteres Problem ist es, rein syntaktisch eine Möglichkeit zum bequemen Umgang mit Graphstrukturen zu finden. Wie eingangs erläutert, sind BReps letztendlich auch nichts anderes als Patchkomplexe. BReps sind die dominierende Datenstruktur in der geometrischen Modellierung, entsprechend gibt es eine uferlose Menge von Implementationen und Dateiformaten, alleine in dieser Arbeit wurde eine Reihe von ihnen erwähnt. Die Verwendung von BReps und ihr Aufbauen innerhalb eines Programms hängt aber vom API der Implementation ab und ist meist extrem kompliziert, weil es das blinde Navigieren in einem Graphen und das Einhalten bestimmter Konsistenzbedingungen erfordert. Die meisten 3D-APIs ermöglichen daher einfach das Einlesen von Geometrien aus Dateien und unterstützen dazu die gängigsten Dateiformate. Dieser Ausweg besteht natürlich nicht, wenn bei der Modellierung von Patchkomplexen die Graphstruktur erst nach und nach entsteht und die Graphen wegen der Mächtigkeit der Formen in der Regel viel kleiner sind als BReps. Um Teile eines Patchkomplexes tatsächlich handhaben zu können, ist im Rahmen der Entwicklung des cns-Paketes eine weitere Implementation in Form einer Klasse für eingebettete Graphen entstanden. Dazu wurde die ’winged-edge’-Datenstruktur gekapselt und der Zugriff auf die in [Män88] beschriebenen, algebraisch fundierten Euleroperatoren beschränkt, die zu einer übersichtlichen Zahl – im wesentlichen acht – von Memberfunktionen des Graphenklasse geführt haben. Schwerpunkt hierbei war es, einen zugleich bequemen wie auch sicheren Zugang zur Datenstruktur durch Iteratoren [SL95] zu ermöglichen, die im topologischen Sinne eine Stelle des Graphen repräsentieren, also Zeiger auf eine Fläche, eine Kante der Fläche und einen Knoten der Kante enthalten. Damit diese Datenstruktur zukünftig als Basis für Patchkomplexe dienen kann, war es aber nötig, sie in Form eines C++-Templates zu realisieren, damit die unterschiedlichsten Flächen und Kurven zusammen mit den nötigen Verwaltungsinformationen wie Parameterwechsel entlang der Ränder und (u; v)-Werte von Ecken in einer gemeinsamen Struktur gehalten werden können. – Dennoch ist im Hinblick auf eine komfortable Bedienbarkeit an dieser Stelle noch einige Arbeit zu leisten, vor allem wenn man an die Verbindung von Patchkomplexen und Generativer Modellierung unter dem Dach einer geometrischen Programmiersprache denkt. Zuletzt stellt die Verbindung der kontinuierlichen Objekte und der diskreten Struktur selber auch noch eine Schwierigkeit dar. Patchkomplexe haben nicht nur die topologische Konsistenz des Graphen zu respektieren, sondern es müssen ebenfalls geometrische Bedingungen erfüllt werden. Es ist aber vollkommen unklar, wie etwa sichergestellt werden soll, daß topologisch benachbarte Flächen auch geometrisch aneinandergrenzen, daß zwischen ihnen kein Spalt bleibt, die breiter als ein Epsilon ist, und wie Randkurven definiert werden können, die tatsächlich in beiden Flächen enthalten sind. Zudem ist vorstellbar, daß es während des Aufbauprozesses einem Patchkomplex durchaus erlaubt sein soll, mehrere Hollows aneinandergrenzen zu lassen, unzusammenhängende Flächen oder Kurven zu enthalten, die später noch aneinandergefügt werden, oder alleinstehende, unverbundene Ecken im Raum als Anhaltspunkte für eine Konstruktion zu benutzen. Für BReps wurde mit den Euleroperatoren eine schöne Methode gefunden, die Datenstruktur zu jedem Zeitpunkt konsistent zu halten und keine hängenden oder NULL-Zeiger zulassen zu müssen. Für Patchkomplexe steht ein ähnlich eleganter Zugang noch aus. Der letzte Problemkomplex in diesem Zusammenhang ist die Definition geeigneter Methoden auf Patchkomplexen. Der Strahlschnitt zum einen zeigt an Kanten und Schnittstellen zwischen zwei oder, schlimmer noch, mehreren Patches zumeist ein pathologisches Verhalten, weil 7.4. AUSBLICK 101 Strahlen von keiner oder von mehreren Flächen in verschiedene Richtungen reflektiert werden. Dadurch, daß man aber Informationen über den lokalen Zusammenhang besitzt, hat man zumindest eine Chance, dieses Problem in den Griff zu bekommen. Dennoch stellt sich das Problem, effizient herauszufinden, daß ein Strahl Gefahr läuft, in der Nähe eines oder mehrerer Flächenränder aufzutreffen. – Die andere zu implementierende Standardmethode ist die Umwandlung in ein BRep, hier müssen geeignete Methoden gefunden werden, um unterschiedlich parametrisierte Patches reißverschlußartig zu verbinden. Zudem eignet sich ein reguläres Sampling überhaupt nicht, um ein gemeinsames BRep für aneinandergrenzende Patches zu bauen, deren geometrische Abmessungen stark unterschiedlich sind. Hier müssen also Strategien für adaptives Sampling im Bildraum gefunden werden. Schwierigkeiten stellen sich bei heterogenen Patchkomplexen aber bereits bei grundsätzlich trivialen Operationen wie der affinen Transformation; bei generativen Modellen könnte man alle Abbildungen, die parametrische Patches definieren, mit einer Transformationsabbildung komponieren. Alles in allem tun sich also viele interessante Fragestellungen auf, wenn man bestrebt ist, das Problem einer geometrischen Programmiersprache für heterogene Patchkomplexe in Verbindung mit Generativer Modellierung und der Lösung von Systemen impliziter Gleichungen anzugehen. KAPITEL 7. ZUSAMMENFASSUNG 102 Literaturverzeichnis [Aut96a] Autodesk Inc. 3D Studio Max Tutorials, 1996. [Aut96b] Autodesk Inc. 3D Studio Max User’s Guide, 1996. Part1, Part 2. [Aut96c] Autodesk Press. Benutzerhandbuch AutoCAD R12, 1996. [BBB87] Richard H. Bartels, John C. Beatty und Brian A. Barsky. An Introduction to Splines for Use in Computer Graphics & Geometric Modeling. Morgan Kaufmann Publishers, California, 1987. [Ben97] Heinzgerd Bendels. Eine topologische Datenstruktur und ihre Anwendungen im 3DGraphiksystem MRT. Diplomarbeit, Universität Bonn, 1997. [BFH95] Heinzgerd Bendels, Dieter W. Fellner und Sven Havemann. Modellierung der Grundlagen. In D. W. Fellner, Hrsg., Beiträge zum internationalen Workshop on Modeling – Virtual worlds – Distributed graphics., 1995. [CLR90] Thomas H. Cormen, Charles E. Leiserson und Ronald L. Rivest. Introduction to Algorithms. MIT Press, 1990. [CS96] Sven Campagna und Philipp Slusallek. Improving Bezier Clipping and Chebychev Boxing for Ray Tracing Parametric Surfaces. In B. Girod, H. Niemann und H.-P. Seidel, Hrsg., 3D Image Analysis and Synthesis ’96, Seiten 95–102. infix, 1996. [Dav96] John E. Davis. S-Lang Programmer’s Guide, 0.1. Auflage, 1996. Softwaredokumentation. [do 93] Manfredo P. do Carmo. Differentialgeometrie von Kurven und Flächen, Kapitel 2. Vieweg, 1993. [Ell96] Conal Elliott. A Brief Introduction to ActiveVRML. Technical Report MSR-TR-96-05, Microsoft Research, 1996. http://research.microsoft.com/pubs/tr-96-05a.html. [ESYAE95] Conal Elliott, Greg Schechter, Ricky Yeung und Salim Abi-Ezzi. TBAG: A High Level Framework for Interactive, Animated 3D Graphics Applications. In Proc. SIGGRAPH ’94, Seiten 421–434. ACM, 1995. [Far90] Gerald Farin. Curves and Surfaces For Computer Aided Geometric Design. Academic Press, San Diego, CA, 2. Auflage, 1990. [Fel92] W. D. Fellner. Computergrafik. BI-Wissenschaftsverlag, Zürich, 2.. Auflage, 1992. [Fel96] Dieter W. Fellner. MRT – A Teaching and Research Platform for 3D Image Synthesis. IEEE CG&A, 16(3), Mai 1996. [FG94] Joanne C. Fullagar und Open Inventor Achitecture Group. Open Inventor C++ Reference Manual. Silicon Graphics Inc., Addison-Wesley, 1994. [Fis95a] Martin Fischer. Reference Pointers for Class Hierarchies. Technical Report IAI-TR-95-12, Universität Bonn, August 1995. [Fis95b] Martin Fischer. Runtime Type Information for Class Hierarchies. Technical Report IAI-TR95-11, Universität Bonn, Juli 1995. [HJe92] P. Hudak, S. Peyton Jones und P. Wadler (editors). Report on the Programming Language Haskell (Version 1.2). ACM SIGPLAN Notices, 27(5), 1992. [HL92] Josef Hoschek und Dieter Lasser. Grundlagen der geometrischen Datenverarbeitung. Teubner, Stuttgart, 2. Auflage, 1992. [Hud89] Paul Hudak. Conception, Evolution, and Application of Functional Programming Languages. ACM Computing Surveys, 21(3), 1989. [Juc95] Oliver Jucknath. Programmiersprache zur Beschreibung von 3D-Objekten. Seminarpapier, 1995. LITERATURVERZEICHNIS 103 [Lut96] Mark Lutz. Programming Python. Nutshell Handbook. O’Reilly & Associates, Inc., 1996. [Män88] Martti Mäntylä. An Introduction to Solid Modeling. Computer Science Press, Rockville, 1988. [Ous95] John K. Ousterhout. Tcl und Tk. Addison-Wesley, 1995. [Prö92] Prömel. Diskrete Mathematik III. Vorlesung, Wintersemester 1991/1992. [SDK96] Schmitt, Deussen und Kreeb. Einführung in graphisch-geometrische Algorithmen. B. G. Teubner, Stuttgart, 1996. [SL95] Alexander Stepanov und Meng Lee. The Standard Template Library. Hewlett-Packard Laboratories, 1995. [Sny92] John M. Snyder. Generative Modeling for Computer Graphics and CAD. Academic Press, 1992. [WG94] Josie Wernecke und Open Inventor Achitecture Group. The Inventor Mentor. Silicon Graphics Inc., Addison-Wesley, 1994. [WS91] Larry Wall und Randal L. Schwartz. Programming Perl. Nutshell Handbook. O’Reilly & Associates, Inc., 1991. [WW92] Alan Watt und Mark Watt. Advanced Animation and Rendering Techniques. Addison-Wesley, 1992. 104 KAPITEL 7. ZUSAMMENFASSUNG Anhang A Haskell-Code Die folgenden beiden Abschnitte enthalten die komplette Definition von Genmod in Haskell in zwei Dateien von 15852 beziehungsweise 1453 Bytes. A.1 Die Datei Genmod.hs module Genmod( Curve (derivOne,derivTwo,tangent,mainNormal,biNormal, evalCurve,diff_1,diff_2), Surface (tangentU,tangentV,normal,evalSurf), Transform (evalTrans), CurveX (CrvX), CurveSegment (CrvSegment), CurveTransform (CrvTransform), CurveFct (CrvFct), CurveInSurf (CrvInSurf), CurveReparam (CrvReparam), CurveReverse (CrvReverse), CurveInterpol (CrvInterpol), CurveKeepU (CrvKeepU), CurveKeepV (CrvKeepV), CurveBezier (CrvBezier), CurveCircle (CrvCircle), CurveEllipse (CrvEllipse), SurfXY (SrfXY), SurfTransform (SrfTransform), SurfFct (SrfFct), SurfParamCrv (SrfParamCrv), SurfBezier (SrfBezier), SurfSphere (SrfSphere), SurfSphereSegment (SrfSphereSegment), SurfInterpolCrv (SrfInterpolCrv), SurfCoons (SrfCoons), SurfWireProd (SrfWireProd), SurfRailProd (SrfRailProd), TransformCmps (TrnCmps), TransformFct (TrnFct), TransformTranslate (TrnTranslate), TransformScale (TrnScale), TransformRotXY (TrnRotXY), TransformRotYZ (TrnRotYZ), TransformRotZX (TrnRotZX), 105 ANHANG A. HASKELL-CODE 106 line, heightfield, heightfieldNrm, sampleCurve, sampleSurf, showSurface, showCurve, showSurfaces, showCurves) where import Vectors -- ***************************************** -- ** HASKELL-GENMOD KERNEL -- ***************************************** class Curve c derivOne derivTwo tangent mainNormal biNormal evalCurve diff_1 diff_2 where :: c -> :: c -> :: c -> :: c -> :: c -> :: c -> :: c -> :: c -> Float Float Float Float Float Float Float Float -> -> -> -> -> -> -> -> Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 diff_1 crv t = sub (evalCurve crv (t+diffQuot_h)) (evalCurve crv (t-diffQuot_h)) diff_2 crv t = sub (add (evalCurve crv (t+diffQuot_h)) (evalCurve crv (t-diffQuot_h))) (add p p) where p=evalCurve crv t derivOne crv t = mult diffQuot_2h (diff_1 crv t) derivTwo crv t = mult diffQuot_h2 (diff_2 crv t) tangent crv t = normalize (diff_1 crv t) biNormal crv t = normalize (cross (diff_1 crv t) (diff_2 crv t)) mainNormal crv t = normalize (cross (cross d1 d2) d1) where d1 = diff_1 crv t d2 = diff_2 crv t class Surface tangentU tangentV normal evalSurf tangentU tangentV normal s where :: s -> :: s -> :: s -> :: s -> Float Float Float Float -> -> -> -> Float Float Float Float -> -> -> -> Vector3 Vector3 Vector3 Vector3 srf u v = tangent (CrvKeepV srf v) u srf u v = tangent (CrvKeepU srf u) v srf u v = normalize $ cross (tangentU srf u v) (tangentV srf u v) class Transform t where evalTrans :: t -> Vector3 -> Vector3 A.1. DIE DATEI GENMOD.HS -- ***************************************** -- ** CURVES -- ***************************************** -- Identical x curve data CurveX = CrvX instance Curve CurveX where evalCurve (CrvX) t = V3 t 0.0 0.0 -- Make a line segment from two points data CurveSegment = CrvSegment Vector3 Vector3 instance Curve CurveSegment where evalCurve (CrvSegment p0 p1) t = line p0 p1 t -- Make a transformed curve a curve data (Transform t, Curve c) => CurveTransform t c = CrvTransform t c instance (Transform t, Curve c) => Curve (CurveTransform t c) where evalCurve (CrvTransform trans crv) t = evalTrans trans (evalCurve crv t) -- Make a function a curve data CurveFct = CrvFct (Float->Vector3) instance Curve CurveFct where evalCurve (CrvFct f) t = f t -- Make a curve in a surface’s parameter space a space curve data (Surface s, Curve c) => CurveInSurf s c = CrvInSurf s c instance (Surface s, Curve c) => Curve (CurveInSurf s c) where evalCurve (CrvInSurf srf crv) t = evalSurf srf (x p) (y p) where p = evalCurve crv t -- Reparameterization: evaluate a curve at t and take the x coordinate -- of the computed point to be the parameter for a second curve data (Curve c, Curve d) => CurveReparam c d = CrvReparam c d instance (Curve c, Curve d) => Curve (CurveReparam c d) where evalCurve (CrvReparam f g) t = evalCurve f (x (evalCurve g t)) -- Reversal of a curve data (Curve c) => CurveReverse c = CrvReverse c instance (Curve c) => Curve (CurveReverse c) where evalCurve (CrvReverse crv) t = evalCurve crv (1.0-t) -- Linear morph between two given curves data (Curve c, Curve d) => CurveInterpol c d = CrvInterpol c d Float instance (Curve c, Curve d) => Curve (CurveInterpol c d) where evalCurve (CrvInterpol crv1 crv2 t0) t = line (evalCurve crv1 t) (evalCurve crv2 t) t0 -- Fixing a surface’s u parameter gives a v-curve in the surface data (Surface s) => CurveKeepU s = CrvKeepU s Float instance (Surface s) => Curve (CurveKeepU s) where evalCurve (CrvKeepU srf u0) t = evalSurf srf u0 t -- Fixing a surface’s v parameter gives a u-curve in the surface data (Surface s) => CurveKeepV s = CrvKeepV s Float instance (Surface s) => Curve (CurveKeepV s) where evalCurve (CrvKeepV srf v0) t = evalSurf srf t v0 107 108 ANHANG A. HASKELL-CODE -- Cubic Bezier curve from four control points data CurveBezier = CrvBezier Vector3 Vector3 Vector3 Vector3 instance Curve CurveBezier where evalCurve (CrvBezier p0 p1 p2 p3) t = line p012 p123 t where p01 = line p0 p1 t p12 = line p1 p2 t p23 = line p2 p3 t p012 = line p01 p12 t p123 = line p12 p23 t -- Standard circle data CurveCircle = CrvCircle instance Curve CurveCircle where evalCurve (CrvCircle) t = V3 (cos u) (sin u) 0.0 where u=t*2.0*pi -- Space ellipse for midpoint and two direction vectors data CurveEllipse = CrvEllipse Vector3 Vector3 Vector3 instance Curve CurveEllipse where evalCurve (CrvEllipse m v w) t = add m (add (mult (cos u) v) (mult (sin u) w)) where u=t*2.0*pi -- ***************************************** -- ** SURFACES -- ***************************************** -- Identical xy rectangle data SurfXY = SrfXY instance Surface SurfXY where evalSurf (SrfXY) u v = V3 u v 0.0 -- Make a transformed surface a surface data (Transform t, Surface s) => SurfTransform t s = SrfTransform t s instance (Transform t, Surface s) => Surface (SurfTransform t s) where evalSurf (SrfTransform trans srf) u v = evalTrans trans (evalSurf srf u v) -- Make a function a surface data SurfFct = SrfFct (Float->Float->Vector3) instance Surface SurfFct where evalSurf (SrfFct f) u v = f u v -- Make a parameterized curve a surface data (Curve c) => SurfParamCrv c = SrfParamCrv (Float->c) instance (Curve c) => Surface (SurfParamCrv c) where evalSurf (SrfParamCrv f) u v = evalCurve (f u) v -- Bicubic Bezier tensor product surface from sixteen control points data SurfBezier = SrfBezier Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 Vector3 instance Surface SurfBezier where evalSurf (SrfBezier p00 p01 p02 p03 p10 p11 p12 p13 p20 p21 p22 p23 p30 p31 p32 p33) u v = evalCurve ( CrvBezier (evalCurve (CrvBezier p00 p01 p02 p03) u) (evalCurve (CrvBezier p10 p11 p12 p13) u) A.1. DIE DATEI GENMOD.HS (evalCurve (CrvBezier p20 p21 p22 p23) u) (evalCurve (CrvBezier p30 p31 p32 p33) u) 109 ) v -- Full sphere in polar coordinate parameterization data SurfSphere = SrfSphere instance Surface SurfSphere where evalSurf (SrfSphere) u v = V3 (cosu*cosv) (cosu*sinv) (sinu) where sinu=sin (pi*(u-0.5)) cosu=cos (pi*(u-0.5)) sinv=sin (2*pi*v) cosv=cos (2*pi*v) -- One sixth of a sphere, a quadriliteral with midpoint (1,0,0) data SurfSphereSegment = SrfSphereSegment instance Surface SurfSphereSegment where evalSurf (SrfSphereSegment) u v = normalize (V3 coss sins ((-coss)+v*2.0*coss)) where s=0.5*pi*(u-0.5) coss = cos s sins = sin s -- Interpolate linearly between corresponding points of given curves data (Curve c, Curve d) => SurfInterpolCrv c d = SrfInterpolCrv c d instance (Curve c, Curve d) => Surface (SurfInterpolCrv c d) where evalSurf (SrfInterpolCrv crv1 crv2) u v = evalCurve (CrvInterpol crv1 crv2 u) v -- Bilinear Coons surface between four border curves, but corners must -- coincide: c0v(0)=cu0(0), c0v(1)=cu1(0), c1v(0)=cu0(1), c1v(1)=cu1(1) data (Curve a, Curve b, Curve c, Curve d) => SurfCoons a b c d = SrfCoons a b c d instance (Curve a, Curve b, Curve c, Curve d) => Surface (SurfCoons a b c d) where evalSurf (SrfCoons c0v c1v cu0 cu1) u v = sub ( add (mult u1 (evalCurve c0v v)) (add (mult u (evalCurve c1v v)) (add (mult v1 (evalCurve cu0 u)) (mult v (evalCurve cu1 u))))) ( add p00 (add p01 (add p10 p11))) where u1=1.0-u v1=1.0-v p00=mult (u1*v1) (evalCurve c0v 0.0) p01=mult (u1*v ) (evalCurve cu1 0.0) p10=mult (u *v1) (evalCurve cu0 1.0) p11=mult (u *v ) (evalCurve c1v 1.0) -- Wire Product operator from Snyder, 3.2.1.2, p. 64 data (Curve a, Curve b) => SurfWireProd a b = SrfWireProd a b instance (Curve a, Curve b) => Surface (SurfWireProd a b) where evalSurf (SrfWireProd cross wire) u v = V3 (x p) (y p) (y crss_v) where tang = tangent wire u wire_u = evalCurve wire u crss_v = evalCurve cross v left = V3 (-(y tang)) (x tang) 0.0 p = add (mult (x crss_v) left) wire_u 110 ANHANG A. HASKELL-CODE -- Rail Product operator from Snyder, 3.2.1.3, p. 66 data (Curve a, Curve b, Curve c) => SurfRailProd a b c = SrfRailProd a b c instance (Curve a, Curve b, Curve c) => Surface (SurfRailProd a b c) where evalSurf (SrfRailProd cross rail1 rail2) u v = V3 (x p) (y p) (y cross_u) where cross_u = evalCurve cross u rail1_v = evalCurve rail1 v rail2_v = evalCurve rail2 v t = sub rail2_v rail1_v m = mult 0.5 (add rail2_v rail1_v) p = add m (mult (0.5*(x cross_u)) t) -- ***************************************** -- ** TRANSFORMS -- ***************************************** -- Make concatenated transforms a transform data (Transform t0, Transform t1) => TransformCmps t0 t1 = TrnCmps t0 t1 instance (Transform t0, Transform t1) => Transform (TransformCmps t0 t1) where evalTrans (TrnCmps trans1 trans0) p = evalTrans trans1 (evalTrans trans0 p) ----- infix notation for transform composition infixr 8 ř (ř)::(Transform t0, Transform t1) => t0 -> t1 -> TransformCompose t0 t1 t0 ř t1 = TrnCmps t0 t1 -- Make a function a transform data TransformFct = TrnFct (Vector3->Vector3) instance Transform TransformFct where evalTrans (TrnFct f) p = f p -- Translation data TransformTranslate = TrnTranslate Vector3 instance Transform TransformTranslate where evalTrans (TrnTranslate v) p = add p v -- Scaling data TransformScale = TrnScale Vector3 instance Transform TransformScale where evalTrans (TrnScale v) p = V3 ((x p)*(x v)) ((y p)*(y v)) ((z p)*(z v)) -- Rotation about the Z axis in the XY plane data TransformRotXY = TrnRotXY Float instance Transform TransformRotXY where evalTrans (TrnRotXY w) p = V3 (c*(x p)-s*(y p)) (s*(x p)+c*(y p)) (z p) where s = sin (2*pi*w) c = cos (2*pi*w) -- Rotation about the X axis in the YZ plane data TransformRotYZ = TrnRotYZ Float instance Transform TransformRotYZ where evalTrans (TrnRotYZ w) p = V3 (x p) (c*(y p)-s*(z p)) (s*(y p)+c*(z p)) where s = sin (2*pi*w) c = cos (2*pi*w) A.1. DIE DATEI GENMOD.HS 111 -- Rotation about the Y axis in the ZX plane data TransformRotZX = TrnRotZX Float instance Transform TransformRotZX where evalTrans (TrnRotZX w) p = V3 (c*(x p)+s*(z p)) (y p) (c*(z p)-s*(x p)) where s = sin (2*pi*w) c = cos (2*pi*w) -- ***************************************** -- ** SERVICE FUNCTIONS AND CONSTANTS -- ***************************************** diffQuot_h::Float diffQuot_h=0.005 diffQuot_2h=0.5/diffQuot_h diffQuot_h2=(1.0/diffQuot_h)/diffQuot_h -- Base of it all: The straight affine line segment line :: Vector v => v->v->Float->v line a b t = add (mult (1-t) a) (mult t b) -- XY-Heightfield over [-1,1]x[-1,1] heightfield a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 a30 a31 a32 a33 = SrfBezier (V3 a a a00) (V3 b a a01) (V3 a b a10) (V3 b b a11) (V3 a c a20) (V3 b c a21) (V3 a d a30) (V3 b d a31) where a= 1.0 b= 1.0/3.0 c=(-1.0)/3.0 d=(-1.0) (V3 (V3 (V3 (V3 c c c c a b c d a02) a12) a22) a32) (V3 (V3 (V3 (V3 d d d d a b c d a03) a13) a23) a33) -- XY-Heightfield over [0,1]x[0,1] (standard surface parameter space) heightfieldNrm a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 a30 a31 a32 a33 = SrfBezier (V3 a a a00) (V3 b a a01) (V3 c a a02) (V3 d a a03) (V3 a b a10) (V3 b b a11) (V3 c b a12) (V3 d b a13) (V3 a c a20) (V3 b c a21) (V3 c c a22) (V3 d c a23) (V3 a d a30) (V3 b d a31) (V3 c d a32) (V3 d d a33) where a= 1.0 b= 2.0/3.0 c= 1.0/3.0 d= 0.0 112 ANHANG A. HASKELL-CODE -- ***************************************** -- ** SAMPLING OF CURVES AND SURFACES -- ***************************************** -- Transform a curve into a list of points on the curve -- Standard curve interval for regular sampling: [0,1] sampleCurve::(Curve c) => Int->c->[Vector3] sampleCurve nn crv = [ evalCurve crv (fromIntegral(i)*dt) | i<-[0..nn] ] where dt = 1.0/(fromIntegral(nn)) -- Transform a surface into a list of list of points on the surface -- Standard surface interval for regular sampling: [0,1]x[0,1] sampleSurf::(Surface s) => Int->Int->s->[[Vector3]] sampleSurf nu nv srf = [ [ evalSurf srf (fromIntegral(i)*du) (fromIntegral(j)*dv) | i<-[0..nu] ] | j<-[0..nv] ] where du = 1.0/(fromIntegral(nu)) dv = 1.0/(fromIntegral(nv)) -- Show a curve in the format "crv_0=[(x,y,z),...]" showSurface::(Surface s) => String->Int->Int->s->String showSurface name nu nv srf = name ++ "=" ++ (show (sampleSurf nu nv srf)) ++ "\n" -- Show a surface in the format "srf_0=[ [(x,y,z),...], ...]" showCurve::(Curve c) => String->Int->c->String showCurve name nn crv = name ++ "=" ++ (show (sampleCurve nn crv)) ++ "\n" -- Sample a function [0,1]->Curve and show the curves showCurves::(Curve c) => String->Int->Int->(Float->c)->String showCurves name nn steps crvFct = showCrv (steps-1) where dt = (1.0/fromIntegral(steps-1)) showCrv (-1) = "" showCrv n = let fn=name++(show n) t =dt*fromIntegral(n) in (showCurve fn nn (crvFct t)) ++ (showCrv (n-1)) -- Sample a function [0,1]->Surface and show the surfaces showSurfaces::(Surface s) => String->Int->Int->Int->(Float->s)->String showSurfaces name nu nv steps srfFct = showSrf (steps-1) where dt = (1.0/fromIntegral(steps-1)) showSrf (-1) = "" showSrf n = let fn=name++(show n) t =dt*fromIntegral(n) in (showSurface fn nu nv (srfFct t)) ++ (showSrf (n-1)) A.2. DIE DATEI VECTORS.HS A.2 113 Die Datei Vectors.hs module Vectors( Vector(add,sub,prod,cross,mult,absLen,normalize,zeig,x,y,z), Vector3(V3), zeigZahl, zeigListe) where class Vector add sub prod cross mult absLen normalize zeig x y z a where :: a->a->a :: a->a->a :: a->a->Float :: a->a->a :: Float->a->a :: a->Float :: a->a :: a->String :: a->Float :: a->Float :: a->Float data Vector3 = V3 Float Float instance Vector Vector3 where add (V3 ax ay az) (V3 bx sub (V3 ax ay az) (V3 bx prod (V3 ax ay az) (V3 bx cross (V3 ax ay az) (V3 bx Float by by by by bz) bz) bz) bz) = = = = (V3 (ax+bx) (ay+by) (az+bz)) (V3 (ax-bx) (ay-by) (az-bz)) (ax*bx)+(ay*by)+(az*bz) (V3 (ay*bz-az*by) (az*bx-ax*bz) (ax*by-ay*bx)) mult a (V3 bx by bz) = (V3 (a*bx) (a*by) (a*bz)) zeig (V3 ax ay az) = "(" ++ (show ax) ++ "," ++ (show ay) ++ "," ++ (show az) ++ ")" absLen (V3 ax ay az) = sqrt ( (ax*ax) + (ay*ay) + (az*az) ) normalize (V3 ax ay az) = (V3 (l*ax) (l*ay) (l*az)) where k = absLen (V3 ax ay az) l = case k of 0 -> 0 _ -> (1/k) x (V3 ax ay az) = ax y (V3 ax ay az) = ay z (V3 ax ay az) = az 114 ANHANG A. HASKELL-CODE A.2. DIE DATEI VECTORS.HS 115 116 ANHANG A. HASKELL-CODE