Perspektivische Suche in geometrischen Szenen

Werbung
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
Herunterladen