Studienarbeit im Studiengang Informatik Perspektivische Suche in geometrischen Szenen Matthias Hilbig 24. Juni 2004 Erstgutachter: Dr. Martin Ziegler Zweitgutachter: Prof. Dr. Wilhelm Schäfer Zusammenfassung In dieser Studienarbeit werden drei Datenstrukturen zur Durchführung einer perspektivischen Suche anhand praktischer Messungen untersucht. Für die Datenstrukturen Θ-Graph mit Levelstruktur, BSPTree und transformierten BSPTree werden anhand realistischer Beispielszenen die jeweils optimalen Werte für freie Parameter (Anzahl Sektoren/Level, Blattgröße) bestimmt. Beim anschließenden Vergleich der Datenstrukturen stellt sich heraus, dass der BSPTree in der Praxis eindeutig am besten abschneidet. Inhaltsverzeichnis 1 Einführung 1.1 Walkthrough Systeme . . . . . . 1.2 Perspektivische Suche . . . . . . 1.3 Und die Datenstrukturen dazu . 1.3.1 Θ-Graph . . . . . . . . . 1.3.2 BSPTree . . . . . . . . . . 1.3.3 Transformierter BSPTree 1.4 Ergebnis . . . . . . . . . . . . . . 1.5 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 4 4 5 7 8 9 2 Implementierung 2.1 Allgemeiner Aufbau . . . . . . . . . 2.2 Generieren . . . . . . . . . . . . . . . 2.3 Preprocessing . . . . . . . . . . . . . 2.3.1 Aufbau des BSPTrees . . . . 2.3.2 Preprocessing des Θ-Graphen 2.4 Zeitmessung . . . . . . . . . . . . . . 2.5 Abfrage . . . . . . . . . . . . . . . . 2.5.1 BSPTree . . . . . . . . . . . . 2.5.2 Θ-Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 12 13 13 14 14 15 15 17 3 Messung 3.1 Vorgehensweise . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Ermittlung der optimalen Parameter . . . . . . . . . . . . 3.2.1 Blattgröße beim BSPTree . . . . . . . . . . . . . . 3.2.2 Sektoranzahl beim Θ-Graphen . . . . . . . . . . . 3.2.3 Levelanzahl beim Θ-Graphen . . . . . . . . . . . . 3.3 Vergleich der Datenstrukturen . . . . . . . . . . . . . . . . 3.4 Härtefälle . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Mit dem transformierten BSPTree in den Abgrund 3.4.2 Ab in die Wüste mit dem Θ-Graphen . . . . . . . 3.5 Ergebnis und Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 19 20 20 22 22 23 25 25 26 27 . . . . . . . . i Kapitel 1 Einführung 1.1 Walkthrough Systeme Walkthrough Systeme erlauben es einem Benutzer interaktiv in großen virtuellen Szenen zu navigieren. Diese Szenen bestehen normalerweise aus einzelnen Objekten, wie z.B. Häusern oder Autos. Diese Objekte sind wiederum aus Dreiecken modelliert und mit Texturen oder anderen Objekteigenschaften versehen. Eine Beispielszene ist in Abbildung 1.1(a) dargestellt. (a) Eine virtuelle Szene (b) Dieselbe Szene, nur dieses mal sind alle Objekte mit Zylindern umgeben. Die Schnitte der Zylinder mit der Bodenfläche ergeben eine Abstraktion der Szene, die nur aus Kreisen besteht (Abbildung 1.3(a)). Abbildung 1.1: Von der virtuellen Szene zu Kreisen Wir werden bei den Szenen nicht die einzelnen Dreiecke betrachten, sondern die aus Dreiecken zusammengesetzten Objekte. Das hat vor allem zwei Gründe: • Diese Struktur bietet sich an, da große virtuelle Szenen eigentlich ausnahmslos aus einzelnen Objekten zusammengesetzt werden. Seien es die einzelnen Möbel, die in einem Haus verteilt werden, oder die Häuser selbst, die zu einer Stadt zusammengesetzt werden. • Auf diese Weise lassen sich größere Szenen betrachten, da sich so weniger Objekte in einer Szene befinden als Dreiecke. Damit ein Benutzer sich flüssig in einem Walkthrough System bewegen kann, müssen mindestens 20 Bilder pro Sekunde angezeigt werden. Zwar können hardwarebeschleunigte Grafikkarten 1 2 KAPITEL 1. EINFÜHRUNG mittlerweile Szenen mit zigtausend Dreiecken genügend schnell rendern, es gibt jedoch immer Szenen, die die Kapazität der Grafikkarte deutlich übersteigen. In diesem Fall müssen Methoden gefunden werden, nur einen kleinen Teil der Szene auszuwählen, der dann an die Grafikkarte übergeben wird. Die Auswahlkriterien sollten dabei vor allem eine effiziente algorithmische Handhabbarkeit und der visuelle Eindruck sein. Walkthrough Systeme haben eine weitere Eigenschaft, die wir noch ausnutzen werden: Bei den Szenen handelt es sich häufig um 2 12 D Szenen. Mit 2 12 D bezeichnet man Szenen, die hauptsächlich auf einer Ebene liegen. Typischerweise ist diese Ebene der Boden auf dem die Objekte stehen. Dieser kann dabei sehr weitläufig sein, während die Höhe im Vergleich dazu stark eingeschränkt ist und nach oben und unten begrenzt ist. 1.2 Perspektivische Suche Ein Beispiel eines algorithmisch sehr schnellen Auswahlverfahrens ist die Kreisbereichsabfrage [8, 7, 5, 6]. Dabei werden alle Objekte zurückgeliefert, die innerhalb eines festgelegten Abstandes um den aktuellen Standort des Benutzers liegen. Diese Art von Abfragen ist sehr effizient, und man kann sie mit dem Θ-Graphen in output-sensitiver Zeit lösen. Allerdings ist der visuelle Eindruck nicht der beste: • Sehr große Objekte, die anfangs ausserhalb des Sichtbarkeitsradiuses liegen, erscheinen unvermittelt mit einer großen Fläche auf dem Bildschirm, wenn sich der Benutzer auf dieses Objekt zu bewegt. • Kleine Objekte, die innerhalb des Sichtbarkeitskreises liegen, werden die ganze Zeit über angezeigt, obwohl sie auf dem Bildschirm kaum zu sehen sind. Wie kann man nun besser entscheiden welche Objekte unwichtig sind? Ein Objekt, das gar nicht erst auf dem Bildschirm erscheint, da es kleiner als ein Pixel ist, kann getrost weggelassen werden. Objekte, die eine große Fläche auf dem Bildschirm einnehmen, sollten dagegen bevorzugt angezeigt werden. Man benötigt also eine Technik, die die Objekte danach auswählt, welche Fläche sie, vom Beobachter aus gesehen, auf dem Bildschirm einnehmen. Diese Art der Abfrage nennt man ,,perspektivische Suche“. Bei der perspektivischen Suche betrachtet man den Sichtbarkeitswinkel der Objekte und filtert diejenigen Objekte heraus, deren Sichbarkeitswinkel unter einem festgelegten Minimum liegt. Was ist der Sichtbarkeitswinkel eines Objektes? An dieser Stelle wollen wir zunächst ausnutzen, dass Walkthrough Szenen 2 12 D sind. Dadurch ist es möglich die 3D Szene zu Kreisen im 2D zu abstrahieren. Dazu umgeben wir alle Objekte mit einem Zylinder, der senkrecht auf dem Boden der Szene steht, wie in Abbildung 1.1(b) gezeigt. Die Schnitte der Zylinder mit dem Boden ergeben dann eine 2D Szene, die nur aus Kreisen besteht. Der Sichtbarkeitswinkel ist nun der Winkel, unter dem einer dieser Kreise vom Beobachter aus sichtbar ist. Bildschirm O1 α1 α3 O3 α2 O2 Abbildung 1.2: Der Sichtbarkeitswinkel von drei Kreisen In Abbildung 1.2 ist der Sichtbarkeitswinkel für drei Kreise exemplarisch dargestellt. Die Kreise O1 und O2 sind etwa gleich weit vom Beobachter entfernt. O1 ist jedoch kleiner, daher ist sein 1.2. PERSPEKTIVISCHE SUCHE 3 Sichtbarkeitswinkel α1 auch kleiner als α2 . Der Sichtbarkeitswinkel hängt aber nicht nur von der Größe der Kreise ab, sondern auch von deren Entfernung zum Beobachter. In der Abbildung sind die beiden Kreise O2 und O3 gleich groß, dennoch ist der Sichtbarkeitswinkel α3 von O3 kleiner als α2 , da der Kreis weiter vom Beobachter entfernt ist. Radius 6 5 4 3 2 1 500 400 0 100 300 200 300 x (a) Die ursprüngliche 2D Szene besteht aus 1732 Kreisen. 200 400 500 y 100 (b) Diese Kreise werden nun in 3D Punkte umgewandelt, indem der Radius als Z-Koordinate interpretiert wird. Radius 6 5 4 3 2 1 0 500 400 100 300 200 300 x 200 400 500 y 100 (c) Mit einer Kegelabfrage findet man alle Kreise, die von einem auf der Kegelspitze stehenden Beobachter, mindestens unter dem Winkel α sichtbar sind. Das Ergebnis dieser Kegelabfrage ist im obigen Bild dargestellt. (d) Das Ergebnis der Kegelabfrage im 2D: Alle Kreise, die vom roten Beobachterstandpunkt aus gesehen mindestens den Sichtbarkeitswinkel α haben, sind grün gefärbt. Abbildung 1.3: Ablauf der perspektivische Suche Um eine solche Suche effizient lösen zu können, werden zunächst die 2D-Kreise in 3D-Punkte umgewandelt, indem der Radius der Kreise als Z-Koordinate genommen wird (Abbildung 1.3(b)). Um nun alle Objekte zu finden, die vom Beobachter aus mindestens mit dem Winkel α sichtbar sind, muss man lediglich eine Kegelabfrage durchführen (Abbildung 1.3(c)). Dabei zeigt die Öffnung des Kegels immer senkrecht nach oben. Der Scheitelpunkt des Kegels liegt auf dem Beobachterpunkt (x0 , y0 ) und der Öffnungswinkel des Kegels beträgt 1 −1 α̃ = tan sin( α2 ) Wir wollen nun die dazugehörige Kegelgleichung entwickeln, dazu betrachten wir zunächst eine Kreisscheibe vom Beobachterpunkt (x0 , y0 ) aus mit Radius r in der Ebene. Damit ein Punkt (x, y) innerhalb dieses Kreises liegt, darf sein Abstand zum Mittelpunkt höchstens gleich dem Radius r sein. Als Gleichung ergibt sich sofort: (x − x0 )2 + (y − y0 )2 ≤ r2 4 KAPITEL 1. EINFÜHRUNG Im dreidimensionalen wird aus der Kreisscheibe ein Zylinder. Wir wählen nun den Radius der Kreisscheibe abhängig von der Höhe z der Punkte und erhalten die Gleichung für einen Kegel mit Öffnungswinkel α̃: (x − x0 )2 + (y − y0 )2 ≤ (z tan α̃)2 Genau die Punkte, die diese Gleichung erfüllen, befinden sich also im Kegel und sind damit mindestens mit dem Winkel α vom Benutzer aus sichtbar. 1.3 Und die Datenstrukturen dazu Ziel dieser Studienarbeit ist es, die praktische Tauglichkeit drei verschiedener Datenstrukturen für Kegelabfragen zu testen. Dabei werden diese Kegelabfragen nicht auf beliebigen geometrischen Objekten durchgeführt, sondern auf einer Punktmenge mit Größe N . Die Anzahl der bei einer Anfrage zurückgelieferten Punkte werden wir mit n bezeichnen. Da die Punktmenge fest ist, ist es möglich die Datenstrukturen vorher geeignet aufzubauen (Preprocessing), um während des interaktiven Walkthrough-Vorganges die Abfragen schneller bearbeiten zu können. Diese drei Datenstrukturen werden nun im Einzelnen vorgestellt: 1.3.1 Θ-Graph Der Θ-Graph ist eine Graphenstruktur auf den Punkten der Szene, die darauf ausgelegt ist, Kreisbereichsabfragen sehr schnell zu beantworten[7]. Der Θ-Graph besteht dazu aus einem Knoten pro Punkt in der Szene. Die Ebene wird um jeden Punkt herum in k Sektoren aufgeteilt, wobei k mindestens 6 sein muss. b b c Entfernung von a zu b a Entfernung von a zu c c d a (a) Die Punkte stellen die Knoten dar, die Sektorgrenzen sind durch durchgezogenen Linien angedeutet. Die Kanten des gerichteten Graphen sind mit Pfeilen dargestellt. (b) Entfernungsmessung beim Θ-Graphen anhand der Winkelhalbierenden eines Sektors. In diesem Beispiel ist c weiter von a entfernt als b, obwohl es nach der euklidischen Distanz (gestrichelter Kreisbogen) gerade andersherum gewesen wäre. Abbildung 1.4: Aufbau des Θ-Graphen Die Knoten sind dann pro Sektor jeweils mit dem benachbarten nächsten Knoten dieses Sektors verbunden (Abbildung 1.4(a)). Die Winkelhalbierende jedes Sektors stellt dazu die Achse zur Entfernungsmessung dar. Die Entfernung von Knoten b bezüglich eines Sektors von Knoten a ist dann der Abstand auf der Winkelhalbierenden von a bis zur orthogonalen Projektion von b auf diese Winkelhalbierende (Abbildung 1.4(b)). Durch diese Struktur können Kreisbereichsabfragen effizient gelöst werden, da nur ein kleiner Teil der Szene durchsucht werden muss, um alle Knoten zu finden, die die Kreisbereichsabfrage erfüllen[7]. Bei einer Kreisbereichsabfrage mit Mittelpunkt q und Radius r wird zunächst der Knoten q 0 gesucht, der am nächsten zum Mittelpunkt des Kreises liegt. Dieses benötigt O(log N ) Zeit. Bei 1.3. UND DIE DATENSTRUKTUREN DAZU 5 großen Szenen dauert das natürlich zu lange. Aber unter der Annahme, dass der Benutzer sich nur langsam durch die Szene bewegt, kann man das Suchen nach dem Startknoten umgehen: Man merkt sich nach jeder Suchanfrage, welcher der zurückgegebenen Knoten am nächsten beim Mittelpunkt lag und nimmt diesen bei der nächsten Suchanfrage als Startknoten. Der Θ-Graph hat eine rein output-sensitive Laufzeit, d.h. die Laufzeit ist nur von der Anzahl n der zurückgelieferten Punkte abhängig O(1 + n). Diese Laufzeit erreicht man durch die nun folgende Suche: Vom Startknoten q 0 aus sucht man mittels Breitensuche alle Knoten, die höchstens s · (r + dist2 (q, q 0 )) Einheiten entfernt sind. s ist dabei der Streckungsfaktor des Θ-Graphen [7] und ergibt sich durch ! r r 2Π 4 Π 1 + 48 sin , 5 − 4 cos s = max k k Von allen betrachteten Punkten gibt man wiederum nur diejenigen zurück, die innerhalb des Kreises mit Radius r liegen. Radius 6 5 4 3 2 1 0 500 400 100 300 200 300 x 200 400 500 y 100 Abbildung 1.5: Annäherung des Kegels mit Zylindern. Die Szene wurde in drei Level von 0-2, 2-4 und 4-6 eingeteilt. Die in schwarz gezeichnete Kegelabfrage wird daher durch drei blauen Zylinderabfragen vom Θ-Graphen angenähert. Punkte, die dabei im Zylinder, aber nicht im Kegel liegen, müssen am Ende noch herausgefiltert werden. Für eine perspektivische Suche braucht man aber keine Kreisabfrage sondern eine Kegelabfrage. Eine Kegelabfrage kann man jedoch durch eine Reihe von Zylinderabfragen annähern, indem man die Punkte der Szene abhängig von ihrer Höhe in unterschiedliche Level einteilt. (Abbildung 1.5). Die Punkte innerhalb dieser Zylinder findet man dann mit einer Kreisabfrage, bei der man die ZKoordinate ignoriert. Um also den Θ-Graphen für Kegelabfragen zu nutzen, zerlegt man die ganze Szene in Z-Richtung in einzelne Level und baut pro Level eine Θ-Graph Struktur auf, mit der man die Zylinderanfragen beantwortet. Da der Kegel nicht genau durch die Zylinder angenähert werden kann, müssen am Ende noch die Punkte herausgefiltert werden, die nicht im Kegel liegen. Parameter des Θ-Graphen Beim Θ-Graphen lässt sich die Anzahl der Sektoren k und die Anzahl der Level l frei wählen. Optimale Werte für diese beiden Parameter werden in Abschnitt 3.2.2 und 3.2.3 ermittelt. 1.3.2 BSPTree Der Begriff des BSPTree bezeichnet einen binären Baum, der auf den geometrischen Objekten des Raumes aufgebaut wird und bei dem jeder Knoten der Unterteilung des Raumes in zwei Hälften 6 KAPITEL 1. EINFÜHRUNG entspricht [2]. Wie der Raum geteilt wird, ist dabei von Fall zu Fall unterschiedlich. Gängige Varianten teilen entweder das Volumen oder die Anzahl der Objekte zur Hälfte. Da wir es in unserem Fall mit einer Punktmenge zu tun haben, bietet es sich an, die Punkte nach ihrer Anzahl zu teilen. Die Unterteilung sollen dabei mit einer Ebene erfolgen, die parallel zur X, Y oder Z-Ebene sind. Die Auswahl dieser Schnittebene erfolgt dann durch die AspectRatio des entstehenden Quaders. In [4] wird für k-d trees 1 , bei denen der Schnitt immer entlang der längsten Richtung der Hüllbox stattfindet, eine worst case Laufzeit für approximative konvexe Bereichsabfragen von O(n+logd−1 N ) gezeigt. Dieser k-d tree hat aber wiederum große Ähnlichkeit mit dem von uns verwandten BSPTree. p1 s1 p8 p2 s2 p5 p3 s2 s3 s3 p7 p4 s1 p6 p1 p3 p5 p6 p2 p4 p8 p7 Abbildung 1.6: Aufbau des BSPTrees Wir wollen das Konstruktionsprinzip des BSPTrees an einem Beispiel im zweidimensionalen erklären (Abbildung 1.6): Die Blattgrösse betrage 2. Die Konstruktion beginnt mit der rot gestrichelten Hüllbox, die die ganze Szene umschliesst. Eine Unterteilung ist nun in zwei Richtungen möglich: Einmal in X-Richtung und einmal in Y -Richtung. Bei einem Schnitt in X-Richtung würden die Punkte {p1 , p2 , p3 , p4 } auf die eine Hälfte und {p5 , p6 , p7 , p8 } auf die andere Hälfte aufgeteilt. Bei einem Schnitt in Y -Richtung ergibt sich die Teilung {p1 , p2 , p5 , p8 } und {p3 , p4 , p6 , p7 }. Die Wahl zwischen diesen beiden Möglichkeiten fällen wir anhand der Aspect-Ratio. Die Aspect-Ratio eines zweidimensionalen Hüllbox ist durch ihr Seitenverhältnis ∆x ∆y definiert. Die Hüllbox ist genau dann ein Quadrat, wenn die Aspect-Ratio Eins ist. Je mehr die Aspect-Ratio von Eins entfernt ist, desto länger bzw. schmaler wird die Hüllbox, bis sie schließlich zu einer Linie entartet. Im BSPTree sollen die Hüllboxen möglichst quadratische Form haben, daher betrachten wir die Abweichung der Aspect-Ratio von Eins: 1 − ∆x ∆y 4 Im Beispiel ergibt sich bei Schnitt in X-Richtung 1 − 88 + 1 − 10 = 0.6 als Abweichung der 1 − 14 + Aspect-Ratio der beiden neuen Hüllboxen, für die Teilung in Y -Richtung ergibt sich 2 1 − 7 = 3.25. Der erste Schnitt s1 findet demnach in X Richtung statt. Dann wird im Baum ein 4 Knoten für den Schnitt s1 angelegt, in dem die zugehörige rote Hüllbox gespeichert wird. Für die beiden Hälften ergeben sich neue Hüllboxen und die Punkte werden auf die beiden Hälften aufgeteilt. Die Konstruktion fährt fort, indem beide Hälften rekursiv unterteilt werden, bis die Anzahl der Punkte die Blattgröße unterschreitet. Im Beispiel beträgt diese Blattgröße 2. Diese Punkte werden dann zusammen mit der zugehörigen Hüllbox in einem Blatt des BSPTrees gespeichert. Dieses Konstruktionsprinzip lässt sich direkt auf den dreidimensionalen Fall übertragen. Es kommen lediglich Schnitte in Z-Richtung dazu und die Aspect-Ratio muss anders berechnet werden: 1 Eine weitere Variante der BSPTrees 1.3. UND DIE DATENSTRUKTUREN DAZU 7 Beim dreidimensionalen BSPTree sind die Hüllboxen keine Rechtecke sondern Quader. Um möglichst günstige Hüllboxen zu finden, werden hier die Aspect-Ratios der drei unterschiedlichen Seiten des Quaders betrachtet. Damit der Quader möglichst Würfelform hat, betrachten wir die Summe der Aspect-Ratios der drei Seiten 1 − ∆x + 1 − ∆y + 1 − ∆z ∆y ∆z ∆x Diese Summe ist genau Null, wenn es sich um einen Würfel handelt und wird größer je mehr der Quader entartet. Da bei jedem Schnitt dafür gesorgt wird, dass die Punkte gleichmässig auf die beiden Hälften aufgeteilt werden, halbiert sich bei jedem Schnitt die Anzahl der Punkte und spätestens nach dlog(N )e Schritten ist man bei einem Punkt angelangt. Die Tiefe eines BSPTrees, der auf diese Art gebildet wird, kann daher höchstens dlog(N )e sein. Nach dieser Vorbereitung ist der BSPTree bereit für Anfragen mit beliebigen geometrischen Objekten. Die Abfrage besteht nun lediglich daraus, den Baum rekursiv zu durchlaufen und dabei zu überprüfen, ob die Hüllboxen den Kegel schneiden. Wenn der Kegel eine Hüllbox eines Knotens nicht schneidet, braucht man die Teiläste unterhalb des Knotens nicht mehr zu betrachten. Alle anderen Knoten, deren Hüllbox von dem Kegel geschnitten wird, müssen betrachtet werden. Wenn man bei einem Blatt ankommt, werden schließlich alle Punkte daraus zurückgeliefert, die innerhalb des Kegels liegen. Parameter des BSPTree Beim BSPTree ist frei wählbar, wieviele Punkte in einem Blatt liegen sollen (Blattgröße b). Welcher Wert sich am besten für die Blattgröße eignet, wird in Abschnitt 3.2.1 gemessen. 1.3.3 Transformierter BSPTree In [11] wird beschrieben, wie man die meisten geometrische Abfragen in Halbraumabfragen umwandelt. Dazu müssen lediglich die Abfrage und alle Punkte auf geeignete Weise transformiert werden. Normalerweise geht dies nur mit einer Dimensionszunahme. Im vorliegenden Fall lässt sich dieses aber verhindern, da alle Kegel • den gleichen Öffnungswinkel haben • in Z-Richtung orientiert sind • und der Apex auf der XY -Ebene liegt. Auf diese Weise soll jetzt die 3D Kegel-Abfrage in eine 3D Halbraumabfrage der Form ax+by+cz ≥ d umgeschrieben werden (Abbildung 1.7). Dazu wird zuerst die Kegelgleichung auf geeignete Weise äquivalent umgeformt: (x − x0 )2 + (y − y0 )2 ≤ z 2 tan2 α̃ ⇔ x2 − 2x0 x + x20 + y 2 − 2y0 y + y02 ≤ z 2 tan2 α̃ ⇔ (2x0 )x + (2y0 )y − x2 − y 2 − x20 − y02 ≥ −z 2 tan2 α̃ ⇔ (2x0 )x + (2y0 )y + 1 · (z 2 tan2 α̃ − x2 − y 2 ) ≥ x20 + y02 Mit z̃ = z 2 tan2 α̃ − x2 − y 2 ergibt sich: (2x0 )x + (2y0 )y + 1 · z̃ ≥ x20 + y02 Die letzte Gleichung hat schon große Ähnlichkeit mit einer Halbraumabfrage. Als Koeffizienten a, b, c und d der Halbraumabfrage ergeben sich 2x0 , 2y0 , 1 und x20 + y02 respektive. Bei den Punkten ist lediglich die Z-Koordinate z in z̃ zu tranformieren. Der Öffnungswinkel des Kegels α̃ wird bei dieser Transformation fest mit der Datenstruktur verknüpft. Abfragen sind daher später nur mit dem Öffnungswinkel möglich, der beim Aufbau der Datenstruktur verwendet wurde. 8 KAPITEL 1. EINFÜHRUNG Radius 200000 100000 0 −100000 −200000 −300000 500 −400000 −500000 400 100 300 200 300 x 200 400 500 y 100 Abbildung 1.7: Wirkung der Transformation auf die Punktmenge. Die vom Kegel zur Halbraumabfrage transformierte Abfrage aus 1.3(b) ist in blau eingezeichnet (der Halbraum ist immer der Raum oberhalb dieser Ebene, da die z Koordinate des Normalvektors konstant 1 ist). Die grünen Punkte liegen innerhalb dieses Halbraumes. Der Vorteil dieser Transformation liegt darin, dass es für 3-dimensionale Halbraumabfragen Datenstrukturen mit nahezu linearer Größe O(N · log log N ) gibt, die solche Abfragen in Laufzeit O(polylog N + n) ermöglichen [9] oder mit Größe O(N 1+ε ) und Laufzeit O(log N + n) [3], siehe auch Theorem 4.7 in [8]. Allerdings sind diese Datenstrukturen wegen der im O-Kalkül versteckten Konstanten und randomisierter Konstruktion in der Praxis wenig brauchbar. Deshalb werden wir im folgenden für die Halbraumabfragen den im letzten Abschnitt beschriebenen BSPTree verwenden. Zusammen mit der vorgeschalteten Transformation wird diese Datenstruktur dann als transformierter BSPTree bezeichnet. Da man die Transformation nicht ohne Rundungsfehler rückgängig machen kann, sollte in jedem Punkt zum transformierten Punkt auch zusätzlich der ursprüngliche Punkt mitgespeichert werden. Dadurch benötigt diese Datenstruktur doppelt so viel Arbeitsspeicher wie der normale BSPTree. 1.4 Ergebnis Ein Ergebnis der Messungen sind die optimalen Werte für freie Parameter der einzelnen Datenstrukturen. Beim BSPTree und dem transformierten BSPTree hat sich eine Blattgröße von 16 bis 64 als Beste herausgestellt. Der Θ-Graph ist am schnellsten mit 15 Sektoren und etwa 10 bis 30 Leveln. Bei den Messungen hat sich ergeben, dass der BSPTree die am besten geeignete Datenstruktur für Walkthrough Systeme ist. Die Abfragezeiten beim transformierten BSPTree sind von der Position des Beobachters abhängig und der Θ-Graph ist sehr empfindlich gegenüber Wüsten in der Szene. Nur der BSPTree zeigt durchgängig eine sehr gute Laufzeit und verbraucht ausserdem am wenigsten Speicher von allen Datenstrukturen. Obwohl die Laufzeit des BSPTrees nicht output-sensitiv ist, spielt dies mindestens bei Szenen mit bis zu 200 Millionen Punkten keine Rolle. 1.5. ÜBERBLICK 1.5 9 Überblick Im ersten Kapitel wurden die Grundlagen der perspektivischen Suche und der dafür benutzten Datenstrukturen erläutert. Im zweiten Kapitel wird näher auf die konkrete Implementierung eingegangen: Es wird erklärt wie realistische Szenen erzeugt werden, wie der Aufbau und die Abfrage bei den einzelnen Datenstrukturen abläuft und wie die Laufzeit gemessen wird. Im letzten Kapitel geht es dann vorallem um die einzelnen Messungen und um die Ergebnisse, die sich aus diesen Messungen ergeben. 10 KAPITEL 1. EINFÜHRUNG Kapitel 2 Implementierung Aus Gründen der besseren Flexibilität und Plattformunabhängigkeit wurde als Programmiersprache Java gewählt. Durch die Wahl der Programmiersprache wird der Vergleich der Datenstrukturen untereinander nicht beeinflusst, daher sind die Ergebnisse auch auf andere Sprachen übertragbar. 2.1 Allgemeiner Aufbau Abfragen virtuelle Szene Generieren Datenstruktur Preprocess Ergebnis Abfrage Überprüfen Abbildung 2.1: Aufbau Im Prinzip besteht eine Messung aus vier Phasen: Da wäre zuerst das automatische Generieren von Punkten und Abfragen. Große Testszenen von Hand zu erzeugen, hätte viel zulange gedauert. Daher werden einige Algorithmen entwickelt, die das Erstellen von großen Szenen zu einer Sache von einigen Minuten machen (siehe Abschnitt 2.2). Außerdem ist es möglich, Abfragedateien anhand einer vorgegebenen Stützpunktdatei zu generieren. Dabei werden die Stützpunkte zu einem Polygon verbunden und die Abfragen auf dieser Linie verteilt. Eine auf diese Weise generierte Szene kann man dem Preprocessor übergeben. Dieser liest die Punkte ein und baut auf diesen die gewünschte Datenstruktur auf, um diese schließlich binär in eine Datei zu schreiben. Alle Datenstrukturen benötigen länger als O(N ) für das Preprocessing. Daher ist es schneller, die Datenstruktur nach dem Preprocessing abzuspeichern und sie bei späteren Anfragen zu laden, als sie für jede Anfrage neu aufzubauen. Die Algorithmen zum Preprocessing werden in Abschnitt 2.3 beschrieben. In der dritten Phase, der Abfrage, werden die Datenstrukturen wieder eingelesen, zusammen mit einer Abfragedatei, in der pro Zeile eine Kegelabfrage beschrieben ist. Auf die eingelesene Datenstruktur werden nun alle Kegelabfragen aus der Abfragedatei der Reihe nach ausgeführt. Das Ergebnis dieser Abfragen wird am Ende in einer Datei gespeichert. Was bei der Abfrage außerdem zu beachten ist und wie sie genau abläuft findet sich in Abschnitt 2.5. Da man einem Computerprogramm nie trauen sollte, wird im letzten Schritt das Ergebnis mit dem trivialen – und dadurch vertrauenswürdigen – Such-Algorithmus überprüft. Dieser wendet die Kegelgleichung sequentiell auf jeden Punkt an und vergleicht das Ergebnis mit dem Ergebnis, dass der BSPTree oder Θ-Graph zurückgeliefert hat. Bei Unterschieden zwischen den beiden Ergebnissen erscheint eine Fehlermeldung. Jetzt werden die ersten drei Phasen noch genauer beschrieben, indem näher auf die verwendeten Algorithmen eingegangen wird. 11 12 2.2 KAPITEL 2. IMPLEMENTIERUNG Generieren Es gibt drei verschiedene Algorithmen zum automatischen Erzeugen großer virtueller Szenen. (a) Bei Szenen, die mit dem Random Algorithmus generiert sind überschneiden sich die Kreise unrealistischerweise. (b) Der Nonintersect Algorithmus generiert künstlerisch wertvolle Szenen ziemlich langsam. (c) Die ersten 1000 Punkte einer großen Landgen Szene. Wenn die Punkte einer Szene richtig sortiert werden, bilden die Teilmengen der Szene konzentrische Kreise. Abbildung 2.2: Die verschiedenen Algorithmen zum Generieren von Szenen in Aktion. Zunächst der einfachste, der Random Algorithmus. Hierbei werden n Punkte gleichverteilt im Raum erzeugt. Die so erzeugte Szene kann man zwar nicht als realistisch bezeichnen, sie eignet sich jedoch sehr gut zum Testen der Datenstrukturen. Der langsamste Algorithmus ist der Nonintersect Algorithmus. Die Idee hierbei ist es, Kreise zu erzeugen, die sich nicht überschneiden. Dazu werden die Kreise zufällig platziert und dabei jeweils überprüft, ob sie irgendeinen der anderen Kreise schneiden. Das ganze wird mit wachsender Kreisanzahl sehr langsam, und daher ist dieser Algorithmus für mehr als 20.000 Punkte praktisch nicht zu gebrauchen. Eine Weiterentwicklung stellt der Landgen Algorithmus dar, benannt nach dem gleichnamigen C++ Programm von Jens Krokowski der Universität Paderborn zum Generieren von großen Open Inventor Szenen. Bei diesem Algorithmus werden große Szenen aus vielen kleinen Kacheln generiert. Jede Kachel enthält dabei einige zufällig generierte nichtschneidende Kreise. Beim zufälligen Aneinandersetzen der Kacheln ergibt sich dann eine große Szene mit Kreisen, die sich nicht überschneiden. Damit die Kreise hinterher nicht zu regelmäßig sind, werden die Radien vor dem Abspeichern noch einmal pertubiert. Für das schnelle Erzeugen von Teilmengen ist es außerdem nützlich, wenn die Kreise nach ihrer Entfernung zu einem Bezugspunkt sortiert werden. Abbildung 2.3: Generieren von Abfragen. Die schwarzen Rechtecke sind die Stützpunkte. Die grauen Punkte sind die generierten Abfragen. Der letzte Algorithmus ist zwar ebenfalls ein Generator, erzeugt jedoch keine Punkte sondern Abfragen. Da die Algorithmen für Walkthrough Systeme eingesetzt werden sollen, bietet es sich an, die Algorithmen mit einer Reihe von Abfragen zu testen, die dem schrittweisen Umherlaufen 2.3. PREPROCESSING 13 Unterteile(node, coords, f rom, to, maxP ointN umber) 1 sortiere coords.x von from bis to nach X-Koordinate 2 sortiere coords.y von from bis to nach Y -Koordinate 3 sortiere coords.z von from bis to nach Z-Koordinate 4 5 6 7 8 node.boundingBox .lowerCorner ← (coords.x [from].x, coords.y[f rom].y, coords.z[f rom].z) node.boundingBox .upperCorner ← (coords.x [to −1].x, coords.y[to − 1].y, coords.z[to − 1].z) if (to − from) ≤ maxPointNumber then node.points ← coords.x [from . . to −1] else richtung ← FindeBesteUnterteilung(coords, from, to) 9 teile Punkte in der ermittelten Richtung auf die beiden Hälften auf j (from + to) 2 k 10 middle ← 11 Unterteile(node.leftchildren, coords, from, middle, maxPointNumber ) Unterteile(node.rightchildren, coords, middle, to, maxPointNumber ) 12 Abbildung 2.4: Preprocessing des BSPTrees eines Benutzers entsprechen. Die einfachste Art solche Abfragen zu generieren, ist anzunehmen, der Benutzer laufe in gerader Linie eine Reihe von Punkten ab. Dazu liest der Algorithmus eine Stützpunktdatei ein und verteilt die Abfragen gleichmäßig auf den Verbindungslinien zwischen den einzelnen Punkten (Abbildung 2.3). Dadurch ist es auch möglich, unterschiedliche “Geschwindigkeiten” des Benutzers zu simulieren: Wenn einige Segmente eine kürzere Länge im Vergleich zu anderen Segmenten haben, werden dort die Abfragen näher beeinander liegen. 2.3 Preprocessing Beim Preprocessing werden die einzelnen Datenstrukturen auf den Punkten aufgebaut. Dabei gibt es nur zwei prinzipiell unterschiedliche Datenstrukturen: den BSPTree und den Θ-Graphen. Zunächst der BSPTree. 2.3.1 Aufbau des BSPTrees Beim Aufbau des BSPTrees muss man sich vor allem um zwei Sachen kümmern: Die Punkte müssen auf die beiden Hälften aufgeteilt werden und die Hüllbox der beiden Hälften muss berechnet werden. Das Aufteilen der Punkte lässt sich durch Sortieren der Punkte in Schnittrichtung erreichen, dadurch bekommt man auch das Berechnen der Hüllbox geschenkt. Doch zuerst wollen wir uns überlegen, wie man eine Hüllbox berechnet. Um ein Quader bzw. in diesem Fall eine Hüllbox vollständig zu definieren, benötigt man nur zwei Eckpunkte der Hüllbox. Diese Eckpunkte stimmen jedoch nicht mit Punkten aus der Szene überein. Der untere ergibt sich jeweils aus der minimalen X,Y und Z-Koordinate aller Punkte und der obere aus der maximalen X,Y und Z-Koordinate aller Punkte. Das bringt uns auf eine Idee zum Berechnen der Hüllbox: Wir nehmen drei Arrays mit Referenzen auf die Punkte der Szene. Bei dem ersten Array sortiert man die Referenzen nach den X-Koordinaten der referenzierten Punkte, bei dem zweiten Array sortiert man nach den Y -Koordinaten und beim dritten Array nach den Z-Koordinaten. Nun stehen die Koordinaten des unteren Eckpunktes der Hüllbox an der ersten Position der Arrays und die des oberen Eckpunktes stehen an der letzten Position des Arrays. Genau dies ist es auch, was im Programm 2.4 in den Zeilen 1 bis 3 gemacht wird. Dabei besteht die Variable coords aus drei Arrays coords.x , coords.y und coords.z die jeweils Referenzen auf die Punkte enthalten. In Zeile 4 und 5 wird dann die Hüllbox im Baumknoten gespeichert. 14 KAPITEL 2. IMPLEMENTIERUNG So weit so gut, doch wir müssen ja noch die Punkte in zwei Hälften teilen und dazu müssen wir erstmal wissen, in welche Richtung überhaupt geteilt werden soll. Hierbei kommt uns zugute, dass die Arrays sortiert sind. Das erste Array ist nach den X-Koordinaten der Punkte sortiert. Wenn in X-Richtung geschnitten wird, ergibt die Teilung des Arrays in der Mitte daher direkt die Aufteilung der Punkte. Analog ist es bei einem Schnitt in Y oder Z Richtung, hier muss dann lediglich das zweite bzw. dritte Array betrachtet werden. In der Methode FindeBesteUnterteilung werden die drei möglichen Richtungen ausprobiert und diejenige zurückgeliefert, die die beste AspectRatio erzielt. Da diese Methode vor allem aus dem Rechnen mit Indizes besteht und ansonsten nicht weiter interessant ist, wird darauf verzichtet sie hier explizit aufzuschreiben. Nach dem Aufteilen der Punkte auf die beiden Hälften, wird die ganze Prozedur rekursiv auf die beiden Hälften ausgeführt. Die Rekursion endet, wenn die Anzahl der Punkte, die geteilt werden sollen, den Parameter maxPointNumber unterschreitet. Die Punkte werden dann komplett im aktuellen Blatt gespeichert. Der Hauptaufwand beim Preprocessing ist das Sortieren der Arrays. Dadurch das bei jedem Schnitt das Array sortiert wird, fallen insgesamt dlog N e Sortiervorgänge aller N Punkte an, da der BSPTree höchstens die Tiefe dlog N e hat. Sortieren benötigt im allgemeinen O(N log N ), daher beträgt der Aufwand für das Preprocessing O(N log2 N ). Beim Aufteilen der Punkte kann man das Preprocessing möglicherweise noch ein wenig beschleunigen, wenn man die vorhandene Sortierung der Arrays ausnutzt. Dadurch entfällt das andauernde Sortieren und die Laufzeit sinkt auf O(N log N ), da nur am Anfang einmal alle Punkte sortiert werden müssen. Dieses wurde allerdings nicht weiter durchdacht und implementiert, da das Preprocessing der BSPTrees eher am Speicherverbrauch scheiterte als an der fehlenden Rechenleistung. 2.3.2 Preprocessing des Θ-Graphen Der Θ-Graph besteht aus mehreren Leveln. Daher müssen die Punkte zunächst auf diese Level aufgeteilt werden, bevor das eigentliche Preprocessing des Θ-Graphen auf den einzelnen Leveln durchgeführt wird. Das Aufteilen geschieht, indem die Punkte bezüglich ihrer Z-Koordinate sortiert werden und dann gleichmässig in die einzelnen Level eingeordnet werden. Der Aufbau der ΘGraphen läuft dann mit dem schnellen Preprocessing Algorithmus in Zeit O(N log N ) ab (für Details zu diesem Algorithmus siehe [8, 5]). 2.4 Zeitmessung Bevor wir zur dritten Phase der Abfrage kommen, zunächst ein paar Worte über die Zeitmessung von Programmen im Allgemeinen und zur Zeitmessung unter Java im Speziellen. Bei der Zeitmessung sind mehrere Punkte zu beachten. Was genau gemessen werden soll, ist die erste Frage, die man sich stellen sollte. Die Java VM von Sun kompiliert den Java Bytecode zur Laufzeit in native Maschinenanweisungen, sogenanntes Just in Time Compiling. Alle Messungen sollten daher nur mit abgeschalteten JIT Compiler der Java VM stattfinden, da man nicht vorhersehen kann, wann und wie optimiert wird. Ein weiteres Problem besteht im Garbage Collector von Java, der von Zeit zu Zeit von einem nebenläufigen Thread aus gestartet wird, um unbenutzte Objekte aus dem Speicher zu entfernen. Idealerweise sollte man diesen vor der Messung abschalten und nach der Messung wieder starten. Das ist jedoch in der aktuellen Sun Java VM nicht möglich. Als einfacher Ausweg bleibt, den Garbage Collector vor der Messung manuell zu starten in der Hoffnung, dass er während der Messung nicht mehr in Aktion tritt. Weiterhin sollte sich die Zeitmessung nicht beeinflussen lassen, wenn gleichzeitig zur Messung noch andere Programme laufen. Da unter allen Betriebssystemen meistens eine Vielzahl von Programmen im Hintergrund laufen, könnte ansonsten die Messung verfälscht werden. Es werden nun drei verschiedene Möglichkeiten zur Zeitmessung unter Java vorgestellt: 1. Zuerst wäre da die Standardmethode: Mittels System.getCurrentTimeMillis() erhält man die aktuelle Systemzeit mit Millisekunden Genauigkeit. Nachteil ist natürlich, dass Messungen beim Laufen von rechenintensiven Hintergrundprozessen sowie durch gleichzeitige Garbage Collection verfälscht werden. 2.5. ABFRAGE 15 2. Eine weitere Methode gibt es über das JVMPI Interface zur Java VM. In diesem Profiler Interface ist eine experimentelle Methode vorgesehen, um die CPU-Zeit des aktuellen Threads abzufragen. Mittels JNI ist es möglich, diese Methode auch dem Java Programm zur Verfügung zu stellen [10]. Leider ist diese Methode unter Linux tatsächlich nur experimentell und gibt lediglich die Systemzeit mit Nanosekunden Genauigkeit zurück. Sie lässt sich also durch andere Programme verfälschen, die auf dem Rechner laufen. Außerdem schwanken die unteren Stellen bei Nanosekundengenauigkeit sehr stark von Messung zu Messung und sind damit unbrauchbar für eine genaue Messung. 3. Die beste Möglichkeit ist, mittels JNI auf Unix/Linux-Funktionen zurückzugreifen, die die von einem Prozess verbrauchte Zeit zurückliefern. Der Scheduler des Kernels muss schließlich genau im Auge behalten, welcher Prozess wieviel Prozessorzeit verbraucht hat. Es gibt gleich drei Funktionen, die die Prozess Zeit zurückliefern: Zum einen wären da clock und times, die jedoch sehr ungenaue Werte liefern, da die an diese Funktionen gelieferten Werte vom Kernel auf Zehntelsekunden gerundet werden. Die genaueste Funktion ist getrusage. Mit getrusage ist es möglich, die Prozesszeit mit Mikrosekunden Genauigkeit zu erhalten. Da die unteren Stellen aber sehr ungenau sind und für unsere Messungen auch Millisekunden ausreichen, wird die von getrusage gelieferte Zeit auf Millisekunden gerundet. Mit dieser Methode ist es möglich, alle oben beschriebenen Probleme zu umgehen. Da getrusage nur die vom Prozess benötigte Zeit misst, wird die Messung nicht von andere Programmen, die gleichzeitig auf dem Rechner laufen, gestört. Und da der Garbage Collector ebenfalls in einem anderen Thread als das Programm selbst läuft, wird auch die Zeit ignoriert, die der Garbage Collector verbraucht. 2.5 Abfrage Der grobe Ablauf einer Abfrage ist bei allen drei Algorithmen gleich. Zuerst wird die Datenstruktur und eine Datei mit Abfragen eingelesen. In einer Abfragedatei können dabei mehrere Abfragen stehen, um den Vorteil des Preprocessings richtig auszunutzen. Nun werden die Abfragen einzeln auf die eingelesene Datenstruktur angewandt. Dazu gibt es bei den Datenstrukturen jeweils zwei Varianten der Suchmethode. Die erste läuft mit Zählern, die alle wichtigen Vorkommnisse während des Suchens mitprotokollieren. Um einen Einfluss dieser Zähler auf die Zeitmessung auszuschließen, läuft die zweite Variante komplett ohne Zähler. Der Ablauf bei einer einzelnen Messungen ist nun so, dass zuerst die Suche ohne Zähler hundert mal durchgeführt wird. Dabei wird jede Suche einzeln gemessen. Aus diesen Messungen wird der Durchschnitt gebildet, außerdem werden die minimale und die maximale Dauer als Maß für die Streuung des Messwertes festgehalten. Danach wird die Suche noch einmal mit Zählern durchlaufen, um nähere Informationen über den Suchvorgang zu erhalten. In den Zählern wird z.B. festgehalten, wieviele Knoten beim Θ-Graphen besucht wurden oder wie viele Schnitte mit Hüllboxen beim BSPTree nötig waren. Die Werte dieser Zähler werden zusammen mit den Werten der Zeitmessung und den Daten für die Kegelabfrage (Kegelspitze und Öffnungswinkel), vor den Punkten, die innerhalb des Kegels liegen, im sogenannten QueryHeader abgespeichert. Dabei werden alle Abfragen hintereinander in eine Datei geschrieben, die Ergebnispunkte sind dann jeweils durch einen QueryHeader voneinander getrennt. Wir wollen nun näher auf die eigentliche Suche bei den einzelnen Datenstrukturen eingehen. 2.5.1 BSPTree Nur im Detail unterscheidet sich die Suche beim normalen und dem transformierten BSPTree. Die Grundstruktur der Suche ist bei beiden gleich und sehr einfach (Programm 2.5): Alle Knoten, deren Hüllbox die Abfrage schneidet, werden rekursiv durchsucht. Wenn ein Knoten ein Blatt ist, werden alle in diesem Knoten enthaltenen Punkte, die die Abfrage erfüllen, zum Ergebnis hinzugefügt. Der einzige Unterschied zwischem normalen und transformiertem BSPTree findet sich in der intersectBox Methode: Beim normalen BSPTree muss ein Kegel mit der Hüllbox geschnitten werden, beim transformierten wird überprüft, ob die Hüllbox einem bestimmten Halbraum schneidet. 16 KAPITEL 2. IMPLEMENTIERUNG Schneiden(node, query, result) 1 if intersectBox(query, node.boundingBox ) 2 then 3 if node.points 6= nil 4 then for i ← 0 to node.points.length −1 5 do if intersectPoint(query, points[i]) 6 then result ← result ∪ points[i] 7 else Schneiden(node.leftchildren, query, result) 8 Schneiden(node.rightchildren, query, result) Abbildung 2.5: Schneiden des BSPTrees Beide Abfragen benötigen offenbar konstante Laufzeit. In der Praxis macht sich diese Konstante jedoch bemerkbar, da die Suche im BSPTree im Prinzip nur aus Schnitten besteht. Daher folgende Gedanken zur schnellen Realisierung der Schnitte: Schnitt von Halbraum und Hüllbox Beim transformierten BSPTree haben wir es mit Halbraumabfragen der Form 2x0 x + 2y0 y + z ≥ x20 + y02 zu tun. Um zu überprüfen, ob die Hüllbox den Halbraumes schneidet, kann man nun einfach alle Eckpunkte der Hüllbox in die Halbraumgleichung einsetzen. Sobald einer der Eckpunkte die Gleichung erfüllt, schneidet die Hüllbox den Halbraum. Man kann die Überprüfung aber auch noch ein wenig effizienter gestalten: Da die Hüllboxen nicht durch Schnitte mit schiefen Ebenen entstehen, sondern nur durch Schnitte mit Ebenen, die parallel zur X, Y oder Z-Ebene sind, sind auch die Seiten der Hüllbox immer parallel zur X,Y und Z-Ebene. Insbesondere hat die Hüllbox vier obere und vier untere Eckpunkte. Man beachte nun, dass z in der obigen Gleichung den Koeffizienten 1 hat. Der Normalvektor der Halbraumebene zeigt also immer in die obere positive Hälfte. Damit ausschließlich untere Eckpunkte im Halbraum liegen, müsste der Normalvektor der Halbraumebene in die negative Hälfte zeigen. Der Normalvektor zeigt allerdings in die positive Hälfte und daher muss immer ein oberer Eckpunkt mit im Halbraum liegen, wenn ein unterer Eckpunkt im Halbraum liegt. Es genügt daher zu überprüfen, ob einer der oberen vier Eckpunkte im Halbraum liegt. Schnitt von Kegel und Hüllbox Was auf den ersten Blick sehr kompliziert aussieht, wird sich als überraschend einfach herausstellen. Dabei werden wir vor allem die Position der Hüllboxen und des Kegels ausnutzen. Erinnern wir uns zunächst an die Erkenntnis aus dem letzten Abschnitt: Die Seiten der Hüllbox sind immer parallel zur X,Y und Z Ebene und die Hüllbox hat vier obere und vier untere Eckpunkte. Die vier oberen Eckpunkte ergeben dabei das obere Begrenzungsrechteck, die vier unteren ergeben das untere Begrenzungsrechteck. Betrachten wir nun den Kegel. Der Kegel steht senkrecht und die Öffnung zeigt immer nach oben. Außerdem stimmt die Kegelspitze mit dem Boden der Hüllbox überein, die die ganze Szene umgibt. Nach oben ist der Kegel prinzipiell unbeschränkt und es kann daher nicht sein, dass der Kegel in die Seitenfläche einer Hüllbox ragt, ohne dass er das obere Begrenzungsrechteck schneidet. Ebenso ist es unmöglich, dass der Kegel das untere Begrenzungsrechteck schneidet, wenn das obere Begrenzungsrechteck nicht geschnitten wird. Es reicht daher zu schauen, ob das obere Begrenzungsrechteck den Kegel schneidet. Wir haben also nun den Schnitt von Kegel und Hüllbox auf den Schnitt von Kreis und Rechteck im zweidimensionalen reduziert, der sich effizient lösen lässt. 2.5. ABFRAGE 17 Suche(startNode, range, stretchFactor , maxMarker ) 1 rangeSquare ← range · range 2 stretchFactorSquare ← stretchFactor · stretchFactor 3 searchRangeSquare ← rangeSquare · stretchFactorSquare 4 result ← ∅ 5 openNodes ← ∅ 6 enqueue(openNodes, startNode) 7 while openNodes 6= ∅ 8 do node ← dequeue(openNodes) 9 distance ← distanceSquared(startNode.location, node.location) 10 if distance ≤ searchRangeSquare 11 then if distance ≤ rangeFactorSquared 12 then result ← result ∪ node 13 for i ← 0 to node.neighbours.length −1 14 do if node.neighbours[i ] 6= nil and node.neighbours[i ]. marker ≤ maxMarker 15 then enqueue(openNodes, node.neighbours[i ]) 16 node.neighbours[i ]. marker ← maxMarker +1 17 return result Abbildung 2.6: Suche im Θ-Graphen 2.5.2 Θ-Graph Die Suche beim Θ-Graphen besteht im Prinzip aus einer Breitensuche. Die speziellen Eigenschaften des Θ-Graphen garantieren, dass ein Knoten, der vom Startknoten höchstens r Einheiten entfernt sind, über einen Pfad vom Startknoten aus erreichbar ist. Dabei ist keiner der Knoten dieses Pfades mehr als s · r Einheiten vom Startknoten entfernt [7]. Um also alle Knoten innerhalb einer bestimmten Entfernung r um den Startknoten zu finden, muss eine Breitensuche auf dem Graphen durchgeführt werden, bei der nur Knoten betrachtet werden, die nicht weiter als s · r Einheiten vom Startknoten entfernt liegen. Dazu muss man zuerst den Startknoten q 0 zu einem gegebenen Abfrage q finden. Diese Suche kann man bei dem Walkthrough System später bei den Abfragen mit erledigen. Da der Benutzer sich nur langsam durch das System bewegt, unterscheiden sich die Startknoten von Abfrage zu Abfrage nur wenig. Man kann daher nach jeder Abfrage prüfen, ob einer der zurückgelieferten Punkte näher am Benutzerstandort lag als der benutzte Startknoten. Dieses Verhalten wurde auch im Θ-Graphen implementiert und ist bei den Zeitmessungen berücksichtigt. Da bei den Messungen aber keine räumlich zusammenhängenden Abfragen vorkommen, muss vor jeder Suche der beste Startknoten für diese Abfrage gesucht werden. Dies geschieht durch eine sequentielle Suche über alle Knoten. Da dies aber später im Walkthrough System nicht vorkommt, wird die für diese Suche benötigte Zeit nicht gemessen. Bei einer Breitensuche muss man sich typischerweise zwei Dinge merken: Einmal die Knoten, die noch betrachtet werden müssen; diese werden bei der Breitensuche in einer Queue verwaltet. Außerdem muss man sich die Knoten merken, die bereits besucht wurden, damit man nicht in einer Endlosschleife landet. Dies lässt sich bei der traditionellen Breitensuche elegant mit Markierungen lösen und da alle Knoten besucht werden, kann man die Markierung bei der Suche automatisch wieder zurücksetzen. Bei der Suche im Θ-Graph werden dagegen nicht alle Knoten besucht, daher können die Markierungen hier nicht einfach wieder zurückgesetzt werden. Es reicht aber auch, wenn man als Markierung eine Zählvariable nimmt. Dann muss man sich nur merken, welchen Wert die höchste Zählvariable im Graphen hat, und dies immer der Suchfunktion als Parameter übergeben (maxMarker im Programm 2.6). Bei einer Suche kann diese Zählvariable pro Knoten maximal um Eins steigen, da jeder Knoten höchstens einmal besucht wird. Wenn man eine Integer Variable als Zählvariable nimmt, dauert es 231 Suchvorgänge bis diese überläuft. Bei 20 Suchvorgängen pro Sekunde dauert das etwa 7 Jahre, daher kann man kurz vor dem Überlaufen ruhig einmal linear über alle Knoten gehen, um die Zählvariablen zurückzusetzen. 18 KAPITEL 2. IMPLEMENTIERUNG Kapitel 3 Messung 3.1 Vorgehensweise Bevor wir zu den Ergebnissen der Messungen kommen, ein paar allgemeine Worte über die untersuchten Parameter vorweg. 1 Pixel Beobachter Bildschirm Abbildung 3.1: Annäherung des Winkels Zunächst wollen wir klären, welche Winkel realistisch sind. Wir erinnern uns: Es geht darum, diejenigen Objekte herauszufiltern, die sowieso nicht auf dem Bildschirm erscheinen. Welchen Sichtbarkeitswinkel haben aber nun die Objekte, die bei der Anzeige kleiner als ein Pixel sind? Eine relativ grobe, dafür aber leicht verständliche Vorstellung ist in Abbildung 3.1 dargestellt. Der gesamte Sichtwinkel des Beobachters wird in kleinere Sichtwinkel aufgeteilt, die beim Schnitt mit dem Bildschirm jeweils die Länge eines Pixels ergeben. Die kleinen Sichtwinkel sind dabei ungefähr gleich groß. Eine grobe Schätzung für den Winkel α ergibt sich daher, indem man die Größe des Beobachterwinkels durch die Anzahl der Pixel teilt. Bei einem typischen Beobachterwinkel von 90◦ und 1024 Pixeln ergibt sich daher für α ≈ 0.09◦ . Eine weitere Größe, die maßgeblich durch den Winkel bestimmt wird, ist die Anzahl der Objekte, die bei einer Anfrage zurückgeliefert werden. Diese Anzahl sollte auf jeden Fall wesentlich kleiner sein als die Gesamtzahl von Punkten in der Szene. Aber andererseits sollte die Anzahl der zurückgelieferten Objekte Grafikkarten auch nicht unterfordern oder überfordern. Grafikkarten schaffen in der Praxis etwa 10 Millionen Dreiecke pro Sekunde. Das macht 500.000 Dreiecke pro 20stel Sekunde. Wenn ein Objekt aus etwa 100 Dreiecken besteht, kann eine Abfrage also etwa 5000 Objekte zurückliefern, damit die Grafikkarte diese noch verarbeiten kann. Bei den Messungen wurde der Winkel α nun so gewählt, dass sich bei den Kegelabfragen auf den Testszenen etwa 5000 Punkte ergaben. Konkret wurde der Wert 1.564 für α̃ gewählt (das entspricht etwa einem Sichtbarkeitswinkel α von 0.6◦ ). Dieser Wert entspricht dann ungefähr 7 × 7 Pixel großen Objekten bei einer Auflösung von 1024 × 768. 19 20 KAPITEL 3. MESSUNG Für die Messungen mit unterschiedlich großen Szenen, wurden immer Teilmengen von riesigen Szenen benutzt. Dabei waren diese Teilmengen nicht einfach ausgedünnte Versionen der ursprünglichen Szene, sondern sie bestanden aus einem zusammenhängenden Bereich der ursprünglichen Szene. Wenn man die Abfragen vollständig auf dem Bereich definiert, der in allen Teilmengen gleich ist, erhält man bei den Abfragen trotz unterschiedlicher Anzahl von Punkten immer dasselbe Ergebnis. Dadurch ist es möglich, die Ergebnisse besser zu vergleichen. Unterschiedliche Laufzeiten der Algorithmen sind dann folglich in der Anzahl der Punkte begründet. Aber wie erstellt man möglichst einfach zusammenhängende Teilmengen von generierten Szenen? Wenn man die Punkte in der Szene beim Generieren nach der Entfernung zu einem festgelegtem Bezugspunkt sortiert, erhält man durch Ausgabe der ersten Punkte genau die Punkte, die am nächsten bei diesem Bezugspunkt liegen. So ist es möglich, sehr schnell Teilmengen dieser Szene zu erstellen und man kann diese sogar benutzen, ohne sie explizit abzuspeichern. Außerdem hat diese Methode den Vorteil, dass die Szene gleichmässig in alle Richtungen größer wird, wenn man eine größere Teilmenge nimmt. 3.2 Ermittlung der optimalen Parameter Alle Datenstrukturen haben Parameter, die man variieren kann. Zuerst wollen wir daher möglichst optimale Werte für diese Parameter ermitteln, die dann später bei den Vergleichsmessungen benutzt werden. Wenn es der Platz in den Diagrammen erlaubt, sind bei den Messungen auch die Schwankung der Zeitmessung in Form von Balken eingetragen. Die mit Punkten markierten Werte sind die Durchschnittswerte von hundert Messungen (siehe auch 2.5). 3.2.1 Blattgröße beim BSPTree Der einzige Parameter beim BSPTree und beim transformierten BSPTree ist die Anzahl der Punkte, die in einem Blatt gespeichert werden. Den Messungen zugrunde liegen eine große Landgen und eine große Random generierte Szene mit jeweils 5 Millionen Punkten. Die Messungen wurden einmal auf den ersten 50.000 Punkten und auf den ersten 1.000.000 Punkten durchgeführt. Bereits hier fällt auf, dass sich die Messergebnisse bei 50.000 Punkten und bei 1.000.000 Punkten kaum unterscheiden. 70 Abfrage 1 (landgen, 50.000 Punkte) Abfrage 1 (landgen, 1.000.000 Punkte) Abfrage 2 (landgen, 50.000 Punkte) Abfrage 2 (landgen, 1.000.000 Punkte) Abfrage 3 (random, 1.000.000 Punkte) 65 60 55 Zeit (ms) 50 45 40 35 30 25 20 1 4 16 64 Punkte pro Blatt Abbildung 3.2: Blattgröße beim BSPTree 256 1024 3.2. ERMITTLUNG DER OPTIMALEN PARAMETER 160 21 Abfrage 1 (landgen, 50.000 Punkte) Abfrage 1 (landgen, 1.000.000 Punkte) Abfrage 2 (landgen, 50.000 Punkte) Abfrage 2 (landgen, 1.000.000 Punkte) Abfrage 3 (random, 1.000.000 Punkte) 140 120 Zeit (ms) 100 80 60 40 20 0 1 4 16 64 256 Punkte pro Blatt 1024 4096 16384 65536 Abbildung 3.3: Blattgröße beim transformierten BSPTree Die Ergebnisse der Messung sind nicht weiter überraschend. Je größer die Blätter sind, desto mehr Punkte müssen linear durchlaufen werden und der Vorteil des Baumes geht verloren. Andererseits je kleiner die Blätter sind, desto mehr Zeit geht beim Schneiden der Abfrage mit den Hüllboxen verloren, da dieses komplizierter ist als das lineare Durchprobieren der Punkte. Ein weiterer Grund für eine eher groß gewählte Blattgröße, ist dass die Größe der Datenstruktur mit größerer Blattgröße zu Beginn stark abnimmt (siehe Abbildung 3.4). 1000 BSPTree (50.000 Punkte) transformierter BSPTree (50.000 Punkte) BSPTree (1.000.000 Punkte) transformierter BSPTree (1.000.000 Punkte) Größe (MB) 100 10 1 1 4 16 64 256 Punkte pro Blatt 1024 4096 16384 Abbildung 3.4: Wirkung der Blattgröße auf die Größe der Datenstruktur 65536 22 KAPITEL 3. MESSUNG Als bester Wert für die Blattgröße ergeben sich nach den Diagrammen Werte zwischen 16 und 64. Im Folgenden wurde 16 für den BSPTree und 32 für den transformierten BSPTree verwendet. 3.2.2 Sektoranzahl beim Θ-Graphen Beim Θ-Graphen gibt es zwei Parameter, die man möglichst optimal wählen möchte. Zum einen die Sektoranzahl des Θ-Graphen und zum anderen die Anzahl der Level. Als erstes wollen wir die beste Anzahl von Sektoren eines Θ-Graphen hinsichtlich der Laufzeit bestimmen, der für den Zweck dieser Messungen nur aus einem Level besteht. 900 Abfrage 1 (landgen, 500.000 Punkte) Abfrage 2 (random, 50.000 Punkte) Abfrage 2 (random, 500.000 Punkte) 800 Zeit (ms) 700 600 500 400 300 200 5 10 15 20 25 30 Sektoranzahl Abbildung 3.5: Sektoranzahl beim Θ-Graphen Die Sektoranzahl beim Θ-Graphen ist ein Kompromiss zwischen kleinerem Suchradius bei mehr Sektoren und mehr Verwaltung bei der Breitensuche mit vielen Sektoren. Als Optimum ergeben sich etwa 15 Sektoren. Vom Speicherplatz her gesehen, sollte man beim Θ-Graphen – im Gegensatz zur Blattgröße beim BSPTree – eine möglichst geringe Sektoranzahl wählen. 3.2.3 Levelanzahl beim Θ-Graphen Mit der im letzten Abschnitt bestimmten Sektoranzahl 15 messen wir nun aus, welche Levelanzahl am besten ist. Eine höhere Levelanzahl beim Θ-Graphen sorgt dafür, dass die Kegelabfrage durch mehr Zylinder angenähert wird. Da der Kegel einen sehr großen Öffnungswinkel hat, bringt das zu Beginn relativ viel. Wenn der Zylinder aber erst mal genau genug angenähert ist, ändert sich an der Zeit nicht mehr viel. Noch höhere Levelanzahlen führen dann schließlich zu einer Verschlechterung, da die Vorteile des Θ-Graphen nicht mehr ausgenutzt werden können, wenn die einzelnen ΘGraphen immer kleiner werden. Wenn die Levelanzahl so groß ist wie die Punktanzahl, degeneriert die Suche zum sequentiellen Durchsuchen aller Punkte. Die Wahl der konkreten Levelanzahl ist relativ unkritisch, es sollten mehr als etwa 10 sein und weniger als 30. Zwischen 10 und 30 verändert sich die Laufzeit nur wenig und auch der zusätzliche Speicherverbrauch durch eine höhere Levelanzahl ist vernachlässigbar. Als Levelanzahl bei den folgenden Messungen wurde daher 16 gewählt. 3.3. VERGLEICH DER DATENSTRUKTUREN 450 23 Abfrage 1 (landgen, 50.000 Punkte) Abfrage 1(landgen, 500.000 Punkte) Abfrage 2 (random, 500.000 Punkte) 400 350 Zeit (ms) 300 250 200 150 100 50 10 20 30 Levelanzahl 40 50 60 Abbildung 3.6: Levelanzahl beim Θ-Graphen 3.3 Vergleich der Datenstrukturen 600 500 BSPTree transformierter BSPTree Theta−Graph Zeit (ms) 400 300 200 100 0 0 2 4 6 8 10 12 Szenengröße (Mio. Punkte) 14 16 18 20 Abbildung 3.7: Vergleich der Laufzeit der drei Datenstrukturen Nachdem wir jetzt gute Parameter für die einzelnen Datenstrukturen ermittelt haben, kommen wir zum Vergleich der damit erzielten Laufzeiten. Hier interessiert vor allem die Frage, ob sich der theoretische Unterschied zwischen Θ-Graph und BSPTree in der Praxis bemerkbar machen. 24 KAPITEL 3. MESSUNG Die Laufzeit des Θ-Graphen ist nur von den zurückgelieferten Punkten abhängig, die Laufzeit des BSPTrees ist dagegen von der Größe der gesamten Szene abhängig. Ab einer bestimmten Szenengröße wird daher der Θ-Graph schneller als der BSPTree sein. Die Messungen fanden auf Teilszenen einer großen Landgen Szene mit 20 Millionen Punkten statt. Daher wurden für die Datenstrukturen, die Parameter gewählt, die auf der Landgen Szene die Besten waren. Konkret ist das die Blattgröße 16 für den BSPTree und 32 für den transformierten BSPTree. Der Θ-Graph wurde mit 15 Sektoren und 16 Leveln ins Rennen geschickt. Alle Messungen fanden auf einem Sun Rechner mit 8GB Arbeitsspeicher statt, um möglichst große Szenen zu testen. Durch Beschränkungen in der Java VM konnte jedoch nur bis zu 3GB Arbeitsspeicher benutzt werden. Dies reichte für 20 Millionen Punkte beim BSPTree, 16 Millionen Punkte beim transformierten BSPTree und 11 Millionen Punkte beim Θ-Graphen. Ein weiterer Nachteil von Solaris ist, dass auf dieser Plattform die Methode zur Zeitmessung nicht Millisekundengenauigkeit sondern nur Hundertstelsekunden Genauigkeit hat. Daher sind auch die Durchschnittswerte nicht allzu genau, da die Zeiten der Datenstrukturen aber weit auseinanderliegen ist ein Vergleich dennoch möglich. Der transformierte BSPTree nimmt bei den Messungen eine Sonderstellung ein, da sich an diesem Bild schon die Achillesferse dieses Algorithmus zeigt. Bei Anfragen, die nahe beim Ursprung liegen, ist der transformierte BSPTree schneller als der BSPTree, aber je mehr man sich vom Ursprung entfernt, desto langsamer werden die Abfragen, bei (1000, 1000) ist der Algorithmus dann schon dreimal langsamer als der Θ-Graph. Mehr zu diesem Verhalten folgt im Abschnitt 3.4.1. 3000 BSPTree transformierter BSPTree Theta−Graph 2500 Größe (MB) 2000 1500 1000 500 0 0 2 4 6 8 10 12 Szenengröße (Mio. Punkte) 14 16 18 20 Abbildung 3.8: Vergleich der Größe der drei Datenstrukturen Doch nicht nur bei der Laufzeit dominiert der BSPTree die anderen beiden Datenstrukturen, auch beim Speicherverbrauch steht er besser da als der Θ-Graph und der transformierte BSPTree. Durch das Mitspeichern des ursprünglichen Punktes beim transformierten BSPTree wird dieser bei gleicher Blattgröße immer mehr Speicher verbrauchen als der BSPTree. Vom Θ-Graphen setzt sich der BSPTree durch das platzsparende Zusammenfassen mehrerer Punkte in einem Blatt ab. 3.4. HÄRTEFÄLLE 3.4 25 Härtefälle Für den transformierten BSPTree und den Θ-Graphen lassen sich spezielle Situationen finden, bei denen die Schwachstellen der Datenstruktur zu Tage treten. 3.4.1 Mit dem transformierten BSPTree in den Abgrund 400 BSPTree transformierter BSPTree Theta Graph 350 300 Zeit (ms) 250 200 150 100 50 0 0 200 400 600 800 Entfernung zum Ursprung 1000 1200 1400 Abbildung 3.9: Mit dem transformierten BSPTree an den Rand des Abgrundes (0,0) E1 Punkte E2 Abbildung 3.10: Mögliche Erklärung der Positionsabhängigkeit des transformierten BSPTrees Der transformierte BSPTree zeigt bei den Messungen ein sehr unerwünschtes Verhalten: Je weiter die Abfrage vom Ursprung entfernt ist, desto länger dauert sie. Bei den anderen beiden Algorithmen hängt die Laufzeit dagegen nicht von der Position ab (siehe Abbildung 3.9) . Dieses Verhalten liegt daran, dass der Normalenvektor der Halbraumebene von der Position abhängt. 26 KAPITEL 3. MESSUNG Je weiter man sich vom Ursprung entfernt, desto steiler liegt die Ebene. Dadurch steigt auch die Anzahl der Hüllboxen, die unnötigerweise geschnitten werden. In Abbildung 3.10 ist ein Schnitt durch die 3D-Szene dargestellt. Die Punkte der Szene ordnen sich in Glockenform (vgl. Abbildung 1.7) um den Ursprung an. Halbraumabfragen wie E1 , die nahe des Ursprungs stattfinden, schneiden dann nur wenige Hüllboxen, während weiter vom Ursprung entfernte Abfragen wie E2 viele Hüllboxen schneiden. Das wirkt sich dann direkt auf die Laufzeit der Abfragen aus. 3.4.2 Ab in die Wüste mit dem Θ-Graphen Beobachter Startknoten Abfrageradius zusätzlicher Radius Abbildung 3.11: In einer Wüste kann der zusätzliche Radius beim Θ-Graphen sehr groß werden, dadurch werden große Teile der Szene unnötig durchsucht. 1000 BSPTree Theta Graph 900 800 700 Zeit (ms) 600 500 400 300 200 100 0 3000 3500 4000 Entfernung zum Ursprung 4500 5000 Abbildung 3.12: Mit dem Θ-Graphen in die Wüste Doch nicht nur der transformierte BSPTree hat eine Schwachstelle, auch der Θ-Graph hat eine. Beim Θ-Graphen ist es seine Wüstenempfindlichkeit. Wenn sich der Beobachter an einer Stelle in der Szene befindet, in der keine Punkte liegen, findet der Θ-Graph keinen naheliegenden Startkno- 3.5. ERGEBNIS UND AUSBLICK 27 ten. Dadurch wird der zusätzliche Radius im Vergleich zum Abfrageradius bei der Suche sehr groß (Abbildung 3.11). Die Wirkung wird in Diagramm 3.12 gemessen. Bei der Szene handelt es sich wieder um den Teil einer Landgen Szene, daher liegen die Punkte auf einer Kreisscheibe um den Ursprung. Durch eine Reihe von Abfragen geht der Benutzer immer näher an den Rand der Scheibe und schließlich auch darüber hinaus in die Wüste. Man sieht deutlich die leichte Beschleunigung, wenn zum Rand hin weniger Punkte im Kegel liegen. Doch schon nach ein paar Schritte in der Wüste werden die Zeiten des Θ-Graphen wesentlich schlechter. 3.5 Ergebnis und Ausblick Das Ergebnis fällt überraschend eindeutig aus. Bei den Testmessungen war der normale BSPTree der einzige Algorithmus, der keine Schwachstellen zeigte und zudem auch der Schnellste, wenn man das Verhalten des transformierten BSPTrees sehr nahe beim Ursprung außer Acht lässt. An dieser Stelle noch ein Hinweis auf die gemessenen Zeiten. Ungefähr 30 Millisekunden beim BSPTree scheinen viel zu langsam für Echtzeit Walkthrough Systeme zu sein. Man darf allerdings nicht vergessen, dass die Messungen mit abgeschaltetem JIT Compiler stattfanden. Mit eingeschaltetem JIT Compiler laufen die Algorithmen etwa 10 mal schneller und wenn die Algorithmen später in C++ implementiert werden, lässt sich möglicherweise noch mehr Performance rausholen. Aber bereits mit Java reicht die Performance vollkommen aus. Mit eingeschaltetem JIT Compiler benötigt der BSPTree etwa 5 Millisekunden für eine Abfrage bei der 5000 Punkte zurückgeliefert werden. In einer 20stel Sekunde ist es daher möglich etwa 50.000 Objekte zurückzuliefern und das entspricht wiederum etwa 5 Millionen Dreiecken. Es wird eine Weile dauern, bis die Grafikkarten eine solche Menge von Dreiecken rendern können und bis dahin sind die Prozessoren ebenfalls schneller geworden. Der transformierte BSPTree ist durch seine positionsabhänge Abfragezeit inakzeptabel für Walkthrough Systeme. Der Θ-Graph schlägt sich wacker, kommt jedoch nicht gegen den normalen BSPTree an. Bei den Abfragen braucht der Θ-Graph etwa dreimal länger als der BSPTree. Doch anhand der theoretischen Laufzeit der beiden Algorithmen, muss es irgendwann einen Punkt geben, ab dem der Θ-Graph schneller ist als der BSPTree. Beim Vergleich der Datenstrukturen mit unterschiedlichen Szenengrößen haben wir gesehen, dass sich die Zeiten des BSPTrees bei großen Szenen nicht drastisch verschlechtern, sondern relativ konstant bleiben. Durch die ungenaue Messung auf der Solaris Maschine sind die Durchschnittswerte nicht repräsentativ. Aber auch wenn die Laufzeit des BSPTrees linear anstiege und der BSPTree bei 21 Millionen Punkten 10 Millisekunden mehr benötigte als bei 1 Million Punkte, ist der Θ-Graph frühestens ab einer Szenengröße von 200 Millionen Punkten schneller. In der Praxis wird diese Grenze zudem noch wesentlich höher liegen. Als wäre das nicht genug, verbraucht der BSPTree außerdem auch weniger Speicher als der Θ-Graph, wenn die Blattgröße genügend groß gewählt wird. Und dann wäre da noch die Sache mit der Wüstenanfälligkeit des Θ-Graphen. Der einzige Vorteil des Θ-Graphen ist, dass es leicht möglich ist, neue Objekte in die Szene einzufügen. Beim BSPTree müsste dafür unter Umständen der ganze Baum neu aufgebaut werden. Weiterhin wird es eine Herausforderung sein, den BSPTree in ein System[5] einzubauen, das bereits mit Θ-Graphen arbeitet um Teilbereiche der Szene zum Rendern an Client Maschinen zu übergeben. Diese Teilbereiche müssten dann sehr schnell zu BSPTrees aufgebaut werden. 28 KAPITEL 3. MESSUNG Literaturverzeichnis [1] P. K. Agarwal und J. Erickson: “Geometric Range Searching and Its Relatives”, Technical Reports CS-1997-11, Duke University, Department of Computer Science, 1997 [2] M. de Berg, M. van Kreveld, M. Overmars und O.Schwarzkopf: “Computational Geometry: algorithms and applications”, Springer,1997 [3] K. L. Clarkson und P. W. Shor, “Application of random sampling in computational geometry”, II. Discrete & Computational Geometry, 4:387-421, 1989 [4] M. Dickerson, C. Duncan und M. Goodrich: “K-D Trees Are Better when Cut on the Longest Side”, S.179-190 in Proc. 8th Annual European Symposium on Algorithms (ESA’2000), Springer LNCS Band 1879 [5] M. Fischer: “Weak-Spanner Algorithm for Distributed and Networked Virtual Environments”, Dissertation (2004); siehe auch: Paderborn Realtime System for Interactive Walkthrough (PaRSIWal), http://www.parsiwal.de [6] M. Fischer, T. Lukovszki, und M. Ziegler: “Geometric searching in walkthrough animations with weak spanners in real time”, S. 163-174 in 6th Annual European Symposium on Algorithms (ESA 1998), Springer LNCS Band 1461 [7] M. Fischer, F. Meyer auf der Heide und W.-B. Strothmann: “Dynamic data structures for realtime management of large geometric scenes”, S. 157-170 in 5th Annual European Symposium on Algorithms (ESA 1997), Springer LNCS Band 1284 [8] T. Lukovszki, “New Results on Geometric Spanners and Their Applications”, Dissertation (1999) [9] J. Matoušek: “Reporting points in halfspaces”, Computational Geometry: Theory and Application, 2:169-186, 1992 [10] M. Tamm: “Java Profiling: Engpässe in Java-Programmen finden”, iX 4/2004, S. 42 [11] A. C. Yao und F. F. Yao, “A General Approach to d-Dimensional Geometric Queries”, S. 163-168 in Proceedings of the 17th Symposium on Theory of Computation (STOC 1985) 29 Danksagung Ganz besonders möchte ich mich bei Dr. Martin Ziegler für die Betreuung und das interessante Thema bedanken. Meinem Vater danke ich für seine Unterstützung und das Korrekturlesen, ohne ihn wäre die Anzahl der Fehler viel grösser. Außerdem möchte ich mich bei meinen Freunden für ihre Unterstützung, aber auch für die Ablenkung bedanken. Versicherung Ich versichere, dass ich die Arbeit ohne fremde Hilfe und ohne Benutzung anderer als der angegebenen Quellen angefertigt habe und dass die Arbeit in gleicher Form noch keiner anderen Prüfungsbehörde vorgelegen hat und von dieser als Teil einer Prüfungsleistung angenommen wurde. Alle Ausführungen, die wörtlich oder sinngemäß übernommen wurden, sind als solche gekennzeichnet. Paderborn, den 24. Juni 2004 Matthias Hilbig