Glesa voxelträd: skapande och lagring
HENNING
TEGEN
Examensarbete
Stockholm, Sverige 2010
Glesa voxelträd: skapande och lagring
HENNING
TEGEN
Examensarbete i datalogi om 30 högskolepoäng
vid Programmet för datateknik
Kungliga Tekniska Högskolan år 2010
Handledare på CSC var Lars Kjelldahl
Examinator var Lars Kjelldahl
TRITA-CSC-E 2010:054
ISRN-KTH/CSC/E--10/054--SE
ISSN-1653-5715
Kungliga tekniska högskolan
Skolan för datavetenskap och kommunikation
KTH CSC
100 44 Stockholm
URL: www.kth.se/csc
Sammanfattning
Voxlar (volumetric-pixel) har historiskt använts mest för
att representera volymetrisk data som rök, vätskor och olika
tillämpningar inom medicin. Minneskomplexiteten för detta
är dock ofta O(n3 ) vilket gör voxelgrafik ineffektivt för
modeller där vi enbart är intresserade av att spara ytor.
Med hjälp av en datastruktur som kallas glesa voxelträd
(Sparse Voxel Octree) går det att minska minneskomplexiteten närmare O(n2 ) förutsatt att man bara är intresserad
av att spara ytor i modellen.
Detta öppnar möjligheter för voxelgrafiken att på sikt
inom vissa områden ersätta triangelgrafik då voxlar har
flera fördelar mot trianglar, som t.ex. automatiserad LOD
(Level Of Detail) kalkylering.
Om voxelgrafik med hjälp av glesa voxelträd ska kunna
ersätta triangelgrafik måste det finnas robusta och effektiva
sätt att skapa och lagra glesa voxelträd vilket är precis vad
jag undersöker i denna rapport. Jag gör en utförlig analys
av fördelar och nackdelar med de olika datastrukturerna
för att spara noderna i ett glest voxelträd. Vidare undersöker jag några viktiga aspekter för voxelmodellering såsom
modeller större än minnesutrymmet samt undo-redo funktionalitet.
Under detta arbete har jag även implementerat en prototyp som demonstrerar att ovanstående teknik fungerar i
praktiken. I slutet av rapporten går jag igenom gränssnittet
för min prototyp.
Abstract
Sparse Voxel Octrees: creating and storing
Historically voxels have mostly been used to represent volymetric data such as smoke, fluids and various applications
in medicine where we are interested in the volume of the
model. The memory complexity for this is usually O(n3 )
which make voxel graphics inefficient for models where we
only are interested in storing surfaces. With the help from
a data structure called Sparse Voxel Octrees it is possible
to reduce the memory complexity closer to O(n2 ) presuming that we are mostly storing surfaces in our model. This
opens up the possibilities for voxel graphics to replace triangle graphics in certain areas where voxels have an advantage
(e.g. automatic LOD calculation).
If voxel graphics, with the help from Sparse Voxel Octrees, should be able to replace triangle graphics there must
exist robust and efficient solutions to create and store them,
which is precisely what I research in the paper. I have made
a comprehensive study on possible data structures for storing node data with there advantages and disadvantages.
Furthermore, I research some important aspects of voxel
modeling, such as larger models than what can fit inside
the main memory and undo-redo functionality.
I have also implemented a prototype to demonstrate
that the above technology really works in practice. At the
end of this paper I describe the interface for my prototype.
Förord
Denna magisteruppstats i datalogi är utförd på CSC på
Kungliga Tekniska Högskolan i Stockholm under våren 2010.
Jag har skrivit rapporten på svenska, inte engelska,
vilket gett mig en del problem då de flesta facktermer kommer från engelskan. Många av de engelska termerna har
ingen tidigare svensk översättning, jag har därför tagit mig
friheten att själv översätta några av dem till svenska. Detta
inkluderar titeln på rapporten: glesa voxelträd.
Jag skulle vilja tacka de personer som gett mig tips och
råd under projektet samt min familj som bland annat har
hjälpt mig med korrekturläsning. Slutligen vill jag tacka
min handledare och examinator Lars Kjelldahl.
Henning Tegen, Stockholm, 9 juni 2010
Innehåll
1 Inledning
1.1 Vad jag förutsätter av läsaren . . . . .
1.2 Bakgrund . . . . . . . . . . . . . . . .
1.2.1 Trianglar och texturer . . . . .
1.2.2 Parallellisering . . . . . . . . .
1.2.3 Ray tracing . . . . . . . . . . .
1.2.4 Ett nytt modelleringsparadigm
1.2.5 Voxelgrafik . . . . . . . . . . .
1.2.6 Glesa voxelträd . . . . . . . . .
1.3 Disposition . . . . . . . . . . . . . . .
1.4 Tekniska termer . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
1
1
1
1
2
2
3
3
4
4
4
2 Målsättning
2.1 Problemställning . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Mål . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Avgränsningar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
7
8
3 Tidigare arbeten
3.1 Voxelforskning
3.2 80- och 90-talet
3.3 2000-talet . . .
3.4 Inläsning . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
9
10
10
4 Teori
4.1 Ray tracing . . . . . . .
4.2 Voxel . . . . . . . . . . .
4.3 Octree . . . . . . . . . .
4.4 Glesa Voxelträd (GVT)
4.5 Dataseparation . . . . .
4.5.1 Topologidata . .
4.5.2 Shadingdata . .
4.6 Mipmapping . . . . . . .
4.7 Paging . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13
13
13
14
15
16
17
17
18
19
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4.8
4.9
4.10
4.11
4.12
Rendering en GVT-modell . . . .
Voxalisering . . . . . . . . . . . .
Transformation . . . . . . . . . .
SMP (Parallellisering på CPU:n)
Dataåtkomst . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
20
22
22
23
23
5 Analys av datastrukturer
5.1 Översikt och problemställning . . . . . . . . . . . . . . . .
5.2 Grundläggande datastrukturer för GVT . . . . . . . . . .
5.2.1 Pekarträd . . . . . . . . . . . . . . . . . . . . . . .
5.2.2 XYZ-Hashtabell . . . . . . . . . . . . . . . . . . .
5.2.3 Morton-Hashtabell . . . . . . . . . . . . . . . . . .
5.2.4 Trädkod . . . . . . . . . . . . . . . . . . . . . . . .
5.2.5 Trädkod-offset . . . . . . . . . . . . . . . . . . . .
5.2.6 Sammanfattning av grundläggande datastrukturer
5.3 Förbättringar till datastrukturerna . . . . . . . . . . . . .
5.3.1 Leafbits . . . . . . . . . . . . . . . . . . . . . . . .
5.3.2 Nodblock . . . . . . . . . . . . . . . . . . . . . . .
5.4 Val av datastruktur . . . . . . . . . . . . . . . . . . . . .
5.4.1 Tre olika datastrukturer . . . . . . . . . . . . . . .
5.4.2 Datastruktur för generering och editering . . . . .
5.4.3 Datastruktur för CUDA rendering . . . . . . . . .
5.5 Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
25
25
26
26
26
27
28
28
29
30
30
31
32
32
33
33
34
6 Analys av algoritmer
6.1 Nodtraversering . . . . . . . . . . . . . . . . .
6.1.1 Bredden-först-sökning (BFS) . . . . .
6.1.2 Djupet-först-sökning (DFS) . . . . . .
6.1.3 Djupet-först-sökning med mipmapping
6.1.4 DFS och BFS kombination . . . . . .
6.1.5 Skapa en nod . . . . . . . . . . . . . .
6.2 Förändring av rotnoden . . . . . . . . . . . .
6.3 Paging . . . . . . . . . . . . . . . . . . . . . .
6.3.1 Översikt . . . . . . . . . . . . . . . . .
6.3.2 Page LRU (Least Recently Used) . . .
6.3.3 Page metadata . . . . . . . . . . . . .
6.3.4 Page datastruktur . . . . . . . . . . .
6.3.5 Parallelliserad paging . . . . . . . . .
6.4 Layers . . . . . . . . . . . . . . . . . . . . . .
6.5 Undo-Redo . . . . . . . . . . . . . . . . . . .
6.5.1 Algoritm . . . . . . . . . . . . . . . .
6.5.2 Datastruktur . . . . . . . . . . . . . .
6.6 Lagring på disk . . . . . . . . . . . . . . . . .
6.6.1 Översikt . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
35
35
35
35
36
37
38
38
40
40
40
40
41
42
42
42
42
43
43
43
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6.6.2
Komprimering till disk . . . . . . . . . . . . . . . . . . . . . .
7 Implementation
7.1 Programmeringsspråk .
7.2 Operativsystemsmiljö .
7.3 Implementeringsstruktur
7.3.1 Front-End . . . .
7.3.2 Back-End . . . .
7.3.3 FS (File System)
7.4 Skal . . . . . . . . . . .
44
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
47
47
48
48
49
50
51
8 Resultat
8.1 Prototypen . . . . . . . . . . . . . . . . . . . . .
8.1.1 Syfte . . . . . . . . . . . . . . . . . . . . .
8.1.2 Implementering . . . . . . . . . . . . . . .
8.1.3 Demonstration . . . . . . . . . . . . . . .
8.2 Datastrukturer . . . . . . . . . . . . . . . . . . .
8.2.1 Nod . . . . . . . . . . . . . . . . . . . . .
8.2.2 Page . . . . . . . . . . . . . . . . . . . . .
8.2.3 Layers och History . . . . . . . . . . . . .
8.3 Uppfyllande av målen . . . . . . . . . . . . . . .
8.3.1 Mål 1: Editering . . . . . . . . . . . . . .
8.3.2 Mål 2: Ingen rumsgräns . . . . . . . . . .
8.3.3 Mål 3: Multipla undo-redo samt layers . .
8.3.4 Mål 4: Modeller större än minnesutrymme
8.3.5 Mål 5: CUDA Export . . . . . . . . . . .
8.3.6 Mål 6: Minimera hårdiskutrymme . . . .
8.3.7 Mål 7: Prestanda . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
53
53
53
53
54
56
56
57
58
58
58
58
58
59
59
59
60
9 Diskussion och slutsats
9.1 Diskussion . . . . . . . . . . . . . . .
9.1.1 Jämförelse med triangelgrafik
9.1.2 Kommersiell framtid för GVT
9.1.3 Min vision . . . . . . . . . . .
9.2 Framtidsförbättringar . . . . . . . .
9.2.1 Optimerade GVT modeller .
9.2.2 Automatisk mipmapping . . .
9.2.3 Nätverkseditering . . . . . . .
9.2.4 GVT-animering . . . . . . . .
9.2.5 Användargränssnitt . . . . .
9.2.6 Progressiv editering . . . . .
9.2.7 CSG modellering . . . . . . .
9.2.8 Pekarträd med ”struct-hack”
9.3 Slutsats . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
61
61
61
61
62
62
62
62
62
62
63
63
63
63
63
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Referenser
65
Kapitel 1
Inledning
1.1
Vad jag förutsätter av läsaren
För att kunna tillgodogöra sig innehållet i denna rapport bör läsaren ha en grundläggande förståelse för programmering och datorgrafik. I kapitel 1.4 finns det en
ordlista över tekniska termer. Ordlistan är dock mer tänkt som en påminnelse. Om
läsaren aldrig hört talas om orden i listan kommer rapporten bli svårbegriplig.
1.2
1.2.1
Bakgrund
Trianglar och texturer
Sedan 3D-grafikens begynnelse har triangeln stått i fokus för all form av modellering av linjära ytor. De viktigaste anledningarna till detta är att en triangel tar lite
plats i minne samt att det finns mycket effektiva algoritmer för att rasterisera dem
till skärmen. Det finns även många metoder för att modellera krökta ytor som t.ex.
bézier-kurvor och NURBS. Gemensamt för alla krökta ytor är dock att det vanligaste sättet att rendera dem är att först konvertera dem till trianglar [Akenine 2002,
p. 512].
Genom ökad efterfrågan på realism och detaljrikedom i datorgrafik har man de
sedan länge börjat lägga på texturer på trianglarna. Till en början för att representera färger men på senare tid även för kunna ändra mer avancerade saker som
normal och reflexion.
När man började använda texturer (framför allt i realtidsgrafik) använde man
först så kallade tiles, vilket innebär att man har en liten textur som man sedan
repetitivt upprepar gång på gång över trianglarna i syfta att spara minne. Detta
minskar dock realismen då varje yta inte kan ha sin egna speciella karaktär. På
senare år har företag som id Software börjat med vad de kallar för unik-texturering
vilket betyder att varje yta i världen får sin egna unika textur. En konstnär kan
sedan gå in och ge varje yta i världen sin egna distinkta karaktär [Carmack 2007b].
När unik-texturering används tar triangelmodellen dock inte längre lite plats i
1
KAPITEL 1. INLEDNING
minne vilket ju var en av anledningarna att använda dem från början. id Software
uppskattar t.ex. att en värld i deras nya spel Rage kan ta över 20 GB i rå form
[Carmack 2007a].
1.2.2
Parallellisering
De senaste åren har hårdvarutillverkare gått mot mer och mer parallellisering i sina
chip till skillnad från 80- och 90-talet då majoriteten av prestandaökningen kom
från högre klockfrekvens. Den drivande anledningen bakom detta är att hårdvarutillverkare får svårare och svårare att höja klockfrekvensen och det är enklare att ha
två beräkningskärnor med halva hastigheten i stället för en med den dubbla. Detta
skapar dock nya problem för programmerare som nu måste anpassa sin mjukvara
för flera beräkningskärnor.
Två intressanta händelser de senaste åren som är relevant för mitt arbete är dels
Cell-processorn som sitter i alla Playstation 3 och dels NVIDIA’s nya grafikkort som
stödjer CUDA. Båda dessa är exempel på att parallellisering är här för att stanna.
Speciellt intressant för mitt arbete är CUDA, det har gjorts ett antal demonstrationer som visar att det nu är möjligt att köra ray tracing i realtid på grafikkortet.
Se [Raytracey] för en bra sammanfattning av realtids ray tracers.
1.2.3
Ray tracing
Under de senaste åren har vi sett ett ökat intresse för ray tracing. Än så länge är det
mest en trend för offline-renderare som RenderMan [Cortes 2006]. Men som framgår
ovan går det idag att köra ray tracing i realtid och jag skulle inte bli förvånad om
det snart dyker upp datorspel och annan interaktiv mjukvara som utnyttjar detta
faktum.
Anledningen till att ray tracing blivit mer populärt är en kombination av implementeringens enkelhet, fysikaliska korrekthet samt dess flexibilitet. Problem som
reflexion, refraktion, skuggor och global ljussättning som alltid varit svåra problem
för en rasteriserare blir triviala för en ray tracer. En ray tracer går även enkelt att
skala mellan kvalitet och prestanda genom att bara höja och sänka upplösningen
samt antalet samplingar per pixel.
Den primära orsak som hållit tillbaka ray tracing är att det än så länge varit betydligt långsammare än triangelrasterisering. Det finns två huvudanledningar
bakom detta:
1. Att undersöka om en triangel och en stråle skär varandra är relativt långsamt.
2. En triangel kan vara utformad precis hur som helst vilket medför att man
måste testa strålen mot alla trianglar.
3. Ray tracing mot trianglar kan söka i minnet i en ordning som inte lämpar sig
bra för cachen.
2
1.2. BAKGRUND
För att lösa problem 2 har ”triangel ray tracers” använt sig av så kallade ”space
partition” scheman [Ericson 2005, kap. 7] vilket är en datastruktur som läggs ovanpå triangelmodellen och gör att strålen inte behöver testas mot alla trianglar. Detta
ökar dock komplexiteten i implementeringen samt begränsar möjligheten för animation.
Även om ray tracing är långsammare än rasterisering i dag är det på sikt möjligt
att ray tracing kan gå om rasterisering i hastighet [Olick 2008] då Amdahl’s lag
sätter begränsningar för hur mycket prestandaökning en triangelrasteriserare kan
uppnå vid parallellisering. Ray tracing har dock ingen sådan begränsning då ray
tracing är vad som kallas för ett ”embarrassingly parallel problem” vilket betyder
att prestanda för ray tracing växer linjärt med antalet processorkärnor.
1.2.4
Ett nytt modelleringsparadigm
För att få 3D-grafik till nästa nivå av realism hävdar jag och många andra [Laine 2010]
[Crassin 2009] [Olick 2008] att ett nytt paradigm för 3D-modeller som bättre möter
dagens krav är nödvändigt. Målen för detta nya paradigm, alltså det man vill undvika från dagens triangelrasterisering är följande:
1. Skala prestanda bra då datorer får mer och mer parallell beräkningskapacitet.
2. Möjlighet för realistisk reflektion, refraktion, skuggor och global-ljussättning.
3. Automatiserad, effektiv och korrekt nersampling av 3D-modellen, så kallad
LOD.
4. Visualisera världar med unik texturering i storleksordningen 100 GB.
5. En så enkel implementering som möjligt.
Det har länge varit känt att ray tracing löser mål 1, 2 och 5 [Suffern 2007]
[Driemeyer 2005]. Problemet är alltså att hitta ett modelleringsparadigm som löser
mål 3 och 4, samt är effektiv för ray tracing.
1.2.5
Voxelgrafik
Parallellt med triangelns framgång har voxeln alltid varit populär i den akademiska
världen samt i vissa specialiserade användningsområden i kommersiellavärlden [Akenine 2002,
s. 342].
På senare senare år har det kommit så kallade digitala skulpteringsprogram
(t.ex. ZBrush och Mudbox) som ofta använder någon form av voxelgrafik. Dessa
skulpteringsprogram lämpar sig väl för ytor med mycket geometriska detaljer men
skulle även teoretiskt fungera för att modellera vilka ytor som helst.
Voxelgrafik är något som passar mycket bra tillsammans med unik texturering
då varje del av modellen ändå måste representeras av en unik voxel. Detta gör
voxelgrafik mer intressant för framtiden då unik texturering antagligen är här för
att stanna.
3
KAPITEL 1. INLEDNING
1.2.6
Glesa voxelträd
För att rendera voxlar snabbt behöver man en datastruktur som kapslar in voxlarna
och lämpar sig för ray tracing. I ett tidigare arbete [Tegen 2009] jag utförde under
hösten 2009 visade jag att en teknik kallad glesa voxelträd (Sparse Voxel Octree) är
en bra kandidat för detta. Det har även bekräftats av andra aktörer speciellt inom
spelindustrin [Laine 2010] [Olick 2008] [Carmack 2008].
Den viktigaste poängen med voxelmodellering med GVT-teknik är att vi inte
längre skiljer på geometri och texturer som i dagens triangelgrafik. Det finns bara
en byggsten för både texturdata och geometridata: voxeln. GVT-tekniken gör det
även möjligt att automatiserat nersampla modeller med en metod som liknar 2D
mipmapping. [Laine 2010] har visat att glesa voxelträd är en datastruktur som är
effektiv att ”ray tracea” emot och ray tracing löser i sin tur ännu flera av de problem
som triangel rasterisering har.
Den största nackdelen med GVT-modellering jämfört med triangelgrafik ser i
dagsläget ut att vara dynamisk geometri och animation. Det är inte trivialt hur detta
kan utföras på en GVT-modell. Vad jag känner till finns det inte någon forskning på
området så vi vet väldigt lite om det i dagsläget. Både [Olick 2008] och [Laine 2010]
har några intressanta idéer för animering av GVT-modeller men ingen har provat
dem i praktiken. Även om inte GVT-modeller lämpar sig för animering finns det
stora användningsområden för statiska modeller som t.ex. landskap och städer. På
sikt är det även möjligt att någon lyckas hitta en effektiv metod för animering av
GVT-modeller vilket skulle öppna möjligheterna ytterligare.
1.3
Disposition
I kapitel 2 presenterar jag problemställningen och målen för mitt arbete. I kapitel 3
presenterar jag tidigare arbeten som berör samma ämne. I kapitel 4 går jag igenom
den grundläggande teorin som läsaren måste känna till för att kunna tillgodogöra
sig resten av rapporten.
Jag har delat upp analysen i två kapitel: kapitel 5 för datastrukturer och kapitel
6 för algoirtmer. Efter analysen följer en genomgång av min implementering i kapitel
7. Slutligen presenterar jag resultatet av arbetet i kapitel 8 följt av en diskution och
slutsats i kapitel 9.
1.4
Tekniska termer
Amdahls lag:
Lag som definierar max teoretiska hastighetsökning vid parallellisering då en
viss del av koden förblir seriell.
Callback funktion:
En funktion som är specificerad av användaren som sedan någon specifik
modul använder.
4
1.4. TEKNISKA TERMER
CPU:
Står för Central Processing Unit, alltså datorns processor.
CSG:
Står för Constructive Solid Geometry, vilket är ett klassiskt sätt att utföra
solid modellering.
GPU:
Står för Graphic Processing Unit, alltså datorns grafikkortsprocessor.
GVT:
Förkortning för Glesa VoxelTräd, alltså det rapporten handlar om. Då jag inte
känner till någon tidigare översättning av Sparse Voxel Octree har jag tagit
mig friheten att själv översätta det till glesa voxelträd.
CUDA:
Ett C liknande programmeringsspråk för nVidias grafikkort.
Gränssnitt:
Publika funktioner från ett programmeringsbibliotek. Detta är den enda kommunikations länken mellan ett programmeringsbibliotek och övrig kod.
Layers:
Hämtat från Photoshop termen som betyder att man har flera bilder (”layers”)
i samma fil.
Octree:
En träddatastruktur där varje nod har upp till åtta barn.
Page:
Ett bit av ett virtuellt minne, som används i syfte att kunna ha mer data än
vad det finns minne (se kapitel 6.3 för en mer utförlig förklaring).
Programmeringsbibliotek:
Kod kompilerat till ett paket som sedan kan användas av andra program.
Rastergrafik:
Rektangulärt rutnät av pixlar. För att kunna visa något på skärmen måste
man först göra om det till rastergrafik.
Rasterisera:
Processen att gå från vektorgrafik till rastergrafik.
Ray tracing:
Processen att skjuta ut en stråle per pixel från skärmen och se vilket som är
det första objekt i virtuell världen som den träffar.
Rendering:
Processen att genera en bild från någon slags modell.
5
KAPITEL 1. INLEDNING
RW-lås:
Står för Read-Write lås, alltså ett lås som ser till att flera trådar kan läsa från
minnet men bara en kan skriva till det.
Scanline conversion:
Den klassiska metoden att rasterisera vektorgrafik.
Shading:
Processen att öka en ytas realism genom att bland annat skugga den enligt
någon speciell ljusmodell.
SMP:
Förkortning för Symmetric MultiProcessing. Det vanligaste sättet att parallellisera arbete på en CPU.
SVO:
Förkortning för Sparse Voxel Octree, engelska benämningen på GVT.
Textur:
Tvådimensionell bild som projiceras på en tredimensionell yta i syfte att öka
detaljrikedomen av denna yta.
Triangelsoppa:
Från engelskans ”triangle soup”, vilket betyder en mängd trianglar där den
inbördes ordningen inte är viktig.
Undo:
Att ångra en skrivoperation.
Unik texturering:
När varje triangel har sin egna texturyta.
Voxel:
Ordet kommer från en sammanslagning av volumetric och pixel vilket alltså
är pixelns motsvarighet i 3D. Geometriskt blir detta en kub i rymden med ett
visst antal attributer som t.ex. färg.
X86:
Vanligaste processorarkitekturen för persondatorer de senaste 25 åren.
6
Kapitel 2
Målsättning
I detta kapitel går jag igenom problemställningen och målen för arbetet. I kapitel
8.3 går jag sedan igenom målen ytterligare en gång för att se hur bra jag lyckats
uppfylla dem.
2.1
Problemställning
Syftet med detta arbete är att undersöka de olika aspekterna för att kunna skapa och
lagra GVT-modeller (Gelsa VoxelTräd modeller). Arbetet kan ses om en förstudie
till ett ”Photoshop för GVT”, alltså en applikation för editering av stora GVTmodeller. För att på sikt kunna bygga en sådan applikation finns det flera tekniska
problem som måste lösas.
Under arbetet har jag byggt en prototyp för att kunna undersöka de tekniska
problemen i praktiken. I kapitel 2.2 listar jag de mål som min prototyp ska uppfylla.
2.2
Mål
Följande mål vill jag att min prototyp ska uppfylla:
1. Editering
Med editering menar jag att användaren ska kunna gå obegränsat fram och
tillbaka mellan skapande och rendering av GVT-modellen.
2. Ingen rumsgräns
Det ska inte finnas någon gräns i rumstorlek för GVT-modellen. Användaren
ska inte heller behöva bestämma i förväg hur stor modellen slutligen kommer
bli.
3. Multipla undo-redo samt layers
Användaren ska kunna ångra ändringar han gjort till GVT-modellen samt ha
flera GVT-modeller i samma fil.
7
KAPITEL 2. MÅLSÄTTNING
4. Stora modeller
En 32-bitars process kan max använda 2 GB minne. GVT-modellerna vi
hanterar i systemet ska dock kunna vara betydligt större än så.
5. CUDA export
Möjlighet att exportera GVT-modellen till CUDA för rendering.
6. Minimera hårdiskutrymme
Rå-data för en GVT-modell kan vara flera gigabyte så det är viktigt att komprimera data så mycket som möjligt innan vi skriver ut den på disk. Då min
prototyp är tänkt för editering begränsar jag mig till lossless komprimering.
7. Prestanda
Det är självklart alltid önskvärt att maximera prestanda. För att ta hjälp
av parallellisering på dagens CPU tänker jag ta hjälp av SMP (Symmetric
MultiProcessing).
Bland ovanstående mål visar det sig att 3 och 4 är betydligt svårare att lösa än
övriga och jag har därför valt att spendera mest tid på dem. Det visar sig även att
varje mål var för sig är relativt lättlösligt men när man ska ha en implementering
som uppfyller alla mål blir det genast mycket svårare.
2.3
Avgränsningar
Mitt arbete har handlat primärt om skapande och lagring av GVT-modeller. Rendering är självklart en mycket viktig del av ett komplett system för GVT men då
det redan finns effektiva algoritmer, framför allt av [Laine 2010] har jag i stället
valt att fokusera på de mer outforskade delarna. Vad jag känner till finns det inget
utförligt arbete om hur man effektivt skapar och lagra GVT-modeller. För en utförlig beskrivning och implementering av rendering av GVT-modeller hänvisar jag
till [Laine 2010] och [Samet 1989] rapport.
Mitt arbete fokuserar på det tekniska i att skapa GVT-modeller. Det finns även
många problem för hur man låter en användare jobba med GVT-modeller på en
smidigt sätt. Detta är dock något som jag lägger utanför fokus för denna rapport.
8
Kapitel 3
Tidigare arbeten
I detta kapitel går jag igenom tidigare arbeten som är relevanta för mitt arbete. I
kapitel 3.4 listar jag litteratur som inte direkt behandlar GVT-teknik, men som ändå
har viss relevans för arbetet.
3.1
Voxelforskning
Visualisering av volymer har varit ett aktivt forskningsområde i många år, mycket
tack vare ett behov från medicinvärlden att visualisera olika delar av kroppen. En
bra sammanfattning över volymvisualisering är [Engel 2006]. Jag är dock primärt
intresserad av att använda voxlar för att visualisera ytor och inte volymer. De flesta
datastrukturer från volymforskningen använder O(n3 ) minne där n är bredd-höjdlängden av volymen och det blir därför inte relevanta för mitt arbete.
3.2
80- och 90-talet
Två utförliga arbeten om octree’s och voxlar är [Samet 1987] och [Samet 1989].
Dessa två artiklar är över 20 år gamla så deras definition av ”mycket minne” (3
MB) har ändrats en del. Den underliggande teorin är dock densamma även om man
i dagsläget har betydligt större möjligheter än de hade. [Samet 1987] är speciellt
intressant för mitt arbete då den beskriver olika implementeringsidéer för GVTträd, de som kapitel 5 handlar om.
En annan intressant rapport som också har några år på nacken är [Zesheng 1991].
Det mest intressanta i denna rapport är hur han konverterar mellan vektorgrafik och
voxelgrafik samt utför olika manipulationer av GVT-modellen. Zesheng’s ”Divide
and Conquer” teknik är en av de tekniker jag använder för generering av GVTmodeller.
[Kaufman 1986] och [Houng 1998] har skrivit två intressanta rapporter om voxalisering, alltså processen att gå från vektormodell till voxelmodell.
9
KAPITEL 3. TIDIGARE ARBETEN
3.3
2000-talet
Under sent 90-tal och tidigt 2000-tal verkar intresset för voxelgrafik varit lågt. Det
finns inte många artiklar från dessa år och jag skulle gissa att anledningen till detta
är triangelns framfart som till stor del berodde på dedikerad hårdvara.
På senare år verkar intresset för voxelgrafik vaknat till liv igen. [Knoll 2006] och
[Knoll 2009] är två rapporter som fortfarande primärt är inriktade på voxelgrafik
för att representera volymer men använder ändå en datastruktur liknande KD-träd,
vilket är intressant för mitt arbete då KD-träd och octree’s delar flera likheter.
[Crassin 2009] presenterar en algoritm som delar upp ray tracing av voxeldata i
två delar. I den första kör han ray tracing mot ett octree, där varje löv representerar
ett block av voxlar. I steg två av algoritmen använder han sedan en metod liknande
[Amanatides 1987]. Crassin’s metod är snabb och lämpar sig bra för dagens hårdvara
men om man bara är intresserad av att spara ytor tenderar blocken att använda
fruktansvärt mycket minne i onödan.
[Olick 2008] har en presentation där han går igenom grunderna för GVT. Då
det bara är en muntlig presentation saknas detaljer, men Olick beskriver precis det
som är intressant för mitt arbete.
Det mest relevanta tidigare arbetet för mig är dock [Laine 2010] som publicerades vintern 2010, två månader efter jag redan börjat på mitt arbete. [Laine 2010]
presenterar en full algoritm med källkod för snabb rendering av GVT-modeller på
CUDA. Deras papper fokuserar mestadels på rendering och fick mig att ändra fokus
på mitt egna arbete från rendering till generering och lagring.
En bra sammanfattande bok över datastrukturer för att spara geometriska ting
är [Samet 2006].
3.4
Inläsning
Utöver den inläsning som är direkt relaterad till voxlar och GVT har jag även funnit
följande böcker användbara för mitt arbete.
• Foundations of Multidimensional and Metric Data Structures [Samet 2006]
En mycket utförlig bok som sammanfattar stor del av all forskning av datastrukturer för att spara geometriska ting. Boken har även en mycket imponerande referenslista vilket är användbart om man vill gå till källan för
en viss datastruktur.
• Ray Tracing from the Group Up [Suffern 2007]
Bästa boken jag känner till gällande ray tracing.
• Computer Graphics Principles and Practice [Foley 1996]
Har ett intressant kapitel om solidmodellering.
• Real-Time Massive Model Rendering [Yoon 2008]
En bra bok som beskriver olika metoder för att rendera stora modeller.
10
3.4. INLÄSNING
• Real-Time Collision Detection [Ericson 2005]
Beskriver Octree’s, Morton Keys, KD-träd och mycket annat som har hög
relevans för mitt arbete och skriver om det på ett sätt som är lätt att ta till
sig. Boken innehåller även källkod till del flesta algoritmerna.
• Rendering with Mental Ray [Driemeyer 2005]
Egentligen en handbok för Mental Ray men beskriver även bra många koncept
angående ray tracing från en implementationsmässig synvinkel.
• GPU Gems 3 [Nguyen 2008]
Har några bra kapitel om CUDA programmering.
11
Kapitel 4
Teori
I detta kapitel går jag igenom den bakomliggande teorin som läsaren bör känna till
för att kunna tillgodogöra sig resterande delen av rapporten.
4.1
Ray tracing
En stråle kan definieras matematiskt med
r = r0 + tv
(0 < t < ∞)
Där r0 är strålens startpunk och v är dess riktning. När vi ”tracear” en stråle är
vi intresserad av att få reda på vilket som är det första objekt strålen träffar (lägst
t) samt träffpunktens position (kan beräknas från t).
Vanligaste användningsområdet för ray tracing är att rendera en bild av en
virtuell värld. För att åstadkomma detta skickar man ut en stråle per pixel för att
ta reda vilket som är det första objektet strålen träffar. Denna process illustreras
i figur 4.1. Även om den vanligaste strålen i ray tracing är den som kommer från
kameran finns det flera andra typer av strålar. En av dem är strålen för att beräkna
skuggor som vi ser exempel på i figur 4.1.
En virtuell värld kan teoretiskt bestå av vilka geometriska objekt som helst
förutsatt att vi kan testa om en stråle träffar dem. Det är dock fördelaktigt att
ha geometriska objekt där beräkningen om strålen träffar dem kan göras effektivt.
En voxel (kub där sidorna är parallella till axlarna) är ett exempel på en sådant
geometriskt objekt [Ericson 2005, s. 179].
4.2
Voxel
Ordet voxel kommer från en sammandragning av volumetric och pixel. En voxel är
alltså pixelns motsvarighet i 3D vilket blir en kub i rymden med ett antal attribut
som t.ex. färg. I figur 4.2 har jag grå-skuggat en av många voxlar.
Precis som i 2D-fallet där flera pixlar kan bygga upp en bild kan flera voxlar
bygga upp en modell. I 2D fallet använder man nästan alltid en matris för att lagra
13
KAPITEL 4. TEORI
Figur 4.1. Beskriver grundläggande konceptet av ray tracing. Bilden är tagen från
Wikipedia.
Figur 4.2. En mängd voxlar varav en är gråskuggad. Bilden är tagen från Wikipedia.
pixlarna och minnesutrymmet kommer då att växa med O(n2 ) där n är matrisen
bredd/höjd.
Även voxlar kan lagras i 3D-versionen av en matris. Minnesutrymmet kommer
dock nu växa med O(n3 ) där n är matrisens bredd/höjd/djup vilket alltså är en
polynomgrad snabbare än för 2D-fallet. En minneskonsumtion på O(n3 ) kan lätt
skapa praktiska problem för lite större voxelmodeller då en modell med enbart en
bredd/höjd/djup på 1000 behöver 1 miljard voxlar (10003 ). I många voxelmodeller
är det heller inte nödvändigt att spara alla voxlar om modellen t.ex. innehåller
mycket tomrum. Detta är sant i vårt fall som primärt är inriktat på att spara ytor
i voxelform. Det finns därför ett behov av en bättre datastruktur som kan utnyttja
tomrum i modellen och därmed kan använda mindre minne än O(n3 ).
4.3
Octree
Octree är en datastruktur som är löst relaterat till voxlar. Från en matematisk synvinkel kan man beskriva ett octree som ett tredimensionellt binärt träd [Ericson 2005,
14
4.4. GLESA VOXELTRÄD (GVT)
s. 308] där varje nod kan ha mellan 0 och 8 barn (childs). Första noden i trädet
kallas för rot och om en nod inte har några barn kallar vi noden för löv. Från en
geometrisk synvinkel kan man säga att ett octree rekursivt delar in rymden i kubiska block där varje block kan ha upp till 8 underblock. Ett exempel på detta kan
ses i figur 4.3.
Djupet i octree’t kalas för nivå, där roten alltid ligger på nivå 0 och löven ligger
i praktiken runt 8 till 18 (beror på hur stor modellen är).
Figur 4.3. Varje nod kan delas upp i 8 underblock som i sin tur kan delas upp i 8
underblock.
4.4
Glesa Voxelträd (GVT)
Ett glest voxelträd (Sparse Voxel Octree) är ett octree där löven i trädet är voxlar.
Denna datastruktur gör att minnesutrymmet växer långsammare än O(n3 ) förutsatt att det finns tomrum i modellen. Anledningen till detta är att vi slutar att
förgrena en specifik nod om det inte finns några flera voxlar inuti denna som vi vill
representera i modellen.
I figur 4.4 ser vi en 2D motsvarighet av ett GVT (Glest VoxelTräd) där man
bara fortsätter förgrena en nod om det finns underliggande geometri (voxlar). När
nodernas storlek nått ett visst tröskelvärde slutar man att att förgrena dem och gör
noderna till voxlar (ifyllda rutorna till vänster i figur 4.4). Denna rekursiva process
skapar ett träd med max fördelningsfaktor 8 per nod som vi ser till höger i figur 4.4
(figuren är 2D fallet med max fördelningsfaktor 4).
Processen är svårare att visualisera för 3D fallet (octree) men om man sätter
kameran väldigt nära en GVT-modell och gör en rendering kan man se voxlarna
(figur 4.5). Notera att i praktiken ska kameran aldrig komma så pass nära GVTmodellen och figur 4.5 är enbart till för att symbolisera GVT-tekniken.
Förutsatt att vi bara sparar ytor i modellen och att voxelstorleken är någorlunda
liten jämfört med ytorna vi vill spara får vi nu en minneskonsumtion som växer närmare mantelytan (O(n2 )) av modellen jämfört med volymen (O(n3 )). Anledningen
till att jag säger närmare mantelytan beror på att den exakta minneskomplexiteten
varierar från modell till modell.
15
KAPITEL 4. TEORI
Figur 4.4. I vänstra bilden ser vi den geometriska aspekten av 2D versionen av ett
octree (quadtree) och i högra ser vi den matematiska.
Figur 4.5. Om kameran kommer för nära modellen ser man enskilda voxlar. Detta
kallas för LEGO effekten.
I tabell 4.1 har jag listat antal noder per nivå i trädet för en viss GVT-modell.
De exakta siffrorna är inte relevanta men vi kan se att antalet noder växer med ca
4 (LAD = 2 motsvarar O(n2 )) per nivå för de högra nivåerna, vilket bekräftar att
antalet noder växer närmare mantelytan av modellen, jämfört med volymen.
4.5
Dataseparation
När man sparar en GVT-modell i minnet eller på hårdisken kan man dela upp
voxeldata i två delar: Den ena definierar jag som topologidata och den andra som
shadingdata. Shadingdata är typiskt data som färg, normal och reflexion, alltså
sådan data som behövs för att utföra ljusberäkningar (shading). Topologidata är
16
4.5. DATASEPARATION
Tabell 4.1. Visar antal noder per nivå för en specifik GVT-modell. LAD värdet
är andra logaritmen av skillnaden mellan nuvarande och föregående nivå (LAD =
log2 (Xn − Xn−1 ), där X är antal noder och n är nivå)
Nivå
0
1
2
3
4
5
6
7
8
9
10
Antal noder
1
2
12
27
91
332
1272
5195
20942
82971
313784
LAD
0.0
1.0
2.58
1.17
1.75
1.87
1.94
2.03
2.01
1.99
1.92
data som bestämmer GVT-modellens form. Majoriteten av detta arbete behandlar
topologidata då det visar sig vara ett mycket svårare problem en shadingdata.
4.5.1
Topologidata
Det finns flera olika varianter hur man kan spara topologidata. Valet av datastruktur är en av de viktigaste delarna av en effektiv implementering av GVTsystemet och jag har därför lagt stora möda på att undersöka de olika datastrukturskandidaterna. Nedan följer en beskrivning av de problem som topologidatan ska
lösa.
Som vi ser i figur 4.6 har varje nod i en GVT-modell potentiellt upp till åtta
barn. För att kunna definiera trädets form (topologi) behöver vi veta två saker för
varje nod:
1. Finns det ett barn c? (där c är ett tal mellan 0 och 7)
2. Var i minnet finns barn c?
Detta vill man göra så flexibelt, snabbt och minneseffektivt som möjligt. I kapitel 5 undersöker jag de olika datastrukturs-kandidaterna för att lösa ovanstående
problem.
4.5.2
Shadingdata
Varje nod i GVT-modellen har utöver topologidata också shadingdata. Denna data
används vid rendering för att bestämma utseendet på noden. Den enklaste shadingdata sparar bara en färg per nod men om man vill ha mer avancerade ljussättning
kan varje nod även behöva spara data som t.ex. normal och reflexion.
17
KAPITEL 4. TEORI
Figur 4.6. En nod med dess 8 barn (childs) utritade.
En praktisk förenkling av shadingdata är att spara den i en lista av kanaler med
1 byte per kanal. Detta gör att övriga delar av koden inte behöver veta exakt vilken
kanal som betyder vad, som i sin tur underlättar för saker som mipmapping (kapitel
4.6) och komprimering.
I tabell 4.2 listar jag några av de vanligaste shadingkanalerna.
Tabell 4.2. Visar några av de vanligaste kanalerna för en voxel.
Typ
Färg
Normal
Specular
Shininess
Transparans
4.6
Antal kanaler
3
3
1
1
1
Mipmapping
En viktig egenskap hos GVT-tekniken är så kallad mipmapping vilket är en metod
för att nersampla shadingdata upp i trädet. Mipmapping är en metod som har
används på texturer i många år för att skapa en miniatyrversion av texturen som
sedan kan användas när texturen ses på långt avstånd. Detta görs dels i syfte att
minska aliasing och dels för att minska mängden minne som behöver läsas in. I figur
4.7 ser vi ett exempel på 2D mipmapping.
18
4.7. PAGING
Figur 4.7. 2D versionen av mipmapping
Mipmapping i en GVT-modell är samma princip men överförd till 3D fallet.
Själva mipmap-processen fungerar så att varje nod tar genomsnittsvärdet per kanal
i shadingdatan av alla sina barn och sätter till sitt eget värde:
n0 =
n0 + n1 + n2 + n3 + n4 + n5 + n6 + n7
8
Detta fortsätter sedan rekursivt upp i trädet enda tills man når roten. Varje nod
har nu ett genomsnittsvärde av alla noder under sig vilket betyder att varje nod
(utom löven) blir en miniatyr av hela trädet under sig.
GVT-modellen blir nu vad som kallas för en ”multiresolution model” [Samet 2006,
s. 391]. Detta kan användas för att dynamiskt bestämma upplösningen på modellen
vid t.ex. rendering. En modell som är långt från kameran behöver inte ha lika hög
upplösning som en modell som är nära. Detta är en mycket viktig egenskap hos
GVT-tekniken då det dels minskar aliasing och dels minskar mängden data man
måste hålla minnet/cachen.
För att illustrera mipmapping av en GVT-modell har jag i figur 4.8 två versioner
av samma modell där den högra är en mipmappad version (3 mipmap-steg för att
framhäva effekten). Om man såg dessa bilder på tillräckligt långt avstånd skulle
man inte kunna skilja dem åt vilket är precis vad mipmapping utnyttjar.
Notera här att vi både samplat ner geometri- och färg-data. Detta är något som
inte går i triangelgrafik då man måste hantera trianglar och texturer separat.
4.7
Paging
Ett av målen för min implementering är att inte hela GVT-modellen ska behöva
ligga i minnet för att kunna rendera och editera den. Detta är nödvändigt eftersom
en GVT-modell kan vara betydligt större än vad som går in i en 32-bitarsprocess
virtuella adressrymd (2 GB). Lösningen på problemet ligger i att på något sätt
19
KAPITEL 4. TEORI
Figur 4.8. Två bilder av samma modell. Den högra är 3 mipmap steg lägre än den
vänstra.
klumpa ihop noder i block, där vi sedan bara har de block som vi behöver för
tillfället i minnet. I operativsystem kallas denna process för paging och jag har valt
att använda samma benämning.
För att förstå varför paging fungerar så bra i ett GVT-system måste man förstå
de tekniska implikationer som mipmapping medför. Datamängden från en GVTmodell som vi max kan behöva för att rendera en bild är enbart relaterat till upplösningen på bilden och inte till modellstorleken. Detta är så viktigt att jag tänkte
upprepa det: Max datamängden för att rendera en bild är enbart relaterat till upplösningen på bilden och inte till modellstorleken.
Vi kommer nu till frågan över hur dessa pageblock ska konstrueras för att på
bästa sätt minimera mängden data som måste läsas och skrivas till disk för att
kunna rendera en bild samt göra förändringar i modellen. Jag testade flera olika
metoder innan jag bestämde mig för vilken jag tyckte var bäst. Både [Olick 2008] och
[Laine 2010] använder sig också av ett paging system där mitt liknar [Laine 2010].
Det finns två varianter för att gruppera noder i pages som jag kallar för horisontal
och vertikal paging. I horisontal paging är alla noder i en viss page på samma nivå i
trädet medan i vertikal paging behöver så inte vara fallet. Skillnaden beskrivs bäst
med en bild. I figur 4.9 ser vi två bilder den vänstra är exempel på horisontalpaging
och i högra på vertikalpaging.
Oavsett om vi använder horisontal- eller vertikal-paging kommer varje page vara
begränsad till en viss del av modellen. Pagingen delar därmed upp modellen i en
form av rutnät som vi ser exempel på i figur 4.10. Detta är önskvärt vid editering
av modellen då majoriteten av alla förändringar är lokala.
4.8
Rendering en GVT-modell
Även om rendering inte ingår i mitt arbete tänkte jag ge en snabb förklaring
hur detta går till. Om läsaren är intresserade av hur rendering av GVT-modeller
kan implementeras finns det flera rapporter som behandlar detta: [Samet 1989]
20
4.8. RENDERING EN GVT-MODELL
Figur 4.9. Till vänster ser vi ett exempel på horisontalpaging. Till höger ser vi ett
exempel av vertikalpaging.
Figur 4.10. En GVT-modell där pages är symboliserade med olika färger.
[Knoll 2006] [Crassin 2009] [Knoll 2009] [Krishnamachari 2009] och [Laine 2010].
Att rendera en GVT-modell på skärmen görs enklast med ray tracing. Tekniken
bygger på algoritmen som [Ericson 2005, p. 179] sammanfattar och avgör om en
stråle (ray) och en låda (box) skär varandra. För att ta reda på vilken voxel strålen
träffar först börjar man med att se om strålen skär lådan för rotnoden i GVT-trädet.
Om den inte skär lådan för rotnoden vet vi att strålen missar GVT-modellen helt.
Om däremot strålen träffar lådan fortsätter vi rekursivt att se vilka av rotnodens
barn strålen skär. Förutsatt att vi itererar nodernas barn i rätt ordning (framifrån,
från kameran sett) vet vi att så fort vi stöter på ett löv (voxel) i trädet är detta den
första voxel strålen träffar.
En viktig teknik vid rendering som använder det faktum att en GVT-modell är
en så kallad ”multiresolution model” (kapitel 4.6) är att sluta traversera trädet när
en specifik nod har mindre projektionsarea på skärmen än en pixel [Knoll 2006].
Detta minimerar aliasing, gör renderingen snabbare samt minskar mängden noder
vi behöver ha i minnet.
21
KAPITEL 4. TEORI
4.9
Voxalisering
När man vill skapa en GVT-modell är det viktigt att man kan importera etablerade
modellformat. Majoriteten av all 3D-modeller sparas i dagsläget som en ”triangelsoppa” med tillhörande texturer. Det är därför viktigt att kunna göra en så kallad
voxalisering av en triangelsoppa till motsvarande GVT-modell. Denna process är
en form av 3D rasterisering där vi går från vektorgrafik till voxelgrafik. [Olick 2008]
och [Zesheng 1991] nämner flera olika sätt detta kan göras på. Nedan listar jag tre
av dem:
1. ”3D scanline conversion” bygger på klassisk 2D scanline conversion [Foley 1996]
överförd till 3D fallet [Houng 1998] [Kaufman 1986].
2. ”Divide and Conquer” söker rekursivt igenom trädet och testar om en specifik
nod skär någon av trianglarna [Zesheng 1991].
3. ”Subdivision” liknar REYES algoritm [Cortes 2006, kap. 1] där varje triangel
”subdividas” tills det inte finns några hål i den för att sedan konvertera alla
vertexes till voxlar.
Det har gjorts mycket forskning på ”3D scanline conversion”-metoden [Kaufman 1986]
[Houng 1998] och jag har därför valt att använda denna metod.
Jag har även undersökt ”Divide and Conquer”-metoden under arbetet. Metoden har en intressant fördel jämfört med ”3D scanline conversion”-metoden: Med
hjälp av en kombination av bredden-först och djupet-först sökning kan man stoppa
voxaliseringen när som helst och få en GVT-modell som går att visa för användaren
även om den inte visas i full upplösning. Tyvärr faller dock ”Divide and Conquer”metoden på grund av prestanda. Jag lyckades som mest få upp metoden till enbart
en tiondel av hastigheten mot ”3D scanline conversion”-metoden vilket är för dåligt
för att kunna används i praktiken. [Zesheng 1991] poängterar dock att ”Divide and
Conquer” kan vara en bra metod för voxalisering av enklare konvexa objekt som:
plan, polyeders, sfärer etc.
4.10
Transformation
Att rotera en GVT-modell runt en godtycklig axel är inte trivialt [Zesheng 1991].
För att komma runt detta problem samt för att kunna utföra alla andra former
av linjära transformationer använder vi en matris M för varje GVT-modell. Alla
former av linjära operationer man vill utföra på GVT-modellen utförs nu enbart på
M vilket självklart går i O(1).
Innan vi vill utföra ray tracing mot GVT-modellen måste vi överföra den till
modellens koordinatsystem:
r0 0 = M −1 r0
v0 = M −1 v
22
(4.1)
4.11. SMP (PARALLELLISERING PÅ CPU:N)
I formel 4.1 förutsätter jag att M är ortogonal. Att sedan transformera tillbaka
den potentiella träffpunkten är lätt:
x0 = M x
Problemet med att rotera en specifik del av GVT-modellen kvarstår dock. Detta
ingår inte i detta arbete och jag hänvisar i stället till [Zesheng 1991] som presenterar
en intressant metod.
4.11
SMP (Parallellisering på CPU:n)
Dagens processorer kommer nästan alltid med två eller fler kärnor. För att utnyttja
alla prestanda till max hos dagens processorer måste programmen delas upp i flera
trådar med hjälp av SMP.
Det bästa sättet att parallellisera ett problem är att dela upp problemet i minst
lika många delproblem som det finns kärnor. Dessa delproblem kan sedan köras på
varsin processortråd.
4.12
Dataåtkomst
När vi vill komma åt till topologidata och shadingdata från GVT-modellen har jag
definierat tre olika åtkomstlägen:
1. Read
Vi får enbart läsa topologidata och shadingdata. Vanligaste användning är för
rendering.
2. Touch
Vi får både läsa och skriva shadingdata, men enbart läsa topologidata. Vanligast för mipmapping.
3. Forge
Vi får skriva och läsa både topologidata och shadingdata. Vanligast för voxalisering av vektorgrafik.
Dessa lägen får stor betydelse när vi använder SMP för att parallellisera arbetet.
23
Kapitel 5
Analys av datastrukturer
I detta kapitel går jag igenom de datastrukturer jag undersökt för att spara GVTmodellens topologidata. I kapitel 6 använder jag sedan några av dessa datastrukturer
för att uppfylla de mål jag satte upp i kapitel 2.2.
5.1
Översikt och problemställning
Jag har undersökt följande datastrukturer: Pekarträd, XYZ-hashtabell, Mortonhashtabell, trädkod samt trädkod-offset. Alla ovanstående datastrukturer försöker
lösa problemet som jag ställde upp i kapitel 4.5.1:
1. Finns det ett barn c (där c är ett tal mellan 0 och 7)?
2. Var i minnet finns barn c?
[Samet 1987] som i sin tur refererar till [Knuth 1975] visar att det går att dela
in alla olika implementeringar av träddatastrukturer i tre kategorier. Nedan ger jag
en kort beskrivning av dessa tre och vilka kategorier mina datastrukturer tillhör.
För en mer utförlig beskrivning hänvisar jag till [Samet 1987].
1. Pekarträd där varje nod sparar en pekare till sina barn. Min pekarträd-lösning
tillhör denna kategori.
2. Förbestämd traversering utnyttjar den inbördes ordningen mellan noderna i
minnet för att spara all topologidata. Min trädkod-lösning tillhör denna denna
kategori och min trädkod-offset-lösning är en modifikation av den.
3. Dewey-decimal kodning kommer från det populära sättet att organisera ett
bibliotek [Dewey 1876] där varje nod i trädet får en distinkt kod som beskriver
hur man tar sig till den från roten. Min Morton-Hashtabell-lösning tillhör
denna kategori och XYZ-hashtabell-lösningen är en modifikation av den.
25
KAPITEL 5. ANALYS AV DATASTRUKTURER
5.2
5.2.1
Grundläggande datastrukturer för GVT
Pekarträd
Det enklast sättet att skapa en datastruktur som uppfyller målen jag satte upp i
kapitel 4.5.1 är följande:
struct node {
struct node *childs[8];
};
struct node *root;
Alltså 8 pekare per nod som pekar vidare till de potentiella barnen. Om ett
specifikt barn inte finns sparar vi NULL i barnpekaren. Denna datastruktur tar 32
bytes per nod (förutsatt att en pekare tar 4 bytes) vilket är ganska mycket men för
det får vi en datastruktur som är blixtsnabb att traversera.
5.2.2
XYZ-Hashtabell
Att använda en hashtabell som datastruktur har varit en populär implementeringsmetod för voxelgrafik [Ericson 2005, s. 288], ett exempel på detta är [Knoll 2006,
Knoll 2009] som använder en hashtabell-baserad datastruktur för voxelrendering.
Min implementering av XYZ-hashtabell datastrukturen ser ut på följande sätt:
struct node {
struct node *next;
int
x, y, z;
};
struct node *hash_buckets[MAX_HASH];
int
root_x, root_y, root_z;
Varje nod sparar sin rumsposition i x, y, z variablerna samt en pekare till nästa
nod i samma hash-hink. När vi traverserar denna datastruktur sparar vi alltid nuvarande x, y, z värde och använder sedan en enkel funktion med invärde c (ett tal
mellan 0 och 7) för att ta oss till ett specifikt barn.
Det finns dock ett allvarligt problem med denna datastruktur: Vi vet inte i förväg
om ett visst barn finns eller ej utan vi måste först göra ett uppslag i hashtabellen
för att avgöra det. Detta blir en extra kostnad som slöar ner traversen men det
finns lyckligtvis ett sätta att undkomma problemet: För varje nod introducerar
vi en så kallad childbits variabel, denna variabel är en byte (8 bitar) där varje
bit representerar om det finns en nod för ett specifikt för barn c. Den nya noddatastrukturen ser nu ut på följande sätt:
26
5.2. GRUNDLÄGGANDE DATASTRUKTURER FÖR GVT
struct node {
struct node *next;
int
x, y, z;
byte
childbits;
};
Ovanstående datastruktur tar 17 bytes per nod plus hashtabellens storlek (kompilatorn kan även avrunda den till 20 bytes på grund av ”aligning”). Om man vill
minimera antalet hashkollisioner bör hashtabellen vara i samma storleksordning som
antalet noder. Detta ger ytterligare ca 4 bytes per nod som leder till en nodstorlek
på ca 21 bytes.
21 bytes är ca två tredjedelar av utrymmet för pekarträd-lösningen vilket är en
förbättring men fortfarande ganska mycket. Det skulle gå att minska precisionen
på x, y, z variablerna då 32 bitar ofta är onödigt mycket. Tyvärr är antagligen 16
bitar för lite och 24 bitar är krångligt att hantera vilket gör att vi är fast med 32
bitar.
En fördel med XYZ-hashtabell-lösningen över pekarträd-lösningen är att det blir
trivialt att hitta en specifik nods granne (bara lägg-till/dra-ifrån 1 från x, y eller z
variabeln) medan det i pekarträd-lösningen är betydligt mer kostsamt.
En annan fördel är att vi undviker direkta minnesreferenser vilket kommer visa
sig ha en stor fördel vid undo-redo samt paging.
Ett problem med XYZ hashtabell-lösningen datastruktur är att hashtabellens
storlek inte kan ändras hur som helst medan man kör programmet utan att iterera
igenom alla noder. Att detta är en nackdel beror på att stora modeller vill ha en
stor hashtabell medan för små modeller tar en stor hashtabell bara onödig plats.
Det finns metoder som linjär och spiral hashning [Samet 2006, s. 729] som kommer runt detta problem, dessa metoder introducerar dock andra problem och ökar
komplexiteten på implementering vilket jag vill undvika.
5.2.3
Morton-Hashtabell
Morton key’s [Ericson 2005, s. 316] är ett koncept relaterat till childbits som går
ut på att varje position i ett octree kan representeras av en serie ettor och nollor
grupperade om 3 där varje grupp säger vilken av de 8 barn man ska ta för att
komma till den specifika noden i fråga. Enklaste sättet att generera en Morton key
för en viss nod är att konkatenera alla c (ett tal mellan 0 och 7, alltså 3 bitar) vi
har använt för att komma ner till den specifika noden.
Storleken för att identifiera en viss nod blir octree’s maxhöjd multiplicerat med
3. En GVT-maxhöjd på 21 skulle räcka för i stort sätt alla modeller vilket skulle
ge en Morton key storlek på 63 bitar per nod. Min implementering av en Mortonhashtabell lösning ser ut på följande sätt:
struct node {
struct node *next;
27
KAPITEL 5. ANALYS AV DATASTRUKTURER
long long
byte
mortonkey;
childbits;
};
struct node *tabel_buckets[MAX_HASH];
int
root_mortonkey;
Datastrukturen tar upp 13 bytes per nod plus hashtabellens storlek vilket ger
ca 17 bytes per nod. Morton-hashtabell-lösningen har samma fördelar som XYZhashtabell-lösningen men vi tjänar 4 bytes per nod samt att det är lite enklare att
traversera trädet.
Det visar sig dock att Morton-hashtabell-lösningen har en rejäl svaghet om
man vill ha möjlighet att lägga till en större rotnod när man redan har en stor
GVT-modell. Detta kräver nämligen att alla Morton key’s i hela modellen måste
omkalkyleras vilket blir enormt kostsamt.
5.2.4
Trädkod
Det går att visa att all topologidata kan komprimeras ner till enbart 1 byte per
nod, både [Olick 2008] och [Samet 1987] poängterar detta faktum. Tekniken kallas
trädkod (treecode) och introduceras av [Oliver 1983]. Tekniken bygger på att serielarisera hela GVT-modellen med bredden-först sökning där varje nivå i trädet
sparas i en egen lista (figur 5.1). Det enda vi då behöver spara ut är per nod är
childbits (1 byte). När vi sedan ska traversera det serialiserade GVT-trädet gör man
på följande sätt:
1. Att veta om ett specifikt barn finns är trivialt då vi enbart tittar på nodens
childbits (det enda vi vet om noden).
2. När vi sedan ska traversera ner till ett specifikt barn hoppar vi tillbaka till
listans start och räknar hur många 1:or (barn) som finns innan vårt specifika
barn. Vi går sedan till listan för nästa nivå i trädet och hoppar in så många
noder som vi räknade 1:or på den tidigare listan. Vi står nu i nodens barn.
Fördelarna med denna metod är att varje nod enbart tar 1 byte. Nackdelen är
att det blir enormt långsamt att traversera.
5.2.5
Trädkod-offset
Om vi använder samma princip som i trädkod-lösningen, men för varje nod även
sparar hur många 1:or det finns före i listan, skulle traverseringen kunna göras
mycket snabbare. Vi kan dock gå ett steg längre och bunta in nästa nivås liststart
så vi slipper ta reda på den separat. Nod-datastrukturen ser då ut på följande sätt:
28
5.2. GRUNDLÄGGANDE DATASTRUKTURER FÖR GVT
Figur 5.1. I trädkod datastrukturen sparar vi varje nivå av trädet i en egen lista.
struct node {
int
offset;
byte
childbits;
};
int
root_offset;
För att traversera till ett specifikt barn c använder vi nuvarande nodpositionen,
adderar offset och adderar ytterligare så många 1:or vi hittar i childbits före c (Detta
kan göras extremt effektivt med en lookup-tabell).
Fördelen med denna implementering är att den enbart tar 5 bytes per nod
(4 bytes om man bara behöver 24 bitar för offset) och är snabb att traversera.
[Laine 2010] antyder till och med att denna metod kan vara snabbare att traversera
än pekarträd lösning på grund av att mängden cachemissar minskar då vi använder
betydligt mindre minne.
Nackdelen med denna datastruktur är att ändringar av trädet blir kostsamma
då stora delar av noderna måste räknas om.
5.2.6
Sammanfattning av grundläggande datastrukturer
I tabell 5.1 har jag gjort en sammanställning över alla grundläggande datastrukturer
med deras fördelar respektive nackdelar.
29
KAPITEL 5. ANALYS AV DATASTRUKTURER
Tabell 5.1. Sammanfattning av alla grundläggande datastrukturer.
Datastruktur
Pekarträd
XYZ-hashtabell
Nodstorlek
32 (100%)
21 (66%)
Fördelar
Snabb
Snabb, flexibel
Morton-hashtabell
17 (53%)
Snabb
Trädkod
Trädkod offset
1 (3%)
5 (16%)
Liten plats
Snabb läsning, liten
plats
5.3
5.3.1
Nackdelar
Minnesstorlek
Minnesstorlek,
förbestämd hashtabell
Minnesstorlek,
förbestämd hashtabell,
fixed rotnod
Långsam
Långsam editering
Förbättringar till datastrukturerna
Leafbits
Om vi mestadels sparar ytor i vår GVT-modell kommer varje nod i snitt ha ca 4
barn. En intressant fråga är då: hur stor andel av alla noder är löv? För att svara
på denna fråga kan vi räkna baklänges från löven sett. Om vi sätter antalet löv till
1 ser vi att nivån ovan löven blir ca 0.25, ytterligare en nivå upp får vi 0.0625. Om
man fortsätter denna beräkning till nivåstorleken blir försumbar ser vi att löven står
för ca 70% av all noder. Detta bekräftas av av tabell 5.2.
Tabell 5.2. Visar antal noder per nivå för en specifik GVT-modell.
Nivå
0
1
2
3
4
5
6
7
8
9
10
11
Antal noder
1
2
12
27
91
332
1272
5195
20942
82971
313784
1070760
Andel
0.0%
0.0%
0.0%
0.0%
0.0%
0.0%
0.1%
0.3%
1.4%
5.5%
21.0%
71.6%
Ett löv behöver inte spara lika mycket topologiinformation som en nod då lövet
30
5.3. FÖRBÄTTRINGAR TILL DATASTRUKTURERNA
omöjligt kan ha barn. Detta har fått [Knoll 2009] att introducera begreppet ”cap
nodes” där vi sparar mindre information för löven än för noderna i syfte att spara
minne. Notera att jag nu bara pratar om topologidata, shadingdata måste vi fortfarande spara för alla löv.
Ett smart tillägg till alla ovanstående datastrukturer är att för varje nod lägga
till en så kallad leafbits variabel. Leafbit variabeln har samma upplägg som childbit
variabeln men där varje bit i stället avgör om barnet är ett löv eller ej. Detta gör
att vi inte behöver spara någon topologidata för löven vilket i sin tur kan spara oss
upp till 70% av all topologidata för kostnaden av 1 extra byte per nod. [Laine 2010]
använder en liknande lösning.
5.3.2
Nodblock
XYZ-hashtabell datastrukturen är en av de mest attraktiva lösningar men den har
två huvudproblem:
1. Tar ganska mycket plats i minne (21 bytes per nod).
2. Hashtabellens storlek skalar inte bra mellan både stora och små modeller.
Det går dock att lösa båda ovanstående problem genom att dela in alla noder
i block. Varje block är en kub i rummet som max kan innehålla 2563 noder. Detta
gör att x, y, z variablerna i nodstrukturen kan använda enbart 1 byte per variabel.
Den nya noddatastrukturen ser ut på följande sätt:
struct node {
struct node *next;
byte
x, y, z;
byte
childbits;
};
Denna noddatastruktur tar enbart 8 bytes (jämfört med 17 bytes) plus hashtabellens storlek, vilket är en klar förbättring. En annan fördel är att vi nu har en
hashtabell per block där vi vet blockets normalstorlek vilket gör att vi kan anpassa
hashtabellens storlek till detta. Datastrukturen för huvudet av ett nodblock ser ut
på följande sätt:
struct block {
struct node
struct block
};
*hash_buckets[MAX_HASH];
*childs[8];
Nodblock är även en form av horisontal paging vilket gör nodblock ännu mer
attraktivt när vi kommer till att implementera paging.
31
KAPITEL 5. ANALYS AV DATASTRUKTURER
5.4
5.4.1
Val av datastruktur
Tre olika datastrukturer
Val av noddatastruktur är ett av de viktigaste besluten för hela GVT-systemet. När
vi hanterar en GVT-modell finns det tre kategorier på saker vi vill kunna göra med
den:
• Generering och editering.
• CUDA Rendering
• Lagring på disk
Dessa tre kategorier kommer ha sina respektive datastrukturer på tre fysikaliska
olika ställen. Generering och editering sker på CPU:n, CUDA rendering på grafikkortet och lagring på disk på en hårdisk, CD, DVD eller Blue-Ray.
För maximera vad som är viktigt för varje enskild kategori har jag valt att
använda olika datastrukturer för de olika kategorierna. Nedan listar jag i prioriteringsordning vad jag anser viktigast för de olika kategorierna:
• Generering och editering (CPU)
1. Möjlighet till Layers och Undo
2. Skriv och lägg-till/ta-bort hastighet
3. Undvik fragmentering
4. Läshastighet
5. Minnesutrymme
• Rendering (GPU)
1. Läshastighet
2. Cachevänligt
3. Minnesutrymme
• Lagring på disk (Hårdisk/CD/DVD)
1. Horisontalpaging
2. Diskutrymme
Den viktigaste kategorin för mitt arbete är generering och editering. Lagring på
disk tillhör förvisso fokus för mitt arbete men detta kan behandlas som en specialfall
när vi väl vill skriva ner noddata på disk.
I kapitel 5.4.3 ger jag en kort beskrivning av vilka datastrukturer som är intressant för CUDA rendering samt hur jag konverterar till dessa.
32
5.4. VAL AV DATASTRUKTUR
5.4.2
Datastruktur för generering och editering
Ett av de viktigaste målen för datastrukturen för generering och editering är att vi
snabbt kan lägga-till och ta-bort noder. Detta utesluter direkt trädkod och trädkodoffset lösningarna. Vidare kan vi även utesluta Morton-hashtabell-lösningen då ett
av målen vi satte upp i kapitel 2.2 kräver att storleken av modellen måste kunna
växa vartefter användaren jobbar med modellen, något som inte går att göra med
en Morton hashtabell.
Nu återstår två datastrukturer: pekarträd och XYZ-hashtabell. Jag har provat
att implementera båda och pekarträdet har fördelen att vara snabbare medan XYZhashtabellen tar mindre minne. I slutändan var jag dock tvungen att även utesluta
pekarträd-lösningen då denna visade sig vara svår att implementera tillsammans
med paging och undo-redo vilket jag återkommer till i kapitel 6.3 och 6.5.
Tabell 5.3. Sammanställning av val av datastruktur för generering och editering.
Datastruktur
Trädkod
Trädkod-offset
Status
Utesluten
Utesluten
Morton hashtabell
Utesluten
Pekarträd
Utesluten
XYZ-hashblock
Uppfyller alla krav
Kommentar
Långsam att traversera
Långsam att lägga till och
ta bort noder
Ineffektivt att ändra rotnoden
Stor plats, svårt att implementera undo-redo samt
paging
För att undkomma XYZ-hashtabellens svagheter (mycket minne och förbestämd
tabellstorlek) använder jag nodblock metoden jag beskrev i kapitel 5.3.2. Detta
visar sig även vara en fenomenal indelning för paging vilket jag återkommer till i
kapitel 6.3. I tabell 5.3 har jag gjort en sammanställning av valet av datastruktur
för generering och editering.
5.4.3
Datastruktur för CUDA rendering
Det är fördelaktigt att kunna exportera GVT-modellen till ett format som lämpar
sig för snabb rendering på CUDA. [Laine 2010] föreslår en trädkod-offset liknande
lösning med leafbits för att undvika att behöva spara topologidata för löven. Vidare
föreslår de ett vertikalt pagingschema för att kunna rendera större GVT-modeller
än vad som får plats i CUDA minnet.
Även jag har implementerat en CUDA renderare som fungerar på liknande sätt
och kan bekräfta att trädkod-offset-lösningen med vertikal paging är en mycket
effektiv datastruktur. För information hur man konstruerar en effektiv GTV rend33
KAPITEL 5. ANALYS AV DATASTRUKTURER
erare för CUDA hänvisar jag till [Laine 2010]. För en mer generell beskrivning av
GVT-rendering hänvisar jag till [Samet 1989].
För att exportera en GVT-modell med XYZ-hastabell som datastruktur till en
trädkod-offset datastruktur väljer man en startnod i XYZ-hashtabell trädet (välj
rotnoden om man vill ha hela trädet). Man traverserar sedan trädet rekursivt tills
man kommer till ett löv eller ett fördefinerat stoppdjup. I varje traverseringssteg
skriver man ut nuvarande nod till en sidobuffer i trädkod-offset format. Denna
sidobuffer kan sedan laddas in i grafikkortet för att renderas av CUDA.
När GVT-modellen förändras måste vi uppdatera sidobuffern. Men då de flesta
förändringar är lokala behöver vi bara uppdatera den del av sidobuffern som innehåller de förändrade noderna vilket snabbar upp processen.
5.5
Metadata
Varje GVT-modell har även lite metadata som jag presenterar nedan:
struct layer {
vec3
vec3
int
};
mins;
maxs;
x, y, z;
Min- och maxvariablerna är lådan för rotnoden. Dessa värden skickas sedan
vidare ner till barnen när vi traverserar GVT-trädet som jag visar i kapitel 6.1.2.
Genom att göra på detta vis kan vi få reda på den aktuella lådan för varje nod utan
att behöva spara den på nodnivå.
x, y, z variablen är xyz värdet för rotnoden i GVT-modellen. Traverseringar av
GVT-modellen börjar alltid i denna nod.
34
Kapitel 6
Analys av algoritmer
I detta kapitel beskriver jag de algoritmer – tillsammans med några av datastrukturerna
från kapitel 5 – jag använder för att uppfylla målen från kapitel 2.2.
6.1
Nodtraversering
Det finns flera tillfällen när vi vill traversera igenom GVT-modellen. Det två vanligaste fallet är vid mipmapping och när vi vill skapa en ny nod. [Levitin 2003, p.
162] visar att det går att traversera träd på två olika sätt: bredden-först- (BFS)
eller djupet-först-sökning (DFS).
6.1.1
Bredden-först-sökning (BFS)
Bredden-först-sökning kräver en kö som är minst lika stor som antal löv i trädet.
Detta gör att sökningen blir svår att implementera i praktiken då en GVT-modell
kan ha fler löv än vad som får plats i minnet.
Bredden-först-sökning kan dock användas om man bara är intresserad av att
traversera den första biten av trädet, något jag återkommer till i kapitel 6.1.4.
6.1.2
Djupet-först-sökning (DFS)
Djupet-först-sökning behöver enbart en stack av samma storlek som maxdjupet i
trädet (några enstaka noder) för att fungera vilket gör att den kan köras på hela
trädet. Nedan ger jag exempel på hur en sådan djupet-först-sökning kan se ut.
1 void DepthFirst ( vec3 mins , vec3 maxs , node_t * node ) {
2
childbits = GetChildBits ( node ) ;
3
4
// ! potenial callback function here !
5
6
if ( ! childbits ) {
7
return ;
8
}
35
KAPITEL 6. ANALYS AV ALGORITMER
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 }
// iterate all potenial childs
for ( c = 0; c < 8; c ++ ) {
// check if this child exist
if ( !( childbits & (1 < < c ) ) ) {
continue ;
}
// create child bounds from parent bounds
PushBounds ( childMins , childMaxs , mins , maxs , c ) ;
// get current child
childNode = GetChild ( node , c ) ;
// continue recursion
DepthFirst ( childMins , childMaxs , childNode ) ;
}
Det finns ett antal saker att notera i ovanstående kod. Det första är användningen av childbits (rad 6 och 14) vilket gör det mycket effektivt att bestämma om ett
visst barn finns eller ej.
Det andra är att vi skickar med den aktuella nodens min- och max-värden
för varje rekursionssteg (rad 1 och 25). Genom att göra på detta vis behöver vi
inte spara dem per nod vilket sparar minne. Funktionen PushBounds (rad 19) kan
implementeras mycket effektivt med en switch-sats samt några additioner.
Det tredje är att det går att stoppa in en callback-funktion i ovanstående kod
(rad 4). Callback-funktionen kan användas för att modifiera shadingdata i noden
samt att bestämma om vi ska fortsätta rekursionen.
Största användingsområdet för ovanstående kod är mipmapping samt voxaliseringsmetoden ”Divide and Conquer”.
6.1.3
Djupet-först-sökning med mipmapping
Vi kan utöka koden i kapitel 6.1.2 för mipmapping. De nya raderna i nedanstående
kod är 10–12, 32 samt 35–37.
1 void DepthFirst ( vec3 mins , vec3 maxs , node_t * node ) {
2
childbits = GetChildBits ( node ) ;
3
4
// ! potenial callback function here !
5
6
if ( ! childbits ) {
7
return ;
8
}
9
36
6.1. NODTRAVERSERING
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 }
6.1.4
// set mipmap to zero
ClearChannels ( mipmap ) ;
mipmapWeight = 0;
// iterate all potenial childs
for ( c = 0; c < 8; c ++ ) {
// check if this child exist
if ( !( childbits & (1 < < c ) ) ) {
continue ;
}
// create child bounds from parent bounds
PushBounds ( childMins , childMaxs , mins , maxs , c ) ;
// get current child
childNode = GetChild ( node , c ) ;
// continue recursion
DepthFirst ( childMins , childMaxs , childNode ) ;
// get child channels and add to mipmap
AddChannels ( mipmap , GetChannels ( childNode ) ) ;
}
// normalize channels and update node
DivideChannels ( mipmap , mipmapWeight ) ;
SetNodeChannels ( node , mipmap ) ;
DFS och BFS kombination
Ett problem med djupet-först-sökning är att det är svårt att parallellisera över
SMP. För att lösa detta problem har jag valt att kombinera DFS och BFS sökning
på följande sätt:
1. Börja med att köra en bredden-först sökning där vi anger ett stoppdjup. Stoppdjupet är maximala djupet vi får traversera i trädet. Detta körs seriellt och
returnerar en lista över alla löv från sökningen.
2. Fortsätt nu genom att iterera igenom alla löv från steg 1 och traversera dessa
med djupet-först-sökning, detta kan köras parallellt. Förutsatt att stoppdjupet i steg 1 är lågt, innehåller detta steg majoriteten av allt jobb.
3. Slutligen kör vi sökningen i steg 1 ytterligare en gång så att mipmappingen
kan fortplanta sig ända upp till roten av trädet, detta körs seriellt.
37
KAPITEL 6. ANALYS AV ALGORITMER
Förutsatt att modellen är stor och man väljer ett bra stoppdjup skalar denna
algoritm linjärt med antalet processorkärnor.
6.1.5
Skapa en nod
Algoritmen för att sätta en specifik nod är en form av djupet-först-sökning. Nedan
är ett exempel på hur det kan se ut:
1 node_t * SetNode ( vec3 pos , vec3 mids , int depth , node_t * n ) {
2
3
// check for stop depth
4
if ( ! depth ) {
5
return ForgeLeafChild ( n , c ) ;
6
}
7
8
// check what child to traverse
9
c = 0;
10
if ( pos . x > mids . x ) c |= (1 < <0) ;
11
if ( pos . y > mids . y ) c |= (1 < <1) ;
12
if ( pos . z > mids . z ) c |= (1 < <2) ;
13
14
// get or create child node
15
childNode = ForgeNodeChild ( n , c ) ;
16
17
// create child mid values from parent
18
PushMids ( childMids , mids , c ) ;
19
20
// continue recursion
21
return SetNode ( pos , childMids , depth - 1 , childNode ) ;
22 }
Algoritmen börjar med add traversera trädet från roten. I varje rekursionssteg
undersöker vi för vilket av de 8 barnen vi ska fortsätta traversen. När vi slutligen
kommit ner till ett visst stoppdjup (när depth är 0) sätter vi noden till ett löv och
returnerar.
Algoritmen för att hämta en specifik voxel liknar ovanstående men med undantaget att vi aldrig ändrar några noder och om ett visst barn inte finns returneras
NULL.
6.2
Förändring av rotnoden
När GVT-modellen växer i rumstorlek på grund av att användaren lägger till fler
och fler noder kommer vi för eller senare till en punkt där den inte längre får plats
i rotnoden. Notera att detta är ett rent geometriskt problem och har inget att göra
med mängden noder i modellen. För att lösa problemet måste vi skapa en ny rotnod,
som är stor nog att innehålla hela den nya noden.
38
6.2. FÖRÄNDRING AV ROTNODEN
Jag har löst detta problem genom att före varje förändring av topologidata
köra en förberedande funktion som får som invariabler min- och max-värdena för
topologiförändringen. Denna funktion undersöker sedan om min- och max-värdena
får plats i rotnoden av GVT-modellen. Om så inte är fallet skapar vi en ny större
nod och sätter som rotnod, den nya rotnoden får nu den gamla som ett av sina
barn. Denna process upprepas rekursivt tills att min- och max-värdena får plats i
vår rotnod. Figur 6.1 illustrerar processen.
Figur 6.1. När modellen växer sig större än rot noden måste vi skapa en ny rotnod.
Eftersom GVT-trädet är ett tredimensionellt binärt träd (octree) måste sidan
för den nya rotnoden alltid vara dubbelt så stor som den gamla. Det enda beslut
vi har kvar att fatta är åt vilket håll den nya rotnoden ska utvidgas. Det finns 8
möjligheter, då den nya rotnoden har potentiellt upp till 8 barn.
Som vi ser i figur 6.1 är det viktigt att utvidga rotnoden åt rätt håll. I praktiken
kan detta göras på två olika sätt: Det enklaste sättet är att bara varannan gång
utvidga roten i positiv riktning och varannan gång i negativ. Detta har dock nackdelen att GVT-modellen får onödigt många nivåer vilket i sin tur ökar söktiden i
trädet då fler noder måste passeras från rot till löv.
En bättre lösning är att välja åt vilket håll den nya rotnoden ska expanderas
för att bäst passa min- och max-värdena för topologiförändringen. Detta är något
som är möjligt för pekarträd, då man där är fri att välja vilket barn man vill för
gamla roten. Jag har dock inte lyckats lösa detta för XYZ-hashtabell-lösningen då
en viss nods x, y, z variabler avgör vilka förälder den får. Det är möjligt att detta
går att komma runt med någon slags permutations-byte eller liknande. I nuvarande
implementering har jag dock satt upp x, y, z variablerna i första rotnoden på ett
sådant sätt att när vi vill ha en ny rotnod kommer vi att pendla mellan utökning
roten i positiv och negativ riktning.
39
KAPITEL 6. ANALYS AV ALGORITMER
6.3
6.3.1
Paging
Översikt
För att dela upp noderna i block använder jag horisontal paging. Fördelen med
horisontal paging jämfört med vertikal är att vi minimerar onödiga noder i minnet
vid ray tracing genom att bara ladda in så många nivåer av GVT-trädet som vi
behöver.
När man försöker implementera paging visar det sig att pekarträd-lösningen
har en negativ sidoeffekt då den använder direkta minnesreferenser till sina barn.
Detta gör att varje gång man läser in eller skriver ut en page på disk måste alla
minnesreferenser uppdateras vilket komplicerar implementeringen (problemet blir
ännu värre när vi sedan försöker implementera undo-redo).
Detta är den primära anledningen till att jag har valt XYZ-hashtabell med
nodblock som datastruktur för generering och editering (kapitel 5.4.2). Eftersom
XYZ-hashtabell inte använder några direkta minnesreferenser kan vi flytta runt
noder och pages hur som helst i minnet vilket underlättar implementeringen.
Jag gjorde ett försök att implementera paging med pekarträd som datastruktur
men fick ge upp då implementeringen blev så pass krånglig (undo-redo gör den ännu
krångligare) att det blev svårt att förutse alla implikationer av den vilket drastiskt
ökar risken för buggar. Eftersom jag inte har något fungerande alternativ till XYZhashtabell paging kan jag inte mäta hur denna jämför sig med andra lösningar
prestandamässigt.
6.3.2
Page LRU (Least Recently Used)
När GVT-systemet kommer till den punkt där vi behöver flera pages i minnet än
vad det finns minne måste systemet prioritera vilka pages som är viktigast. Vi avgör
detta genom att titta på vilken page som varit oanvänd längst, och sedan byta ut
denna mot den nya pagen som vi behöver. För att ta reda på vilken page som varit
oanvänd längst har jag lagt till en variabel per page som sparar den tiden då den
senast vart traverserad.
Om en viss page var traverserad vid tid t betyder det att dess förälder var
traverserad ögonblicken före t. Vi kan utnyttja detta faktum när vi letar efter den
page som inte använts på längst tid genom att göra en traversering av alla pages
och där vi för varje enskild page fortsätter traversen i det barn med lägst t. När vi
har kommit till ett pagelöv vet vi att denna page har lägst t av alla pages i hela
GVT-modellen och ska därför skrivas ut på disk så vi kan ladda in den nya pagen.
Att hitta LRU (Least Recently Used) pagen går nu i O(log(n)) där n är antal inlästa
pages jämfört med O(n) som skulle behövas vid linjär sökning.
6.3.3
Page metadata
I [Laine 2010] implementering ligger page-metadata alltid i minnet då de hävdar att
page-metadata tar relativt lite plats samt att det är bekvämt att alltid ha tillgång
40
6.3. PAGING
till den.
Page-metadata i mitt fall är 8 stycken barnpekare (32 bytes) samt ca 18 bytes
för övrig information. Även jag började med att alltid lägga page-metadata i minne,
men har dock sedan dess kommit fram till att det är bättre att spara de tillsammans
med övrig pagedata. Huvudanledningen till detta är att för stora modeller kan
även page-metadata ta mycket plats. Som jag skrev ovan tar page-metadata i min
implementering ca 50 bytes per page, riktigt stora modeller kan behöva flera miljoner
pages vilket kan skapa metadata i storleksordningen 250 MB. Så pass mycket data
skapar problem om det alltid måste ligga i minnet dels för att de ockuperar en så
pass stor andel av användarens totala minne, dels för att uppstartningstiden för en
viss modell blir långsam då 250 MB data måste läsas från disk.
Jag har därför valt att lägga page-metadata tillsammans med övrig pagedata
och skapar därför inga begränsningar på hur många pages vi kan ha samt att GVTmodeller får näst intill noll i uppstartningstid. Jag har inte hittat något ställe där
man skulle behöva tillgång till page metadata utan att övrig pagedata redan är
inladdad. Detta gör att [Laine 2010] argument om att det är bekvämt att hålla all
page-metadata i minnet faller.
6.3.4
Page datastruktur
Nedan visar jag de viktigaste variablerna i min pagedatastruktur:
struct page {
int
int
time;
refpush;
// used for paging
// used for undo-redo
// 8 page childs
int
childs[8];
// hashtabel for nodes in this page
struct node
*tabel[MAX_HASH];
};
struct page g_pages[MAX_PAGES];
struct node g_nodes[MAX_NODES];
Time-variabeln används för att avgöra vilken page som det var längst tid sedan
vi traverserade. ”Refpush”-variabeln förklaras i kapitel 6.5.
Childs-variabeln är en pekare till pagens potentiella barn. Detta är inte direkta
minnesreferenser utan anger vilket pagenummer pagen har. Detta pagenummer kan
både används för att läsa pagen från disk eller hämta en page direkt från minnet.
Table-listan är hashtabellen för noderna (vi använder XYZ-hashtabell-lösningen),
där noderna sedan sparas i en global lista.
41
KAPITEL 6. ANALYS AV ALGORITMER
6.3.5
Parallelliserad paging
När vi är tvungen att läsa in en page från disk måste vi se till att ingen annan tråd
redan håller på att läsa in pagen. Enklaste sättet att implementera detta är med ett
RW-lås (Read-Write lås) där skriva (write) betyder ”läsa in en page” i vårat fall.
För att en tråd ska få läsa in en page måste den skaffa ett skriv-lås (bara en
tråd i taget kan ha detta), och ett skriv-lås kan bara utfärdas om alla andra trådar
är i ett säkert läge där de inte läser från den data vi vill skriva till.
För att undvika att en tråd kan få vänta på ett skriv-lås i oändlighet har jag
använt mig av en så kallad svält-fri låsmekanism [Herlihy 2008, kap. 8.3].
6.4
Layers
Layers visar sig ett vara enkelt problem att lösa: Tekniskt sett är layers-möjligheten
att ha flera GVT-modeller i samma fil. Dessa GVT-modeller har ingen data gemensamt och blir därför triviala att implementera.
6.5
6.5.1
Undo-Redo
Algoritm
En viktig funktion för att kunna editera GVT-modeller är att kunna ångra tidigare
förändringar av modellen. Ett trivialt sätt att implementera detta är att före en
förändring av modellen duplicerar man hela modellen och utför sedan förändringen
på enbart en av dubbletterna. Detta är dock helt ohållbart i praktiken förutom för
mycket små modeller av två anledningar:
1. Varje gång vi duplicerar modellen dubblar vi också mängden minne och hårddiskutrymme.
2. Eftersom en modell kan vara på flera GB data kan enorma datamängder
behöva dupliceras före varje förändringar av modellen, vilket gör editering
extremt långsam.
De flesta förändringar användaren förväntas göra är lokala vilket medför att
duplicering av hela modellen inte heller är nödvändigt. Jag har löst detta problem
med hjälp av en funktion jag kallar Fork. Före varje förändring av trädet som man
vill kunna gå tillbaka till med Undo körs funktionen Fork som har följande uppgifter:
1. Duplicera alla metadata om GVT-modellen. Detta inkluderar max- och minvärdena för rotnoden samt ett rotnod-index.
2. Gå till pagen som innehåller rotnoden och öka en variabel som vi kallar för
refpush med ett. Detta går i O(1) tid då det inte innehåller någon iteration
eller rekursion. Notera också att den enda page vi behöver tillgång till är den
som innehåller rotnoden.
42
6.6. LAGRING PÅ DISK
När Fork är klar kan vi börja förändringen av modellen. Alla funktioner som
vill kunna editera GVT-modellen måste använda sig av en intern funktion som jag
kallar GetPage. GetPage tar bland annat en flagga, som talar om för GetPage om
användaren vill editera modellen. Om GetPage finner denna flagga specificerad utför
den följande operationer:
1. Om refpush-variabeln för den page vi vill komma åt är större än 0 måste
vi ”trycka ut” refpush värdet till alla pagebarn. Detta betyder att vi laddar
in alla pagebarn och adderar original-pagens refpush till deras deras refpushvariabel.
2. Pagen är nu redo att klonas vilket betyder att vi skapar en dubblett av pagen
som vi returnerar med GetPage.
Även denna operation går i O(1) tid då en page kan ha max 8 barn samt att
en page bara kan ha ett begränsat antal noder. Den page GetPage returnerar är nu
garanterat att vara unik vilket betyder att enbart denna version av GVT-modellen
(den som Fork returnerade) kan komma åt den. Detta betyder i sin tur att vi kan
ändra pagen utan att förstöra originalet.
6.5.2
Datastruktur
Alla versioner av metadata om GVT-modellen som uppstår av Fork sparar jag i en
lista där man sedan har en pekare till den aktiva versionen av metadatan. Detta
gör det väldigt smidigt att utföra Undo och Redo då vi enbart behöver addera eller
subtrahera 1 från pekaren:
struct history {
boolean
struct layer
} history;
used;
layers[MAX_LAYERS];
struct history g_history[MAX_HISTORY];
struct int g_current;
Allt detta gör att vi kan utföra undo- och redo-kommandona i realtid samt att
en förändring av modellen inte har någon märkbar overhead.
6.6
6.6.1
Lagring på disk
Översikt
Vi måste självklart kunna spara ner GVT-modellen på disk. Det finns tre tillfällen
när GVT-systemet måste använda hårdisken:
43
KAPITEL 6. ANALYS AV ALGORITMER
1. En nod som ska renderas eller editeras finns inte i minnet. GVT-systemet
måste nu läsa in den page som noden tillhör från hårdisken och göra den
tillgänglig i minnet.
2. Det finns inte tillräckligt med minne för att läsa in en page eller allokera
en nod. GVT-systemet letar nu efter den page som varit oanvänd längst och
skriver ut denna till disk varpå den kan tas bort från minnet.
3. Användaren vill stänga programmet. Innan vi stänger programmet måste
GVT-systemet se till att alla noder finns på disk.
GVT-systemet läser aldrig in enskilda noder från disk utan behandlar alltid
diskoperationer på pagenivå. Den primära fördelen med detta är att vi minskar
antalet diskoperationer samt att vi kan utföra komprimering på hela pagen vilket
blir mer effektivt.
Varje page sparas sedan som en virtuell fil på hårddisken. Användaren ser dock
bara en fil på sin hårddisk vilket göra det enkelt för användaren att flytta filen från
en plats till en annan. Den filen som användaren ser består dock internt av flera
virtuella filer: en per page. Att använda en virtuell fil per page är en bra abstraktion
då det blir mycket enklare att uppdatera och ta bort pages.
6.6.2
Komprimering till disk
Eftersom en diskoperation ändå är långsam har vi tid att utföra komprimering av
noderna innan vi skriver dem till disk. Det finns många metoder för att göra detta
på och jag har inte haft tid att undersöka alla.
Komprimering av noderna sker alltid per page. [Olick 2008] visar att det finns
olika former av ”wavelet” komprimering [Sayood 2006, kap. 15] som är effektiv för
GVT-modeller. Dessa algoritmer är antagligen de mest effektiva, men jag har tyvärr
fått lägga dem åt sidan då de inte lämpar sig för modeller som man vill kunna
editera. Anledningen till detta är att om alla pages är beroende av sina föräldrar
blir alla pages i slutändan beroende av rotpagen. Om rotpagen ändras måste vi nu
uppdatera alla pages i hela modellen vilket blir på tok för långsamt.
Jag har därför inriktat mig på den typ av komprimering som enbart använder
data som finns i den aktuella pagen. Min komprimeringsimplementation kan delas
upp i fem steg:
1. Hilbertkurva
Första steget är att iterera igenom alla noder i pagen på ett sådant sätt
att skillnaden från en specifik nod till nästa är så liten som möjligt. Det
finns flera sätt att göra detta på, en av metoderna kallas för en Hilbertkurva
[Hilbert 1891]. Poängen med en Hilbertkurva är att vi enbart flyttar oss ett
steg i taget i x, y eller z led samtidigt som vi ändå i slutändan itererar igenom
alla noder exakt en gång [Samet 2006, s. 200]. Detta kommer leda till bättre
komprimering i steg 3, 4, 5.
44
6.6. LAGRING PÅ DISK
2. Kolumnordning
I stället för att spara noddata grupperat per nod grupperar vi dem i stället
på datatyp. De viktigaste datatyperna är childbits samt alla shadingkanaler.
Detta steg gör de nästkommande komprimeringsstegen mer effektiva. När
detta steg är klart har vi en lista per noddatatyp där listornas längd är antalet
noder vi började med.
3. Deltakomprimering
Varje element i listorna vi fick ut från steg 2 deltas nu från sitt tidigare värde.
Delta betyder att man sparar skillnaden mellan nuvarande och föregående
värde i förhoppningen att förändringen ska vara liten. Notera att i detta steg
har noddatan fortfarande samma storlek som vi började, men vi har nu arrangerat om noddatan på ett sådan sätt att steg 4 och 5 kommer göra ett så
bra jobb som möjligt.
4. Run-Length kodning
Efter steg 2 och 3 är det vanligt att flera av listorna innehåller långa sektioner
av enbart nollor. Childbit-listan för lövpages är ett exempel på detta. RunLength kodning komprimerar ner dessa långa sektioner av nollor (och även
andra värden) till en nolla och en siffra som säger hur många fler nollor som
återstår.
5. Entropikodning
Det sista steget komprimerar listorna ytterligare och bygger på Huffman entropikodning [Huffman 1952]. Tekniken bygger på att om vissa tal uppkommer oftare än andra tal i listan (efter deltakomprimering kommer små tal vara
mycket vanligare än stora) kan vi spara dessa tal med färre bitar och därmed
spara utrymme.
När dessa steg är utförda har vi ett antal listor med komprimerad noddata
som sedan skrivs till disk. När vi sedan ska läsa in noddata från disk kör vi alla
ovanstående steg baklänges.
Hela ovanstående process tar självklart en del tid att köra på CPU:n men det
visar sig fortfarande bara vara en bråkdel av den tiden det tar för CPU:n att vänta på
hårddisken. I vissa fall kan ovanstående process till och med göra paging snabbare,
då mindre mängd data behöver skrivas till hårddisken.
45
Kapitel 7
Implementation
I detta kapitel går jag igenom hur jag har implementerat och strukturerat min prototyp samt de viktigaste gränssnitten. Jag berör även valet av programmeringsspråk
samt operativsystem
7.1
Programmeringsspråk
Jag har valt C som programmeringsspråk. Jag ser framförallt två fördelar med C
framför andra språk:
• Prestanda är alltid mycket viktigt när vi pratar om datorgrafik. C språket har
varit med länge och dagens C kompilatorer har genom åren visat sig kunna
generera mycket snabb kod, näst intill samma prestanda som väloptimerad
assembler.
• Bra språk för bibliotek. C++ kod kan enkelt använda C kod men inte vice
versa. Detta gör att C++ är en nackdel då program skrivna i C får svårare
att använda min GVT-prototyp. Vidare har jag inte hittat något ställe i implementeringen där C++ skulle ha en stor fördel över C vilket ytterligare talar
för C som språk då det är enklare och snabbare kompilera.
7.2
Operativsystemsmiljö
Jag har valt att implementera prototypen på Windows-datorer. Största anledningen
till detta är att jag själv använder Windows till vardags. Men Windows är också en
välbeprövad plattform med gott stöd för bland annat CUDA.
Hjärtat i min implementering använder dock minimialt med operativsystemsfunktionalitet och skulle därför vara mycket enkelt att porta till andra plattformar
som t.ex. Linux och MacOS X.
47
KAPITEL 7. IMPLEMENTATION
7.3
Implementeringsstruktur
Målet för prototypen är att skapa ett programmeringsbibliotek som gömmer alla
detaljer som till exempel nod- och page-datastrukturer från användaren (nu pratar
jag om användaren av programmeringsbiblioteket vilket inte nödvändigtvis är samma som slutanvändaren). Detta programmeringsbibliotek kan sedan användas av
alla typer av program som behöver hantera GVT-modeller genom ett enkel gränssnitt. Programmeringsbiblioteket kallar jag för SVO-modulen (SVO från en engelska
benämningen av GVT).
För att strukturera implementeringen av SVO-modulen har jag valt att dela upp
den i tre delar: Front-end, back-end samt FS som vi ser i figur 7.1.
Figur 7.1. De olika modulerna i mitt GVT-system.
7.3.1
Front-End
Front-end-modulens gränssnitt är det enda gränssnitt som användaren av SVOmodulen kommer att se. Jag har lagt stort fokus på att göra detta gränssnitt så
enkelt och intuitivt som möjligt för att förenkla för användaren. Front-end-modulen
tar sedan dessa enkla funktionsanrop som användaren ger och översätter dem till
kommandon för back-end modulen som i sin tur utför jobbet. Nedan listar jag de
viktigaste funktionerna i front-end gränssnittet:
SetVoxel()
Tar en position samt ett antal voxelparametrar som argument. Funktionen
traverserar sedan ner GVT-trädet tills den kommer till den position användaren har angivit och ändrar sedan voxelparametrarna för den aktuella voxeln
som jag beskriver i kapitel 6.1.5. Om en nod i traverseringen inte redan finns
kommer back-end modulen lägga till en nod. Detta gör att front-end modulen
aldrig behöver bry sig om att allokeringen av nya noder.
Det primära användningsområdet för denna funktion är vid voxalisering av
vektorgrafik till voxelgrafik.
GetVoxel()
Tar en position som argument. Funktionen traverserar sedan ned genom GVTträdet tills vi stöter på ett löv och returnerar i så fall detta. Om vi inte finner
något löv returnerar funktionen NULL.
48
7.3. IMPLEMENTERINGSSTRUKTUR
RayTrace()
Tar en start punkt samt en riktning som argument. Funktionen utför sedan en
ray trace genom GVT-modellen och returnerar den första voxeln som strålen
träffar eller NULL om den inte träffade någon voxel. Denna funktionen kan
även ta extra argument om kameran (upplösningen och linstyp) som sedan
kan användas för att stoppa traverseringen när en nod i GVT-modellen får
mindre projektionsytor på skärmen än en pixel (kapitel 4.8).
Det primära användningsområdet för denna funktion är rendering av GVTmodeller.
Do()
Sparar nuvarande GVT-modellen så att man kan återvända hit med funktionen Undo(). Enklaste sättet av implementera denna funktion skulle vara att
ta en kopia av hela GVT-modellen något som i praktiken är helt omöjligt för
stora GVT-modeller. I stället använder jag metoden jag beskriver i kapitel
6.5.
Undo()
Återvänder till det läge GVT-modellen befann sig i när vi körde Do(). Notera
att denna funktion kan köras flera gånger och vi kommer då hoppa tillbaka
ett Do() läge per gång.
Touch()
Användaren anger en callback funktion som sedan kommer köras rekursivt på
hela GVT-modellen så länge callbacken returnerar sant (kapitel 6.1). Callbackfunktionen får som argument den aktuella nodens max- och min-värden samt
nuvarande voxelparametrar.
Denna funktion kan användas vid mipmapping men även för andra saker som
t.ex att förkalkylera ljuset för en GVT-modell.
7.3.2
Back-End
Back-end modulens primära uppgift är att hantera nod- och page-datastrukturerna.
Ingen annan del av koden känner till hur dessa datastrukturer är implementerad.
Detta gör att vi kan testa flera olika nod-datastrukturer genom att enbart skriva
om back-end modulen vilket besparar oss tid jämfört med att behöva skriva om hela
SVO-modulen.
En annan fördel med att back-end och front-end modulerna inte delar nod/page
datastrukturerna är att vi kan ha flera back-end moduler som är optimerade för olika
ändamål. Vi kan till exempel ha en back-end för att läsa och skriva till modellen
medan en annan back-end enbart är till för att läsa. Detta ger möjligheten för backend modulen att implementeras på de mest optimala sätt vilket kan öka prestanda.
49
KAPITEL 7. IMPLEMENTATION
Back-end modulens viktigaste uppgifter är allokera och frigöra noder, paging
samt komprimerision till hårddisk. Back-end modulen har även en låsmekanism
som gör att man kan använda SMP i front-end modulen.
Gränssnittet för back-end modulen (kommunikationen mellan front-end och
back-end) tar stor användning av dataåtkomstlägena jag definierade i kapitel 4.12.
Back-end modulen använder sedan dessa dataåtkomstlägen för att veta hur den ska
behandla nodförfrågan samt att sätta upp korrekta lås för SMP. Nedan listar jag
de viktigaste funktionerna i back-end gränssnittet:
GetRoot() GetChild()
Används för att rekursivt traversera GVT-trädet för läsoperationer som till
exempel rendering. GetChild funktionen tar som argument nuvarande nod
samt en siffra c mellan 0 och 7 som säger vilket barn vi vill att funktionen ska
returnera. Om barnet inte finns returneras NULL.
TouchRoot() TouchChild()
Samma argument som för GetRoot() och GetChild() men back-end modulen
vet nu att front-end modulen tänker ändra på shadingdata i noden. Precis
som för GetChild() returnerar vi NULL om noden inte finns. Denna funktion
används primärt av front-end funktionen Touch().
ForgeRoot() ForgeNodeChild() ForgeLeafChild() ForgeEmptyChild()
Samma argument som tidigare funktioner. Dessa funktioner kommer dock
alltid att returnera en nod, om noden inte redan finns skapas en ny. ForgeEmptyChild() är ett undantag till detta då denna funktion friar alla potentiella
barn under c och har därför ingen returvariabel.
Dessa funktioner är den enda funktionalitet back-end erbjuder för att skapa
och ta bort noder. Det är sedan front-end-modulens uppgift att använda dessa
funktioner på ett kreativitet sätt för att tillhandahålla de funktioner som vi
exporterar ut till front-end gränssnittet.
GetMins() GetMaxs()
Tar inga argument. Returnerar min- respektive max-värdena för GVT-modellen.
Front-end modulen tar kopior på dessa värden när den ska börja en rekursion
och uppdaterar sedan värdena under rekursionen så att vi alltid vet den aktuella nodens position (kapitel 6.1.2).
7.3.3
FS (File System)
FS modulen är en modul som gör att vi lagra flera virtuella filer i en vanlig fil. Detta
är praktiskt då jag har valt att ha en fil per page vilket skapar tusentals filer för
medelstora modeller och hundratusentals filer för stora modeller. Att användaren
ska ha hundratusentals filer på hårdisken för att spara en GVT-modell bör självklart
50
7.4. SKAL
undvikas, vilket har lätt mig till att implementera denna modul som undkommer
problemet.
Gränssnittet för FS modulen liknar det som ett operativsystem tillhandahåller
men med skillnaden att alla virtuella filer i FS modulen enbart adresseras med en
siffra i stället för en textsträng.
De interna detaljerna för hur FS modulen fungerar är utanför fokuset för denna
rapport.
7.4
Skal
För att kunna testa mitt programmeringsbibliotek har jag bäddad in det i ett skal
(shell). Skalet består av några enkla kommandon inspirerade från [Cortes 2006] för
att operera på GVT-modellen. Nedan listar jag de viktigaste:
origin <x> <y> <z>
Sätter startpunken för framtida editeringar.
spm <num>
Står för Subdivisions Per Meter och anger precision för framtida editeringar.
vox mesh <path>
Voxaliserar en triangelsoppa från en fil.
vox sphere <radius>
Voxaliserar en sfär med en viss radie.
do <text>
Sparar nuvarande utseende för modellen så vi senare kan återvända hit.
undo
Återställer modellen till det utseende den hade vid senaste do-kommandot.
render <camera>
Renderar en bild av GVT-modellen från en specifik kameravinkel. Bilden kan
sedan antingen visas i ett fönster direkt på skärmen eller skrivas till disk.
display [-double]
Renderar en bild av GVT-modellen från standard kameravinkeln och visar
den i ett fönster. -double parametern anger att vi ska dubbla storleken på
fönstret.
51
Kapitel 8
Resultat
I detta kapitel visar jag resultatet av arbetet. Jag går även ytterligare en gång igenom
de mål jag satte upp i kapitel 2.2 för att se hur bra jag har uppfyllt dem.
8.1
8.1.1
Prototypen
Syfte
I och med att jag har lyckats bygga en fungerande prototyp har jag visat att de
ideer som diskuteras i rapporten är genomförbara i praktiken.
8.1.2
Implementering
Gränssnittet till prototypen för detta arbete är inspirerat av [Cortes 2006] och
[Driemeyer 2005].
Min prototyp ligger totalt på ca 13500 rader kod (tabell 8.1) varav 7500 ligger i
SVO-modulen (Back-end, front-end samt FS). För att minimera buggar har jag även
skrivit ett testprogram som kan verifiera korrekt beteende. Det har dock inte varit
möjligt att implementera testprogrammet tillräckligt utförligt för att helt kunna
garantera buggfri kod inom ramen för detta arbete.
Tabell 8.1. Storleken av de olika modulerna i prototypen räknat till antal rader kod.
Modul
Shell
Front-end
Back-end
FS
Total
Rader kod
6000
2500
4000
1000
13500
53
KAPITEL 8. RESULTAT
8.1.3
Demonstration
Nedan är ett exempel från en körning av min prototyp. Rader som börjar med xade$
är kommandon från användaren. Notera att de specifika tidsangivelserna nedan inte
är relevanta.
xade$ spm 6
xade$ scale 0.01
xade$ bind texture "data/mesh/house.tga"
xade$ do "house"
xade$ vox mesh data/mesh/house.obj
...loaded ’house.obj’ 3225 verts, 1075 faces
Voxelized ’house.obj’ in 1.563 seconds
xade$ display
Rendererd 512x512 pixels in 0.304 seconds
Resultatet av ovanstående körning visar jag i figur 8.1.
Figur 8.1. En rendering av en GVT-modell från min prototyp.
Låt säga att användaren vill lägga till ytterligare en modell med några tunnor
bredvid huset, han fortsätter då på följande sätt:
xade$ bind texture "data/mesh/barrel.tga"
xade$ do "barrel"
xade$ vox mesh data/mesh/barrel.obj
54
8.1. PROTOTYPEN
...loaded ’barrel.obj’ 195 verts, 61 faces
Voxelized ’barrel.obj’ in 0.092 seconds
xade$ display
Rendererd 512x512 pixels in 0.297 seconds
Figur 8.2. Ytterligare en rendering men med några felplacerade tunnor.
Som vi ser i figur 8.2 är tunnorna lite felplacerade. Användaren kan åtgärda
problemet med undo- följt av origin-kommandot.
xade$ undo
Undo barrel
xade$ origin 0 0 1
xade$ vox mesh data/mesh/barrel.obj
...loaded ’barrel.obj’ 195 verts, 61 faces
Voxelized ’barrel.obj’ in 0.078 seconds
xade$ display
Rendererd 512x512 pixels in 0.299 seconds
Resultatet ser vi i figur 8.3. En viktig poäng att notera med ovanstående körning
är att undo-kommandot alltid går i real-tid (och även O(1)).
55
KAPITEL 8. RESULTAT
Figur 8.3. Slutliga renderingen av modellen med tunnorna rätt placerade.
8.2
Datastrukturer
En av de viktigaste delarna under arbetet har varit att fastställa de datastrukturer
som ska användas. Med bra datastrukturer blir algoritmerna mer eller mindre triviala att skriva, och med triviala algoritmer minimerar man risken för buggar:
”If you have chosen the right data structures and organized things well,
the algorithms will almost always be self-evident. Data structures, not
algorithms, are central to programming” –Rob Pike
Nedan presenterar jag de viktigaste datastrukturerna för systemet.
8.2.1
Nod
enum channel {
M_NORMAL_X,
M_NORMAL_Y,
M_NORMAL_Z,
M_DIFFUSE_R,
M_DIFFUSE_G,
M_DIFFUSE_B,
M_SPECULAR,
M_SHININESS,
56
8.2. DATASTRUKTURER
MAX_CHANNELS
};
struct node {
struct node
char
byte
byte
};
*next;
// next in hash tabel
x, y, z;
childbits;
channels[MAX_CHANNELS];
struct node g_nodes[MAX_NODES];
Min noddatastruktur tar 8 + hashtabell ≈ 12 bytes per nod för topologidata
och ytterliggare 8 bytes för shadingdata. Detta gör att den slutaktiga nodstorleken
landar på ca 20 bytes, vilket jag anser vara rätt bra. För dessa 20 bytes får vi noder
som enkelt kan flytta in och ut på disk samt bra prestanda (dock inte lika bra
som för ett pekarträd). För en förklaring av upplägget bakom denna datastruktur
hänvisar jag till kapitel 5.
8.2.2
Page
struct page {
int
int
int
key;
time;
refpush;
// this page key on disk
// next mapped/unmapped page in linked list
struct page
*next;
// 8 page child keys
int
childs[8];
// hashtabel for nodes in this page
struct node
*tabel[MAX_HASH];
};
struct page g_pages[MAX_PAGES];
struct node *g_mappedPages[MAX_HASH];
struct node *g_unmappedPages;
// mapped pages, hashed on key
// linked list of unused pages
Storleken på en page är försumbar (utom node hashtabellen som vi redan har
räknat in i nodstorleken) jämfört med storleken på noderna. För en förklaring av
upplägget bakom denna datastruktur hänvisar jag till kapitel 6.3.4.
57
KAPITEL 8. RESULTAT
8.2.3
Layers och History
struct layer {
vec3
vec3
int
byte
};
mins;
maxs;
key;
x, y, z;
struct history {
boolean
struct layer
} history;
used;
layers[MAX_LAYERS];
struct history g_history[MAX_HISTORY];
struct int g_current;
Detta ingår i metadata för varje GVT-modell. Minnesutrymme och prestanda
är inte relevant för dessa datastrukturer. För en förklaring av upplägget av datastrukturerna hänvisar jag till kapitel 5.5 och 6.5.
8.3
8.3.1
Uppfyllande av målen
Mål 1: Editering
Uppfyllt. Det finns inga begränsningar i systemet mellan rendering och generering.
Det enda jag behövde kompromissa på, för att få detta mål uppfyllt, var att komprimering av pages inte får använda page-föräldern som jag skriver om i kapitel
6.6.2.
8.3.2
Mål 2: Ingen rumsgräns
Uppfyllt. Eftersom vi undersöker om en editering av modellen sträcker sig utanför
rotnoden och i sådana fall uppdaterar rotnoden med en större nod kan modellen
växa vart eftersom användaren arbetar på den. Användaren behöver heller aldrig
bestämma i förväg hur stor modellen slutligen kommer bli. Det är dock möjligt att
min metod inte generar ett optimalat GVT-träd, läs mer i kapitel 9.2.1.
8.3.3
Mål 3: Multipla undo-redo samt layers
Uppfyllt. Systemet klarar teoretiskt sett oändligt med Undo. Även om vi bara duplicerar den del av trädet där användaren har ändrat modellen mellan två Undo steg
kan detta snabbt ta upp mycket plats vilket i praktiken dock begränsar antalet
Undo. Jag har satt en nuvarande gräns på 32 Undo.
58
8.3. UPPFYLLANDE AV MÅLEN
Layers visade sig vara lätt att implementera eftersom de behandlas som separata
GVT-modeller som enbart råkar ligga i samma fil.
8.3.4
Mål 4: Modeller större än minnesutrymme
Uppfyllt. Systemet använder horisontell paging och kan därmed ladda in och ut
pages från disk medan programmet körs. Detta visade sig vara det svåraste målet
att implementera i praktiken.
8.3.5
Mål 5: CUDA Export
Uppfyllt. Vi kan exportera hela modellen eller delar av modellen för CUDA rendering. Som datastruktur för CUDA rendering använder jag vertikal paging med
trädkod-offset-lösningen vilket [Laine 2010] har visat vara en mycket effektiv datastruktur.
Jag har själv implementerat en CUDA-renderare under arbetet även om denna
ligger utanför fokuset för rapporten.
8.3.6
Mål 6: Minimera hårdiskutrymme
Uppfyllt. Bästa sättet att mäta hur mycket man lyckats komprimera en GVT-modell
är att mäta snittvärdet för antalet bytes per nod. I tabell 8.2 kan vi se de olika stegen
i komprimering och hur många bytes per nod de generar för en viss GVT-modell.
Tabell 8.2. Visar hur effektiva de olika komprimeringsstegen är.
Ingen komprimering
Ingen next-pekare
Delta + RLE + Huffman
Delta + RLE + Huffman + Hilbert kurva
Delta + RLE + Huffman (per kanal) + Hilbert kurva
Bytes
15
11
5.48
3.83
3.46
Procent
100%
73%
37%
26%
23%
Ingen komprimering är vad XYZ-hashtabell datastrukturen tar per nod förutsatt att varje nod har 7 kanaler (3 för normal, 3 för färg och 1 för specular). Ingen
next-pekare är samma som ovanstående men där vi tar bort next-pekaren för varje
nod vilket vi inte behöver spara på disk. Delta + RLE + Huffman betyder deltakomprimering, Run-Length kodning samt entropikodning med Huffman-algoritmen.
Hilbert kurva betyder att vi sorterar noderna i en ordning som gör att deltakomprimeringen i sin tur kan göra ett bättre jobb. Huffman (per kanal) betyder att vi använder olika Huffman träd för de olika kanalerna vilket tillåter Huffman-algoritmen
att göra ett bättre jobb.
För en viss testmodell i mitt system tar alltså varje nod i snitt 3.46 bytes på
disken där varje nod innehåller normal, färg samt ett specular-värde.
59
KAPITEL 8. RESULTAT
8.3.7
Mål 7: Prestanda
Prestanda kan mätas på flera olika sätt. Nedan listar jag några av dem med en
kommentar om hur bra jag lyckats uppfylla dem. Eftersom jag inte har något att
jämföra med är det svårt att säga om detta mål är uppfyllt eller ej.
1. Läshastighet
Mest avgörande för prestanda är valet av datastruktur för noderna. Även om
min XYZ-hashtabell lösning inte är lika snabb som pekarträd-lösningen är
den ändå nära. Skillnaden mellan de två datastrukturerna varierar mycket
från modell till modell (beror på modellstorlek, träddjup, stoppdjup m.m.)
men vanligtvis ligger det mellan ca 60% och 80% av pekarträd-lösningen
(pekarträd-lösningen klarar dock inte paging och undo-redo).
Läshastigheten skalar näst intill linjärt med antalet processorkärnor så länge
majoriteten av modellen ligger i minne.
2. Mipmap-hastighet
Här visar valet av datastruktur mindre skillnad än vid läsning. Detta beror
sannolikt på att mipmapping kräver betydligt fler klockcyklar till stor del
för att det ligger en normalisering per nod. En normalisering kräver de två
kostsamma operationerna FDIV och FSQRT på en x86-processor [Intel 1].
Det mest avgörandet för prestandan hos mipmapping är hur ofta vi måste gå
ut på disk för att hämta noddata. Här har XYZ-hashtabell-lösningen en fördel
framför ett pekarträd då den tar mindre plats i minnet, vilket tillåter oss att
hålla fler noder i minne.
Även mipmapping skalar näst intill linjärt med antalet processorkärnor.
3. Skrivhastighet
Den helt avgörande delen för prestanda här är algoritmen för voxalisering. ”3D
scan conversion”-metoden visar sig vara ca 10 gånger så snabb på modeller
med många trianglar som ”Divide and Conquer”-metoden även fast jag har
lagt ner mycket tid på att optimera ”Dividie and Conquer”.
På denna punkt finns det dock mycket mer jobb som går att göras. Min
implementering av ”3D scan conversion” kan antagligen optimeras ytterligare
och implementeringen skalar inte alls med antalet processorkärnor.
4. Paginghastighet
Prestanda här beror primärt på hårdvara och är därför inte lika viktigt att
optimera i mjukvara. I nuvarande implementering väntar dock hela systemet
så fort vi vill läsa eller skriva en page från disk vilket självklart inte är den
bästa lösningen.
Jag är nöjd med prestandan hos min prototyp även om det antagligen går att
öka den ytterligare, framför allt för skrivning och paging.
60
Kapitel 9
Diskussion och slutsats
I denna kapitel diskuterar jag arbetet samt presenterar några poteniella framtidsförbättringar. Slutligen presenterar jag min slutsats.
9.1
9.1.1
Diskussion
Jämförelse med triangelgrafik
Precis som [Olick 2008] och [Laine 2010] har även jag kommit fram till att så
länge du vill ha unik-texturering innebär inte GVT-teknik någon större förlust i
diskutrymme jämfört med triangelgrafik. GVT-modeller ger också många fördelar
framför triangelgrafik, som till exempel möjligheten att ha mycket fler geometriska
detaljer samt automatiserad LOD.
Tekniken har också vissa begränsningar, där den största verkar vara animering.
För att komma runt detta problem går det att blanda triangelgrafik och voxelgrafik,
detta skulle kunna ge det bästa av två världar. Detta kan göras på två olika sätt:
Det första är en renderare som blandar ray tracing och triangelrasterisering.
Nackdelen med denna metod är dock att ljussättning och skuggor då kan skilja sig
mellan statisk och dynamisk geometri [Carmack 2008].
Det andra är att voxalisera all dynamisk geometri i real-tid för att sedan utföra
ray tracing mot dem. Detta skulle garantera att ljussättningen and skuggor förblir
korrekt men skulle antagligen bli långsammare än första metoden.
9.1.2
Kommersiell framtid för GVT
Jag tror GVT-teknik kan ha en ljus framtid. Tekniken lämpar sig bäst för stora
statiska modeller som landskap och städer. [Laine 2010] gör en uppskattning att
det kommer ta ca 7 år innan vi potentiellt kan se de första applikationerna börja
använda GVT-teknik i kommersiellt syfte.
Anledningen att [Laine 2010] säger just 7 år beror på att det är så lång tid
man uppskattar för Moores lag att göra ray tracing en magnitud snabbare samt att
61
KAPITEL 9. DISKUSSION OCH SLUTSATS
efterföljaren till Blue-Ray gjort det möjligt att distrubuera GVT-modeller i 100 GB
klassen.
9.1.3
Min vision
Om GVT-teknik slår igenom kommer det finnas ett behov av applikationer för att
skapa och editera dem. Min vision för framtiden är att kunna skapa en klient-server
applikation för att lösa detta problem. Modellen skulle då alltid ligga på servern, och
flera användare kan sedan logga in samtidigt från olika klienter och kollaborativt
skapa jättelika modeller som till exempel en stad.
9.2
9.2.1
Framtidsförbättringar
Optimerade GVT modeller
[Olick 2008] har en lång lista på saker som kan effektivisera en GVT-modell. En
av de viktigaste delarna är att se till att GVT-modellen har så få nivåer (höjd på
trädet) som möjligt eftersom detta minskar söktiden från roten till löven.
I min prototyp utökar vi rotnoden vart eftersom vi ser att det behövs vilket
i praktiken ofta skapar en eller två onödiga nivåer. Enklaste lösningen på detta
problem skulle vara att tillhandahålla ett kommando som användaren kan köra
vilket möblerar om GVT-modellen i syfte att minska antalet nivåer.
9.2.2
Automatisk mipmapping
I min nuvarande implementering måste mipmapping köras som ett eget steg efter
vi har skrivit noddata. För stora modeller är detta en stor nackdel för prestandan
då noder kan behövas skrivas in och ut på disk en extra gång.
En bättre lösning vore att på något sätt utföra mipmappingen direkt i backend-modulen när man skriver till noderna. Jag vet inte om detta är möjligt, men
om det är det skulle vi antagligen kunna öka prestandan för skrivandet av noder en
hel del.
9.2.3
Nätverkseditering
Ett av de bästa användningsområdena för GVT modeller är stora modeller som
landskap och städer. När man vill bygga så pass stora modeller är det fördelaktigt
att flera användare kollaborativt kan editera modellen över nätverk. Tanken är att
GVT-modellen ligger på en server där flera klienter kan logga in samtidigt och
editera modellen.
9.2.4
GVT-animering
En av de största svagheterna med GVT-teknik är att vi inte vet hur man animerar
sådan modeller. Det har gjorts väldigt lite forskning på området så det är fullt
62
9.3. SLUTSATS
möjligt att det går att animera GVT-modeller effektivt.
9.2.5
Användargränssnitt
Utveckla ett användargränssnitt så att användare med begränsad teknisk kompetens kan använda systemet. Till detta tillkommer även att undersöka vilket som
är det mest effektiva sättet att bygga en GVT-modell från ett användarperspektiv. I min nuvarande prototyp voxaliserar jag triangelmodeller till voxelmodeller
vilket knappast är bästa lösningen då slutresultatet både får begränsningar från
triangelgrafiken samt voxelgrafiken.
9.2.6
Progressiv editering
Ett problem i nuvarande implementering är att när man gör stora editeringar av
modeller måste användaren vänta tills förändringen är helt klar innan han kan se
resultatet. En bättre lösning vore att ha en kö med alla förändringar som användaren vill göra och först bara genomföra förändringen i lågupplösning. Systemet kan
sedan fortsätta förfina förändringen som en bakgrundsprocess tills man uppnått full
upplösning.
9.2.7
CSG modellering
[Foley 1996, p. 552] visar att det är lätt att utföra booliska operationer mellan två
eller flera GVT-modeller. Detta kan utnyttjas för editering genom att låta användaren utföra olika CSG-operationer på modellerna.
En annan användning för booliska operationer mellan GVT-modeller är att använda dem mellan olika layers. På detta sätt kan man skapa layers som ”subtraherar
bort” geometri från en annan modell.
9.2.8
Pekarträd med ”struct-hack”
I slutet av arbetet kom jag på ett nytt sätt att spara pekarträd som jag tyvärr
inte hade tid att testa: Om vi bara sparar de pekare vi behöver (de som faktiskt
pekar på något) för varje nod skulle vi kunna spara enorma mängder minne. För
att avgöra vilka pekare vi sparar och i vilken ordning de kommer kan vi använda
childbit-variablen på liknande sätt som i trädkod-offset-lösningen.
Detta skulle medföra att löv i trädet inte skulle få några pekare överhuvudtaget
utan att behöva det extra krånglet som leafbits medför, och då löv står för ca 70%
av all noddata (kapitel 5.3.1) skulle detta spara stora mängder minne.
9.3
Slutsats
Jag har lyckats implementera en prototyp som uppfyller alla mål (utom möjligen
prestandamålet) som jag satte upp i kapitel 2.2. Denna prototyp demonstrerar att
63
KAPITEL 9. DISKUSSION OCH SLUTSATS
det är praktiskt möjligt att implementera de viktigaste aspekterna som undo-redo
och paging för att kunna bygga ett ”Photoshop för GVT”. En sådan applikation
har självklart många andra problem som måste lösas, inte minst rörande användarvänlighet, men detta arbete kan då användas som teknisk bas att bygga vidare
på.
64
Referenser
[Akenine 2002] Tomas Akenine-Möller. Real-Time Rendering. 2nd Edition, 2002,
ISBN 1-56881-182-9.
[Amanatides 1987] John Amanatides, Andrew Woo. A Fast Voxel Traversal Algorithm for Ray Tracing. Eurographics proceedings ’87, G. Marechal Ed. Elsevier
North-Holland, New York. 1987.
[Carmack 2007a] John Carmack. World Wide Developer Conference. id Software
2007.
[Carmack 2007b] John Carmack. id Tech 5 Developer Walktrough. (finns på google
video), id Software 2007.
[Carmack 2008] John Carmack. Interview with CD-Action. (finns på youtube.com),
2008.
[Cortes 2006] Rudy Cortes. The RenderMan Shading Language Guide. Thomas
Course Technology, 2006. ISBN 1-59863-286-8.
[Crassin 2009] Cyril Crassin, Fabrice Neyret, Sylvain Lefebvre, Elmar Eisemann.
GigaVoxels: Ray-Guided Streaming for Efficient and Detailed Voxel Rendering.
I3D, 2009.
[Dewey 1876] Melvil Dewey. A Classification and Subject Index for Cataloguing and
Arranging the Books and Pamphlets of a Library.. Amherst, Mass. 1876.
[Driemeyer 2005] Thomas Driemeyer. Rendering with Mental Ray. SpringerWienNewYork, 3nd Edition, 2005, ISBN 3-211-22875-6.
[Engel 2006] Klaus Engel, Markus Hadwiger, Joe M. Kniss, Christof Rezk-Salaman
Daniel Weiskopf. Real-Time Volume Graphics. A K Peters, 2006, ISBN 1-56881266-3.
[Ericson 2005] Christer Ericson. Realtime Collision Detection. Morgan Kaufmann,
2005, ISBN 1-55860-732-2.
[Foley 1996] James D. Foley, Andries van Dam, Steven K. Feiner, John F. Hughes.
Computer Graphics principles and practice. Addison-Wesley Professional, 2nd
Edition, 1996, ISBN 0-201-84840-6.
65
REFERENSER
[Herlihy 2008] Maurice Herlihy, Nir Shavit. The Art of Multiprocessor Porgramming. Morgan Kaufmann, 2008. ISBN 0-123-70591-6.
[Hilbert 1891] David Hilbert. Ueber stetige abbildung einer linie auf flächenstück.
Mathematische Annalen 38:459-460. 1891.
[Houng 1998] Jian Huang, Roni Yagel, Vassily Filippov, Yair Kurzion. An Accurate Method for Voxelizing Polygon Meshes. ACM 1998 Symposium on Volume
Visualization, 119–126, 1998.
[Huffman 1952] David A. Huffman. A Method for the Construction of MinimumRedundancy Codes. Proceedings of the I.R.E, September 1952.
[Intel 1] IA-32 Intel Architecture Software Developer’s Manual. 2006.
[Kaufman 1986] Arie Kaufman, Eyal Shimony. 3D Scan-Conversion Algorithms for
Voxel-Based Graphics. ACM Workshop in Interactive 3D Graphics, Chapel Hill,
NC, October 1986, 46-75. 1986.
[Knoll 2006] Aaron Knoll, Ingo Wald, Steven Parker, Charles Hansen. Interactive
Isosurface Ray Tracing of Large Octree Volumes. IEEE Symposium on Interactive Ray Tracing, 2006.
[Knoll 2009] Aaron Knoll, Charles Hansen, Ingo Wald. Coherent Multiresolution
Isosurface Ray Tracing. Technical Report UUSCI-2007-001, SCI Institute,
2009.
[Knuth 1975] Donal E. Knuth. The Art of Computer Porgramming, vol. 1, Fundamental Algorithms. Addison-Wesley Professional, Second Edition, 1975. ISBN
0-201-89683-4.
[Krishnamachari 2009] Parashar Krishnamachari. http://chiranjivi.tripod.com/octrav.html
[Laine 2010] Samuli Laine, Tero Karras. Efficient Sparse Voxel Octrees – Analysis,
Extensions and Implementation. NVIDIA Research, I3D, 2010.
[Levitin 2003] Anany Levitin. Introduction to The Design & Analyis of Algorithms.
Addison Wesley, 2003, ISBN 0-201-74395-7.
[Nguyen 2008] Hubert Nguyen. GPU Gems 3. Addison-Wesley Professional, 2008,
ISBN 0-321-51526-9.
[Oliver 1983] M.A. Oliver, N.E. Wiseman. Operations on quadtree-encoded images.
Computer Journal 26, 1, 1983.
[Olick 2008] Jon Olick. Current Generation Parallelism In Games. SIGGRAPH,
2008.
[Raytracey] Ray Tracey’s Blog. http://raytracey.blogspot.com/
66
REFERENSER
[Samet 1987] Hanan Samet, Robert E. Webber. Hierarchucal Data Structures and
Algorithms for Computer Graphics. IEEE Computer Graphics and Applications, Vol 8, No 4, July 1988, s. 59-75, 1987.
[Samet 1989] Hanan Samet. Implementing Ray Tracing with Octrees and Neighbor
Finding. Computers & Graphics, 13(4): 445–460, 1989.
[Samet 2006] Hanan Samet. Foundations of Multidimensional and Metric Data
Structures. Morgan Kaufmann, 2006, ISBN 0-12-369-446-9.
[Sayood 2006] Khalid Sayhood. Introduction to Data Compression. Morgan Kaufmann, Third Edition, 2006, ISBN 0-12-620862-X.
[Suffern 2007] Kevin Suffern. Ray Tracing from the Ground Up. A K Peters, 2007,
ISBN 978-1-56881-272-4.
[Tegen 2009] Henning Tegen. Xade: individuellt projektarbete i datalogi. 2009.
[Yoon 2008] Sung-eui Yoon, Enrico Gobbetti, David Kasik, Dinesh Manocha.
Real-Time Massive Model Rendering. Morgan & Claypool 2008. ISBN
9781598297928.
[Zesheng 1991] Tang Zesheng. Octree Representation and Its Applications in CAD.
1991.
67
TRITA-CSC-E 2010:054
ISRN-KTH/CSC/E--10/054--SE
ISSN-1653-5715
www.kth.se