|
Die eierlegende Wollmilchsau Synopsis
Bei der Programmierung von 3D-Szenen ist es notwendig, Daten auf die eine oder andere Art zu organisieren. Obwohl es viele verschiedene Methoden gibt, 3D Umgebungen computerunterstützt zu simulieren (polygonzentrierte Scanlinerenderer, Raytracer und Voxelszenarien, um die drei Bekanntesten zu nennen), konzentriert sich dieser Artikel vor allem auf Polygonrenderer, die von aktueller Consumergrafikhardware am Besten unterstützt werden. Im Speziellen geht es um das Objektmodell von BaseGraph, in dem versucht wird OpenGL Funktionalität möglichst flexibel zu kapseln, bzw. dessen Erweiterung in Carad, die fähig ist, beinahe beliebige Objekte zu verwalten. Eine der Prämissen von BaseGraph ist es, dem Anwender einerseits nicht vorzuschreiben, wie eine bestimmter Effekt implementiert wird, andererseits sollen mit möglichst wenigen Befehlen komplexe Szenarien erstellt werden können – dass sich beides widerspricht, ist leicht einsichtig – die Philosophie von BaseGraph lautet daher, dass komplexe Funktionalität zwar vorhanden ist und genutzt werden kann - aber nicht muss. Um eigene Objektstrukturen in den BaseGraph Szenegraphen einzubinden, reicht es aus, Objekte von der Basisklasse Obj abzuleiten und mit eigener Funktionalität zu befüllen – wobei der Term Objekt durchaus weit gefasst ist, und z.B. auch die gesamte „Welt“ einer Simulation umfassen kann. Jede Entität, sichtbar oder nicht, die eine bestimmte Position im Raum einnimmt, wird als Objekt definiert und sollte von Obj abgeleitet werden, um möglichst leicht in BaseGraph integriert werden zu können. Obj selbst ist dabei eher von abstrakter Natur – schließlich soll der Anwender in der Art der Realisierung von Entitäten nicht eingeschränkt sein – und beinhält folgende Eigenschaften:
Selbstverständlich ist es nicht notwendig, ein Objekt etwa mit einem Mesh, Animationen oder Verbindungsgelenken zu versehen, allerdings erweisen sich diese Eigenschaften als recht häufig benötigt, sodass Verweise darauf bereits in der grundlegenden Objektstruktur verankert wurden. Wer sich die gekapselten Daten genau ansieht, wird bemerken, dass keinerlei Informationen vorliegen, wie das Objekt nun tatsächlich aussieht (das Mesh ist nur optional und enthält, wenn vorhanden, reine Positionsdaten, die z.B. auch nur ein vereinfachtes Objektmodell enthalten können) – dies ist auch so beabsichtigt, als sich ein Obj für sich gesehen gar nicht darstellen kann, erst davon abgeleitete Objekte haben dann die Informationen, die für eine tatsächliche Darstellung notwendig sind, die im einfachsten Fall aus einer Aneinanderreihung von OpenGL Kommandos in der (in Obj abstrakten) Routine DrawObj bestehen. Nun mag die Obj Klasse zwar recht flexibel sein, für die tatsächliche Verwaltung beliebiger Objekte zur Laufzeit, wie es etwa in Carad notwendig ist, reicht sie aber bei Weitem nicht aus, da sie eben keine Informationen über das gekapselte Objekt enthält. Carad instanziiert daher nur die von Obj abgeleitete Klasse Body (nicht in BaseGraph enthalten, da nur für die Entwicklung von 3D-Editoren interessant), bzw. von Body abgeleitete Klassen. Body erfüllt dabei drei Aufgaben:
Interessant für eine eigene Implementation einer ähnlichen Klasse ist dabei Punkt 1., da die Verwaltung von OpenGL Primitiven in Carad erst kürzlich revidiert und neu strukturiert wurde. Ursprünglich (bis Version 1.2) konnte der Mcad, der Vorläufer von Carad, nur statische Objekte erstellen, die direkt als OpenGL Kommandos realisiert wurden und vom Anwender im erstellten Programm selbst animiert werden mussten, falls eine Animation gewünscht war. Nachdem OpenGL Primitive recht einfach gehalten sind – man gibt an, um was es sich handelt (GL_TRIANGLES, GL_QUADS, GL_QUAD_STRIP, …) und sendet die entsprechenden Schnittpunktdaten, wurden diese in Mcad genau so verwaltet. Der Nachteil dieser Methode ist, dass es recht viele identische Schnittpunkte gibt. Inzwischen kann Carad aber nicht nur direkte OpenGL Kommandos erstellen, sondern Objekte auch als Vertex Arrays (mit Erweiterung durch VBOs) und Frame- und Boneanimationen speichern. Diese Funktionalität, die doch ziemlich unterschiedliche Datenstrukturen erfordert, sowie die Möglichkeit Schnittpunkte und deren Eigenschaften direkt zu berechnen, zu optimieren und später eventuell sogar einen „echten“ Schnittpunkt- und Polygoneditor einzubauen, setzen eine etwas differenziertere Betrachtungsweise der gekapselten Daten voraus. Gesucht ist also eine Datenstruktur, die einerseits möglichst wenig Platz braucht, andererseits möglichst schnell in möglichst viele unterschiedliche „Formen“ umberechnet werden kann, ohne dabei aufwendigerweise doppelte Schnittpunkte entfernen zu müssen. Diese Formen sind:
Um das Ganze ein wenig zu illustrieren, werden verschiedene Möglichkeiten dargstellt, das folgende Viereck zu realisieren (Orientierung erfolgt im Uhrzeigersinn)
Im Folgenden steht: V für Vertex (ein Schnittpunkt mit drei Komponenten)
OpenGL Kommandos, normal glBegin(GL_TRIANGLES)
OpenGL Kommandos, optimiert (macht BaseGraph automatisch) glBegin(GL_TRIANGLES)
Schnittpunktdaten für glDrawArrays V: (0,0,0)(1,0,0)(1,1,0)(1,1,0)(0,1,0)(0,0,0)
Schnittpunktdaten und Indizes für glDrawElements V: (0,0,0)(1,0,0)(1,1,0)(0,1,0) I: 0,1,2, 2,3,0
Schnittpunktdaten und Indizes intern von Body verwaltet - diese können leicht auf jede der obigen Strukturen umgemünzt werden V: (0,0,0)(1,0,0)(1,1,0)(0,1,0) VI: 0,1,2, 2,3,0
Wie man sieht, kommt nur im letzten Beispiel jeder Schnittpunkt und Schnittpunktparameter nur genau einmal vor, als optimal für die Kapselung sämtlicher OpenGL Funktionalität haben sich dabei folgende Eigenschaften gezeigt:
Ein Primitive ist dabei eine Liste von OpenGL Kommandos, die zwischen glBegin und glEnd stehen kann, wird dieses als Befehlsstrom realisiert, werden tatsächlich nur diejenigen Kommandos gesendet, die sich tatsächlich ändern. Haben also mehrere aufeinanderfolgende Schnittpunkte die selbe Normale, wird die Normale nur einmal beim ersten Schnittpunkt gesendet, da sich OpenGL den entsprechenden State „merkt“. Die Primitivestruktur schaut dabei folgendermaßen aus:
Vorteile gegenüber unindizierten Daten sind: 1) keine
doppelten Schnittpunkte oder Schnittpunktparameter Der einzige Nachteil dabei ist die etwas umständlichere Verwaltung der ganzen Listen, sowie das Sicherstellen, dass ein neu hinzugefügter Schnittpunkt oder Schnittpunktparameter nicht bereits vorhanden ist. Ich hoffe, dass dieser Text bei der Implementierung einer eigenen Objektstruktur
die eine oder andere Orientierungshilfe bietet,
|