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