Programmering av 3D-grafik för datorer med låg prestanda. Peter Halin



Relevanta dokument
Kvalificeringstävling den 30 september 2008

Algoritmer, datastrukturer och komplexitet

Algoritmer, datastrukturer och komplexitet

Tema: Pythagoras sats. Linnéa Utterström & Malin Öberg

Konvexa höljet Laboration 6 GruDat, DD1344

Föreläsning 8: Aritmetik och stora heltal

Rumsuppfattning är förmågan att behandla sinnesintryck av former

9-1 Koordinatsystem och funktioner. Namn:

Sammanfattningar Matematikboken X

Föreläsning 11: Beräkningsgeometri

Att förstå bråk och decimaltal

Institutionen för matematik och datavetenskap Karlstads universitet. GeoGebra. ett digitalt verktyg för framtidens matematikundervisning

Matematiska uppgifter

Grafisk Teknik. Rastrering. Övningar med lösningar/svar. Sasan Gooran (HT 2013)

Ordlista 5A:1. term. faktor. täljare. nämnare. Dessa ord ska du träna. Öva orden

Datorsystemteknik DVG A03 Föreläsning 3

Lokala mål i matematik

Programmering II (ID1019) :00-11:00

TANA17 Matematiska beräkningar med Matlab

TAIU07 Matematiska beräkningar med Matlab

Beräkningsvetenskap föreläsning 2

Extramaterial till Matematik Y

Sidor i boken 8-9, 90-93

varandra. Vi börjar med att behandla en linjes ekvation med hjälp av figur 7 och dess bildtext.

Användarhandledning Version 1.2

Sammanfattningar Matematikboken Y

Komposanter, koordinater och vektorlängd Ja, den här teorin gick vi igenom igår. Istället koncentrerar vi oss på träning inför KS3 och tentamen.

a), c), e) och g) är olikheter. Av dem har c) och g) sanningsvärdet 1.

Föreläsning 9: Talteori

5 Linjär algebra. 5.1 Addition av matriser 5 LINJÄR ALGEBRA

Matematik klass 4. Vårterminen. Namn: Anneli Weiland Matematik åk 4 VT 1

Precis som var fallet med förra artikeln, Geogebra för de yngre i Nämnaren

Robotarm och algebra

Talsystem Teori. Vad är talsystem? Av Johan Johansson

Översikt. Stegvis förfining. Stegvis förfining. Dekomposition. Algoritmer. Metod för att skapa ett program från ett analyserat problem

1 Den Speciella Relativitetsteorin

Känguru 2012 Junior sivu 1 / 8 (gymnasiet åk 1) i samarbete med Jan-Anders Salenius vid Brändö gymnasiet

Grafik. För enklare datorsystem

Block 1 - Mängder och tal

MATEMATIK GU. LLMA60 MATEMATIK FÖR LÄRARE, GYMNASIET Analys, ht Block 5, översikt

Grafik. För enklare datorsystem

Lösning till fråga 5 kappa-06

Alla datorprogram har en sak gemensam; alla processerar indata för att producera något slags resultat, utdata.

Programmering i C++ En manual för kursen Datavetenskaplig introduktionskurs 5p

Sphero SPRK+ Appen som används är Sphero Edu. När appen öppnas kommer man till denna bild.

Optimering av depåpositioner för den minimala bensinförbrukningen i öknen

GeoGebra i matematikundervisningen - Inspirationsdagar för gymnasielärare. Karlstads universitet april. Liten introduktionsguide för nybörjare

Föreläsning 3.1: Datastrukturer, en översikt

Taluppfattning och problemlösning

Complex numbers. William Sandqvist

Matematik EXTRAUPPGIFTER FÖR SKOLÅR 7-9

1 Josefs bil har gått kilometer. Hur långt har den gått när han har kört (3) tio kilometer till? km

Grunderna i stegkodsprogrammering

Matematiska uppgifter

Optimala vinkeln av bortklippt cirkelsektor fo r maximal volym pa glasstrut

Exempel. Vi skall bestämma koordinaterna för de punkter som finns i bild 3. OBS! Varje ruta motsvarar 1mm

Dra streck. Vilka är talen? Dra pil till tallinjen. Skriv på vanligt sätt. Sätt ut <, > eller =

IE1205 Digital Design: F6 : Digital aritmetik 2

Matematik 3 Digitala övningar med TI-82 Stats, TI-84 Plus och TI-Nspire CAS

Matematik klass 4. Vårterminen FACIT. Namn:

Läxa 9 7 b) Dividera 84 cm med π för att få reda på hur lång diametern är. 8 1 mm motsvarar 150 / 30 mil = = 5 mil. Omvandla till millimeter.

Dekomposition och dynamisk programmering

Kort introduktion till POV-Ray, del 1

RÖRELSE. - Mätningar och mätinstrument och hur de kan kombineras för att mäta storheter, till exempel fart, tryck och effekt.

INNEHÅLL XYZ. Hösten 2011 provpass 2 12 provpass Våren 2012 provpass 3 20 provpass Övningsprovet 28 KVA

Matematik med Scratch

Tentamen TEN1 HI

Kapitel 4. cos(64 )= s s = 9 cos(64 )= 3.9m. cos(78 )= s s = 9 cos(78 )= 1.9m. a) tan(34 )= x x = 35 tan(34 )= 24cm

Lösningar till udda övningsuppgifter

Känguru 2012 Student sid 1 / 8 (gymnasiet åk 2 och 3) i samarbete med Jan-Anders Salenius vid Brändö gymnasiet

Moment 4.2.1, 4.2.2, 4.2.3, Viktiga exempel 4.1, 4.3, 4.4, 4.5, 4.6, 4.13, 4.14 Övningsuppgifter 4.1 a-h, 4.2, 4.3, 4.4, 4.5, 4.

i LabVIEW. Några programmeringstekniska grundbegrepp

Parabeln och vad man kan ha den till

Grafiska pipelinens funktion

Tentamen TNM061, 3D-grafik och animering för MT2. Onsdag 20/ kl SP71. Inga hjälpmedel

1. (a) Bestäm alla värden på c som gör att matrisen A(c) saknar invers: c 1

Aktivitetsbank. Matematikundervisning med digitala verktyg II, åk 1-3. Maria Johansson, Ulrica Dahlberg

Lennart Rolandsson, Uppsala universitet, Ulrica Dahlberg och Ola Helenius, NCM

Ekvationer och system av ekvationer

Uppsala Universitet Matematiska Institutionen Thomas Erlandsson

Komplexa tal: Begrepp och definitioner

Grafiska pipelinen. Edvin Fischer

Uppgift 1 ( Betyg 3 uppgift )

Laboration 4: Digitala bilder

Komplettering till kursboken i Numeriska beräkningar. 1 Beräkningsfelsanalys. 1.1 Uttryck med kancellation

i=1 β i a i. (Rudolf Tabbe.) i=1 b i a i n

Regression med Genetiska Algoritmer

7 Extremvärden med bivillkor, obegränsade områden

SF1624 Algebra och geometri Lösningsförslag till tentamen DEL A

Kvalificeringstävling den 26 september 2017

2-7: Bråk-förlängning Namn:.. Inledning

Bråk. Introduktion. Omvandlingar

Lösningar till utvalda uppgifter i kapitel 1

Programmera i C Varför programmera i C när det finns språk som Simula och Pascal??

Funktionsstudier med derivata

Ekvivalensrelationer

matematik Lektion Kapitel Uppgift Lösningg T.ex. print(9-2 * 2) a) b) c) d)

Språkstart Matematik Facit. Matematik för nyanlända. Jöran Petersson

Introduktion till Datalogi DD1339. Föreläsning 2 22 sept 2014

Moment 4.2.1, 4.2.2, 4.2.3, Viktiga exempel 4.4, 4.5, 4.6, 4.7, 4.13 Handräkning 4.1, 4.2, 4.3, 4.4, 4.5, 4.7 Datorräkning 1-9 i detta dokument

Transkript:

Programmering av 3D-grafik för datorer med låg prestanda Peter Halin

Abstrakt Tredimensionell grafik har blivit vardagligt på allt från mobiltelefoner till hemdatorer. Fastän en allt större del av 3D-grafiken nuförtiden ritas med hjälp av sk. acceleratorkretsar, så finns det fortfarande situationer där detta inte är möjligt, t.ex. inbyggda system (embedded systems) som endast består av en centralprocessor och en simpel grafikkrets utan 3D-egenskaper. I detta fall är oftast processorn av låg prestanda, och det behövs rutiner som är snabba och tar så lite resurser som möjligt. I uppsatsen har jag koncentrerat mig på grunderna bakom matrisrotationer, linje- och polygonrutiner och sortering av ytor, samt lämpliga algoritmer för dessa. Som exempelspråk använder jag pseudokod, för att implementering på olika plattformer skulle vara möjligt. Exemplen är skrivna så att de inte tar resolutionen av skärmen i beaktande för att få dem tydligare, fastän en riktig implementation kunde ha en fastslagen resolution för maximal hastighet. Sökord: 3D-grafik, matrisrotation, optimering 2

1. Introduktion 4 2. Grunderna för 3D-programmering 5 2.1 Fixedpoint aritmetik 5 2.2 Matrisrotationer 8 2.3 Perspektivkorrigering 10 3. Minimering av ritandet 12 3.1 Sortering 12 3.2 Icke-synliga ytor 15 4. Ritandet 18 4.1 Trådmodeller 18 4.2 Fyllda ytor 21 5. Diskussion 25 3

1. Introduktion Tredimensionell grafik börjar bli allt populärare i all sorts mjukvara, speciellt efter att speciella 3d-acceleratorer börjat bli vanligare och kan åstadkomma nästan fotorealistiska bilder och animationer utan att belasta själva huvudprocessorn. Men fastän det finns 3dkretsar för de flesta ändamålen, är det ändå nödvändigt att få fram snabb 3d-grafik utan dem. I dessa fall faller allt arbete på huvudprocessorn, och då gäller det att mjukvaran också skall vara väl programmerad och snabb. Speciellt i t.ex. nöjeselektronik och motsvarande apparater där man har en liten skärm, så vill man ofta ha snabb grafik fastän det skulle vara på bekostnad av precision. Här behandlas de huvudområden som behövs för att få tredimensionella objekt att röra sig på skärmen. Kapitlet om grunderna för 3D-programmering behandlar aritmetik med fixedpoint-värden, matrisrotationer och perspektivkorrigering. Dessa räcker redan till att uppnå simpel och rörlig 3D-grafik, som består av punkter eller andra lättritade figurer. Till nästa behandlas sortering och icke-synliga ytor, vilka är förberedande skeden när man vill rita ut 3D-grafik med fyllda ytor. Med sorteringen ser man till att ytorna ritas i rätt ordning, och ger därmed en känsla av djup, medan man genom att slopa de ickesynliga ytorna snabbar upp ritandet, vilket i sin tur är den mest tidskrävande delen. Till slut behandlas själva ritandet, och metoder för att rita linjer och fyllda ytor tas upp. Och för att dessa är de mest tidskrävande behandlas dessa också mer djupgående än de andra områdena. Emedan de metoder som behandlas inte nödvändigtvis är de absolut snabbaste, så är de mycket bra kompromisser och är lätta att implementera, oberoende av plattform eller programmeringsspråk. Alla programexempel är skrivna i pseudokod, och är lätta att skriva om för de flesta programmeringsspråken, allt från assembler till högnivåspråk. 4

2. Grunderna för 3D-programmering 2.1 Fixedpoint-aritmetik För att få uträkningar snabbare på enkla processorer gäller det att hålla sig till heltalsuträkningar, men pga. att man behöver mera precision än så, behöver man göra uträkningar med decimaltal. Lättaste sättet skulle vara att använda flyttal, men dessa är långsamma i bruk ifall man inte har tillgång till en flyttalsprocessor, och man kan inte anta att en maskin innehåller en sådan. Så man behöver göra något för att komma runt detta problem. Ett bra alternativ är att använda sig av fixedpoint-värden [1], där man har ett heltal som representerar både heltalsdelen och decimaldelen av ett decimaltal. Om man använder 32-bitars tal så kan övre 16 bitarna representera heltalsdelen och lägre 16 bitarna decimaldelen, och detta kallas för 16.16 fixedpoint-representation. Man kan också dela talet på andra sätt, som t.ex. 20.12 eller 12.20, beroende hurdan precision man behöver. Att dela ett 16-bitars tal i 8.8 format kan också komma i fråga när man inte behöver hög precision, och ifall processorn inte klarar av större tal. Uträkningarna gällande addition och subtraktion fungerar helt lika som med normala heltal, dvs. addition av två 16-bitars fixedpoint-tal ger ett 16-bitars fixedpoint-tal som resultat, som i följande exempel: 3F23h 3F.23h + 1F34h + 1F.34h = 5E57h = 5E.57h Samma gäller också vid subtraktion: 3F23h 3F.23h - 1F34h - 1F.34h = 1FEFh = 1F.EFh Man behöver också möjligheten att multiplicera och dividera vid olika uträkningar, men dessa är inte lika triviala som addition och multiplikation. 5

När man multiplicerar två faktorer med 32-bitars precision kommer produkten att vara ett 64-bitars tal, och i fallet av 16.16 fixedpoint-värden kommer produkten att ha en precision på 32.32. En 32-bitars produkt får man genom att slopa de 16 högsta och de 16 lägsta bitarna, vilket resulterar i ett 32-bitars, eller 16.16 fixedpoint tal. I detta skede måste man också kolla efter overflow, och då hantera det på lämpligt sätt. Och ifall man använder 16-bitars tal blir resultatet ett 32-bitars tal, enligt följande exempel: 0F69h 0F.69h x 1070h x 10.70h 00FD4DF0h 00FD.4DF0h -> FD.4Eh Ett annat problem som kan uppstå vid multiplikation är att alla arkitekturer inte stöder 64- bitars resultat vid multiplicering av två 32-bitars tal, och i detta fall måste man spjälka upp talet i mindre bitar. Ett behändigt sätt är att dela upp båda talen i fyra 16-bitars delar, och sedan multiplicera dem med varandra, och använda dessa produkter för att bygga ihop det slutliga talet. I exemplet används två 16-bitars tal: 69h x 70h = 2DF0h -> 002Dh 002D h 0Fh x 70h = 0690h -> 0690h + 0690 h 69h x 10h = 0690h -> 0690h + 0690 h 0Fh x 10h = 00F0h -> F000h + F000 h FD4D h I exemplet tar man och multiplicerar de olika 8-bitars värden, och använder de olika 16- bitars värden för att åstadkomma det slutliga 16-bitars talet. Första talet måste skiftas åtta bitar åt höger för att man inte vill ta de lägsta 8 bitarna i beaktande, och det sista talet skiftas åtta bitar åt vänster för att man inte heller behöver då 8 högsta bitarna. Detta leder till att alla talen ryms i 16 bitar, och man kan addera dem. Det blir ett litet avrundningsfel i decimaldelen i detta fall, pga. av att när man skiftar tal så blir det avrundade neråt. Detta kan korrigeras med att addera till 80h till tal som skiftas med 8 (och 8000h ifall man skulle hantera 32-bitars tal, vilka i sin tur skulle skiftas 16 steg). 2DF0h + 0080h = 2E70h -> 002Eh 002Eh + 0690h + 0690h + F000h = FD4Eh 6

Speciellt när man räknar med 32-bitars tal brukar dessa avrundningsfel inte vara av stort värde, men dessa kan ju bli större ifall man behöver göra flere uträkningar där man utgår från de föregående talen. Precis som i multiplikationen så kan man inte dividera två 16.16 fixedpoint värden och få ett resultat av samma precision. Det som händer då man dividerar dessa två är att man får ett heltal som resultat, och medan man inte behöver dividera två decimaltal med ett decimaltal som resultat under flesta omständigheter, så kan det ändå behövas. Lösningen till detta är att man förlänger täljaren till ett 64-bitars värde i 32.32 precision (eller 32- bitars med 16.16 precision ifall man använder sig av 16-bitars värden), och behåller nämnaren i sin ursprungliga form. Kvoten man får kommer då att vara i det önskade formatet. Man kommer också att behöva dividera två heltal med ett decimaltal som resultat, och då förlänger man täljaren till ett fixedpoint tal och dividerar. Exemplet visar division med två 16-bitars tal, och division med täljaren förlängd till 32 bitar: 3F15h ----- = 0001h (endast heltalsdelen) 23EAh 003F1500h --------- = 01C1h (med decimaldel) 23EAh Medan ett möjligt problem i multiplikationen var att produkten blir ett 64-bitars tal, medan man endast kan hantera 32-bitars värden, så är problemet i division att täljaren har 64 bitar, och man måste få utfört uträkningarna med 32-bitars register eller variabler. Men pga. att denna precision inte är nödvändig i normalt bruk så kommer metoder för att lösa detta inte tas upp. Ifall man behöver ett decimalvärde så kan man skifta nämnaren åt höger med 8 bitar, så man får ett 8.8 fixedpoint-värde, och dividera 16.16 täljaren med denna. Svaret blir då ett tal med 8.8 precision, vilket kan förlängas till 16.16. Ifall man endast är intresserad av heltalsdelen, vilket kommer att vara aktuellt i perspektivkorrigeringen, så tar man och dividerar båda 16.16 fixedpoint-talen med varandra. 7

2.2 Matrisrotationer Matrisrotationer [2] är centrala i uträkningen av tredimensionella världar. Det används för att roterar objekt runt sin mittpunkt (eller annan punkt), och för att rikta kameran i scenen. Här tas endast upp hur man roterar objektets punkter runt sin mittpunkt, men samma metoder kan också användas för rotationen av kameran. För rotation av en punkt runt de olika axlarna använder man följande matriser (där cx står för cos(x) och sx för sin(x) osv.): x-axeln 1 0 0 0 0 cx sx 0 0 -sx cx 0 0 0 0 1 y-axeln cy 0 -sy 0 0 1 0 0 sy 0 cy 0 0 0 0 1 z-axeln cz sz 0 0 -sz cz 0 0 0 0 1 0 0 0 0 1 När man implementerar denna är man inte intresserad av sista kolumnen eller sista raden, utan det är 3x3 matrisen i övre vänstra hörnet som är den intressanta. Vill man rotera en punkt runt alla axlar så roterar man punkten först runt x-axeln, sedan y-axeln och sedan ännu z-axeln. Exempelprogrammet för detta skulle se ut som följande: x_1 = orig_x; y_1 = orig_y * cos(x_rot) + orig_z * sin(x_rot); z_1 = -orig_y * sin(x_rot) + orig_z * cos(x_rot); x_2 = x_1 * cos(y_rot) z_1 * sin(y_rot); y_2 = y_1; z_2 = x_1 * sin(y_rot) + z_1 * cos(y_rot); new_x = x_2 * cos(z_rot) + y_2 * sin(z_rot); new_y = -x_2 * sin(z_rot) + y_2 * cos(z_rot); new_z = z_2; Med denna programkod har man roterat punkterna orig_x, orig_y och orig_z runt alla tre axlarna, och nya koordinaterna finns i new_x, new_y och new_z. Detta är dock inte det mest optimala sättet att rotera, för nu behövs det 12 multiplikationer för varje punkt, vilket kräver en hel del processortid ifall man roterar en stor mängd punkter. Detta kan 8

lösas med att slå ihop dessa rotationer och förenkla dem. Först slår man ihop roteringarna runt x- och y-axeln: sx = sin(x_rot); cx = cos(x_rot); sy = sin(y_rot); cy = cos(y_rot); x_2 = orig_x * cy (-orig_y * sx + orig_z * cx) * sy; y_2 = orig_y * cx + orig_z * sx; z_2 = orig_x * sy + (-orig_y * sx + orig_z * cx) * cy; Vilket i sin tur kan förenklas till: x_2 = orig_x * cy + orig_y * sx * sy - orig_z * cx * sy; y_2 = orig_y * cx + orig_z * sx; z_2 = orig_x * sy - orig_y * sx * cy + orig_z * cx * cy; Efter detta slår man samman och förenklar de sista raderna. I detta skede är det också behändigt att spara de roterade värdena i en två-dimensionell tabell, för att sedan kunna användas om igen för varje punkt man vill rotera: matrix[0][0] = cy * cz; matrix[1][0] = sx * sy * cz cx * sz; matrix[2][0] = cx * sy * cz + sx * sz; matrix[0][1] = cy * sz; matrix[1][1] = sx * sy * sz + cx * cz; matrix[2][1] = cx * sy * sz + sx * cz; matrix[0][1] = -sy; matrix[1][1] = sx * cy; matrix[2][1] = cx * cy; new_x = orig_x * matrix[0][0] + orig_y * matrix[1][0] + orig_z * matrix[2][0]; new_y = orig_x * matrix[0][1] + orig_y * matrix[1][1] + orig_z * matrix[2][1]; new_z = orig_x * matrix[0][2] + orig_y * matrix[1][2] + orig_z * matrix[2][2]; Beroende på hur hög precision man vill ha, och också beroende på vilket språk man skriver i, så kan man först räkna alla värden i rotationstabellen som flyttal och sedan räkna om dem till fixedpoint decimaltal. Detta görs endast en gång per beräknad bild, så hastigheten drabbas mycket minimalt av detta. Ifall minnet tillåter kan man räkna ut en färdig sinustabell i fixedpoint format, och använda detta för att räkna ut rotationstabellen. Denna metod är att föredra ifall man programmerar i assembler. 9

2.3 Perspektivkorrigering Fastän alla uträkningar sker i tre dimensioner så har en skärm endast två, och detta leder till att man måste korrigera de tredimensionella värdena till två dimensioner. Lättaste och snabbaste sättet är parallellprojektion [3], som går ut på att slopa z-värdet, och endast multiplicera x- och y-värden med en lämplig konstant och därmed använda heltalsdelen för ritandet. Emedan detta är ett snabbt sätt så ser det man ritar förvrängt ut, pga. att man inte får något djup i bilden. Utan perspektiv Med perspektiv För att representera en tredimensionell värld måste man perspektivkorrigera [4] punkterna, dvs. ta z-värdet i beaktande på det sätt att objekt som är längre bort från tittaren ser mindre ut än de som är närmare. Detta gör man genom att dividerar både x- och y-värdet med z-värdet, och därmed får ett perspektivkorrigerat värde. Och eftersom koordinatsystemet har y-axeln i motsatt riktning jämfört med en skärm, så brukar man ändra tecknet på y-värdet före divisionen. korr_x = x / z korr_y = -y / z Man måste också se till att z inte är 0 för detta skulle resultera i en division-by-zero, vilket oftast leder till att programmet kraschar. Eftersom resultatet skall bli ett heltal, kan man dividera de båda decimaltalen rakt med varandra, men pga. att talen oftast inte är stora så brukar man multiplicera x- och y-värden med en konstant. korr_x = (k * x) / z korr_y = -(k * y) / z 10

När man använder sig av en låg resolution, som 320x200, brukar 256 vara ett lämpligt värde för konstanten. Detta värde är också lämpligt med tanke på uträkningarna, med tanke på att man istället för att multiplicera kan man skifta bitarna åt vänster med 8 steg. korr_x = (x << 8) / z korr_y = -(y << 8) / z Beroende på hur nära tittaren man vill att punkterna skall vara så kan man addera en konstant till z-värdet före divisionen. Desto större värde man adderar med, desto längre borta från tittaren verkar punkterna vara. korr_x = (x << 8) / (z + avstand) korr_y = -(y << 8) / (z + avstand) Efter detta är mittpunkten fortfarande i origo, dvs. (0,0), och på en skärm är detta i övre vänstra hörnet. Beroende på var på rutan man vill att mittpunkten skall finnas, så adderar man konstanter till både nya x- och y-värden. korr_x = (x << 8) / (z + avstand) + mitt_x korr_y = -(y << 8) / (z + avstand) + mitt_y Ett sätt att snabba upp perspektivkorrigeringen är att i förväg räkna ut två tvådimensionella tabeller som innehåller de korrigerade värden med hänsyn till x/z och y/z. Dessa tabeller äter dock upp en hel del minne, och är därför användbara endast ifall man använder små resolutioner och ändå har mycket minne till sitt förfogande. Och korrigeringen är en mycket lite del av hela processen i räknandet av en bild, så det lönar sig nödvändigtvis inte att slösa minne för att snabba upp just denna del av räknandet. 11

3. Minimering av ritandet 3.1 Sortering Efter att man räknat ut alla punkter som skall användas för ritandet av ytorna bör man göra en del förberedelser. En av de viktigaste är sorteringen av ytorna, pga. att man måste rita ut ytorna i rätt ordning, dvs. från den som är längst bort till den som är närmast. I detta skede av uträkningarna kan man också kolla ifall ytan råkar komma bakom kameran, dvs. ifall ytan är bakom den punkten från vilken man ser på den tredimensionella världen. Dessa ytor kan man helt enkelt slopa, och inte ta i beaktande i de följande stegen. Vad normal sortering dock inte tar i beaktande är att ytor kan gå igenom varandra, men ifall man planerar sin 3D-värld bra så kan man minimera detta. En lösning för problemet skulle vara att använda z-buffering [5], vilket går ut på att man har en skild tabell som innehåller ett z-värde för varje pixel, och att man interpolerar z-värdet för varje pixel man ritar och kollar ifall den är större än förra värdet på samma pixel. Ifall värdet är större så ritar man ut pixeln och uppdaterar tabellen, annars låter man bli. Detta är en bra metod som alla 3D-acceleratorchip använder, men är mycket långsam att implementera i mjukvara på en långsam processor. Emedan quicksort [6] är en populär algoritm för sortering, så passar sig radixsort [7], uppfunnen av Harold H. Seward år 1954, bättre för detta ändamål. Radixsort passar bra för att sortera värden av en bestämd precision, vilket i detta fall är 32-bitar. Att värden är fixedpoint decimaltal behöver man inte ta i beaktande, men de negativa talen måste fås positiva, och detta kan åstadkomma genom att addera 80000000h till varje tal, vilket gör att talen sträcker sig från 0h till FFFFFFFFh istället för från -80000000h till 7FFFFFFFFh. Implementeringen av radixsort är också lättare i assembler än quicksort, vilken är en rekursiv algoritm. Den enda negativa sidan i radixsort är att den kräver dubbelt så mycket minne som man har ytor att sortera. 12

Radixsort går ut på att man sorterar räckan av tal i flere olika steg, t.ex. som i detta fall enligt varje byte i talet börjande med det lägsta och slutande med det högsta. Det är viktigt att behålla ordningen på de tal vilka har samma värden i den sorterade byten, för annars blir resultatet fel. Man kan använda länkade listor för de sorterade värden, men användningen av tabeller är snabbare, speciellt när man programmerar i assembler. I följande exempel sorteras talen enligt varje tal av basen 10 istället för varje byte: Ursprungliga ordningen av talen: 1523 5256 3462 9235 1551 4212 8555 7251 2334 9191 2356 Efter första omgången: 1551 7251 9191 3462 4212 1523 2334 9235 8555 5256 2356 Efter andra omgången: 4212 1523 2334 9235 1551 7251 8555 5256 2356 3462 9191 Efter tredje omgången: 9191 4212 9234 7251 5256 2334 2356 3462 1523 1551 8555 Efter fjärde omgången: 1523 1551 2334 2356 3462 4212 5256 7251 8555 9191 9234 Metoden att använda radixsort med tabeller går ut på att man har två tabeller av samma storlek, och en indextabell där storleken bestäms av enligt hur stora tal man sorterar med, vilket i detta fall är 256, dvs. storleken av värden i en bytes precision. Man börjar med att gå igenom de lägsta bytes i de värden som skall sorteras, och i detta fall gäller det medeltalet punkternas z-värden i varje yta. Varje värde används som index i indextabellen, för att inkrementera värdet på det element indexet pekar på. På detta sätt får man veta hur många det finns av varje tal som skall sorteras. Efter detta fixar man indextabellen så att varje element är summan av de föregående värdena. Sedan går man igenom tabellen som skall sorteras med hjälp av dessa indexvärden så att man använder indexvärden som index när man flyttar talen till den temporära tabellen. Efter varje tal som flyttas inkrementerar man värdet i indextabellen. Denna process görs sedan om för varje byte i talen, och med 32-bitars tal blir det allt som allt fyra gånger. När talen är sorterade så kan man ännu ta och svänga om räckan med talen för att få räckan i en fallande ordning, dvs. de ytor som är längst bort kommer först i räckan. 13

Följande exempel är i pseudokod, och är lätt att implementera i flesta olika programmeringsspråk: ; Tömmer indexräckan for (i = 0; i < 256; i++) { index[i] = 0; ; går igenom varje tal och inkrementerar rätta värdet i ; indexräckan for (i = 0; i < amount_of_faces; i++) { new_index = z_values[face_index[i]] and 255; index[new_index] = index[new_index] + 1; ; går igenom indexräckan och ändrar värden så att varje element ; i räckan motsvarar summan av de föregående elementen sum_index = index[i]; index[i] = 0; for (i = 1; i < 256; i++) { curr_index = index[i] index[i] = sum_index; sum_index = sum_index + curr_index; ; flyttar över talen från ursprungliga räckan med indexräckan ; som index för varje element for (i = 0; i < amount_of_face; i++) { new_index = z_values[face_index[i]] and 255; temp_index[index[new_index]] = face_index[i]; index[new_index] = index[new_index] + 1; Föregående pseudokod var ett exempel på hur en omgång i sorteringen går till, och den måste i detta fall göras fyra gånger, dock med den skillnaden att raderna som definierar new_index skall varje gång motsvara den byte man vill sortera enligt. Med 32-bitars tal skall de se ut enligt följande: Första omgången: new_index = z_values[face_index[i]] and 255; Andra omgången: new_index = (z_values[face_index[i]] >> 8) and 255; osv. I exemplet finns också en räcka z_values, som man har räknat ut i förväg, och som innehåller medeltalet av z-värden för varje punkt i en yta. 14

3.2 Icke-synliga ytor Som redan nämndes vid sorteringen så skall man inte ta i beaktande de ytor som skulle komma bakom kameran eller åskådaren. Men så länge man inte har transparenta ytor, så finns det också en hel del ytor som inte behöver ritas ut, dvs. de ytor som är riktade bort från åskådaren. Genom att slopa dessa ytor slipper man ofta av med nästan hälften av ytorna, och med tanke på att det är ritandet av ytorna som tar mest tid så sparar man en massa tid och gör ritandet mycket snabbare. Före man börjar rita bör man också se efter ifall ytorna hamnar delvis eller helt utanför den synliga rutan. Ifall ytan blir helt utanför så kan man slopa den, men ifall den är delvis utanför så måste man ta och klippa den enligt kanterna. Detta kan implementeras i själva ritrutinen, eller alternativ ändrar man om triangeln till en fyrhörning (eller flerhörning) beroende på hur mycket av ytan hamnar utanför. För att kunna veta vilka ytor är riktade bort från åskådaren använder man en metod som kallas för backface culling [8]. Detta går i sin enklaste form ut på att man räknar ut kryssprodukten för en yta och beaktar ifall den är negativ eller inte. Ifall kryssprodukten är positiv så ritar man ut ytan, och ifall den är negativ så pekar ytan åt fel håll och den ritas inte ut. Denna metod fungerar endast ifall punkterna för varje yta är definierade i 15

motsols riktning. Följande exempel i pseudokod antar att man använder perspektivkorrigerade värden, och returnerar TRUE ifall ytan är synlig och FALSE ifall den inte är det: BackfaceCull(x1, y1, x2, y2, x3, y3) { if ((x3 x1) * (y3 - y2) (x3 x2) * (y3 y1)) > 0 return TRUE else return FALSE; Detta kan också demonstreras med figuren nedan, där den första triangeln skulle vara synlig, medan den andra inte skulle vara det: Ifall den bild man ritar ut innehåller många ytor så kan det vara behändigt att göra backface culling före man sorterar ytorna, för då sorterar man inte i onödan ytor som ändå inte ritas ut. Detta kan implementeras genom att gå igenom alla ytor och göra en tabell med endast de ytor som är synliga, och vilka sedan sorteras. j = 0; for (i = 0; i < amount_of_faces; i++) { curr_face = face_index[i] * 3; nx1 = x_points[faces[curr_face]]; nx2 = x_points[faces[curr_face + 1]]; nx3 = x_points[faces[curr_face + 2]]; ny1 = y_points[faces[curr_face]]; ny2 = y_points[faces[curr_face + 1]]; ny3 = y_points[faces[curr_face + 2]]; if BackfaceCull(nx1, ny1, nx2, ny2, nx3, ny3) { new_face_index[j] = face_index[i]; j = j + 1; amount_of_new_faces = j; 16

I exemplet ovan innehåller face_index-tabellen indexet för de tre efter varandra liggande värden i faces-tabellen vilka bygger upp en triangel, dvs. en yta. Dessa tre värden används sedan som index för x_points- och y_points-tabellerna, vilka innehåller de perspektivkorrigerade x- och y-koordinaterna. Idén med att ha allt detta i egna tabeller är att man inte behöver ändra på värden i t.ex. faces-tabellen, utan endast ändrar på indextabellens värden. 17

4. Ritandet 4.1 Trådmodeller För att få en enkel visuell bild av det som man tidigare räknat ut så kan man använda sig av trådmodeller (wireframe-models). Eftersom hela den scenen man räknat ut består av trianglar i detta fall, så kan man med en simpel linjerutin rita konturerna av varje triangel. Med detta kan man få ett bra grepp om hur den tredimensionella världen ser ut vid olika tidpunkter, och detta är också ett snabbt sätt att rita. En negativ sida är dock att pga. att man inte fyller ytorna så ser man igenom varje föremål i den uträknade världen. En vanlig linjerutin går ut på att man ritar en linje mellan två punkter på det tvådimensionella planet. Hur det detta går till beror på skillnaden mellan avstånden på x- och y-ledet. Ifall linjen är längre på x-ledet så räknar man ut en ny y-koordinat för varje steg på x-axeln, och är y-ledet längre så räknar man y-koordinater enligt stegen på x- axeln. Detta för att linjen inte skall få några springor, som i exemplet nedan, där den vänstra linjen är räknad med hänsyn till x-axeln och den högra med hänsyn till y-axeln: En av de mest populära linjealgoritmerna är Bresenhams algoritm [9], vilken passar bra för detta ändamål, för att den i sin optimerade form endast behöver additioner och subtraktioner med heltal för att rita linjer. Algoritmen går ut på att man tar i beaktande det faktum att ifall man ritar en linje från vänster till höger längs med x-axeln och uppifrån neråt på y-axeln, så har varje nya punkt endast två alternativ, den är antingen (x+1,y) eller (x+1,y+1). Man bestämmer ifall man adderar 1 till y genom att räkna ut absolutbeloppet för skillnaden mellan de två olika x-värden och de två olika y-värden, och sedan dividera dessa för att ge linjens sluttning. Efter varje steg adderar man 18

sluttningen till en fri variabel som är mellan 0 och 1, och ifall denna variabel är större än 0.5 så adderar man 1 till y. Exempelprogrammet skulle se ut som följande: DrawLine(x1, y1, x2, y2) { // räknar ut skillnaden för x och y värden delta_x = abs(x2 x1); delta_y = abs(y2 y1); var = 0; // räknar ut sluttningen slope = delta_y / delta_x; y = y1; for (x=x1; x < x2; x++) { put_pixel(x,y,1); var = var + slope; if (var > 0.5) { y = y + 1; // för att hålla var mellan 0 och 1 var = var 1; Problemet med denna lösning är att det behövs decimaltal för att representera både var och slope variablerna. Detta kan lösas med att multiplicera alla decimaltal med delta_x, för att då kunna använda endast heltal. I jämförelsen av var med 0.5 kan man multiplicera båda sidorna med 2, för att helt slippa av med decimaltalen. Nya lösningen ser ut som följande: DrawLine(x1, y1, x2, y2) { delta_x = abs(x2 x1); delta_y = abs(y2 y1); var = 0; slope = delta_y; y = y1; for (x=x1; x < x2; x++) { put_pixel(x,y,1); var = var + slope; if ((var << 1) > delta_x) { y = y + 1; var = var delta_x; 19

Efter detta behöver man endast skriva om programkoden så att den tar de olika vinklarna i beaktande. Ifall delta_y är större än delta_x behöver man endast svänga om de två deltan och x- och y-koordinaterna. Sedan måste man ta i beaktande ifall x2 är mindre än x1, eller y2 mindre än y1, i vilka fall man måste subtrahera 1 från y-koordinaten ifall man ritar längs med x-axeln, eller tvärtom. 20

4.2 Fyllda ytor För att få 3D-bilder att se intressantare ut än trådmodeller, så måste man ta och fylla trianglarna som scenen består av. Det finns flere olika alternativ till hur man vill fylla trianglarna, vilket sedan påverkar på både utseende och hastighet. Som tumregel kan man ha att desto finare man vill att allt ser ut, desto långsammare blir utritandet. Lättaste att implementera är en simpel flat-fill [10], vilket betyder att man fyller ytorna med en färg. Redan med detta får man till stånd mycket intressantare och livligare bilder än med endast trådmodeller. Trådmodell Modell med fyllda ytor Att rita en triangel går ut på att man har tre punkter a, b och c, som i exempelbilden nedan. Idén är då att man räknar ut vinkeln för linjerna ab, ac och bc, och hjälp av dessa fyller man triangeln uppifrån neråt [4]. Det är alltså viktigt att punkterna är sorterade, dvs. att a är översta, b är mittersta och c är nedersta hörnet i triangeln. Ritandet delas i två delar, först ritar man från a till b på y- ledet, och i andra skedet från b till c. I första skedet räknar man ut nya x-koordinater för 21

både ab och ac för varje steg på y-ledet, och drar ett streck mellan dessa punkter, sedan när man nått b börjar man räkna x-värdet för bc istället för ac och fortsätter tills man nått punkten c. Implementationen i sin enklaste form går ut på att man räknar sluttningarna för ab, ac och bc, dvs. man interpolerar x-värdet från xa till xb i (yb-ya) antal steg. Om man antar att punkterna redan är sorterade enligt höjden, som i figuren ovan, ser exempelprogrammet ut enligt följande: ; första delen av triangeln, från a till b på y-ledet for (y = ya; y < yb; y++) { x1 = (y ya) * ((xb xa) / (yb ya)) + xa; x2 = (y ya) * ((xa xc) / (ya yc)) + xa; draw_line(x1, y, x2, y, color); ; andra delen av triangeln, från b till c på y-ledet for (y = yb; y < yc; y++) { x1 = (y ya) * ((xb xc) / (yb yc)) + xb; x2 = (y ya) * ((xa xc) / (ya yc)) + xa; draw_line(x1, y, x2, y, color); Exemplet ovan är långt ifrån den mest optimala lösningen, men demonstrerar bra hur man ritar en triangel på skärmen. Idén med att dela ritandet i två delar är att man lätt kan ta specialfall i beaktande. Ifall både yb och yc skulle råka vara lika, så skulle man endast behöva gå igenom första delen av rutinen, och ifall ya och yb är lika så räcker kan man hoppa över första delen och börja från den andra. 22

Fastän exemplet är fungerande, så är det långt ifrån att vara snabb. Ett stort problem är att man gör två multiplikationer för varje steg på y-ledet för att interpolera x-värdet. En lösning är att använda fixedpoint tal istället för flyttal, men detta snabbar inte upp räknandet tillräckligt ändå. Funktionen för interpolering av t.ex. 4 till 9 i åtta steg är enligt följande: f(x) = 4 + x * ((9 4) / 8) 4.000 4.625 5.250 5.875 6.500 7.125 7.750 8.375 9.000 Funktionen kan generaliseras som: f(x) = A + x * ((B A) / antalet_steg) Man kan nu märka att ((B A) / antalet_steg) är konstant för varje steg, och att man då kunde skriva om den så att man börjar med A och för varje steg adderar till denna konstant. På detta sätt slipper man av med en multiplikation, en division och en subtraktion för varje steg, vilket snabbar upp räknandet, speciellt med tanke på att dessa uträkningar görs otaliga gånger under ritandet av en bild. Detta kan då utnyttjas i exempelprogrammet, vilken efter förändringarna skulle se ut som följande: ; räknar ut konstanterna för sluttningarna, förlänger täljaren ; till ett 32-bitars fixedpoint decimaltal, medan nämnaren hålls ; som ett 16-bitars heltal och kvoten fås då i 32-bitars ; fixedpoint format. slope_ab = ((xb xa) << 16) / (yb ya); slope_ac = ((xa xc) << 16) / (ya yc); slope_bc = ((xb xc) << 16) / (yb yc); ; första delen av triangeln, från a till b på y-ledet x1 = xa << 16; x2 = xa << 16; for (y = ya; y < yb; y++) { draw_line(x1 >> 16, y, x2 >> 16, y, color); x1 = x1 + slope_ab; x2 = x2 + slope_ac; ; andra delen av triangeln, från b till c på y-ledet x1 = xb << 16; for (y = yb; y < yc; y++) { draw_line(x1 >> 16, y, x2 >> 16, y, color); x1 = x1 + slope_bc; x2 = x2 + slope_ac; 23

I detta exempel har man nu endast två additioner per y-koordinat, istället för att utföra två divisioner, två multiplikationer, två additioner och flertal subtraktioner. Vad exemplet fortfarande inte tar i beaktande är ifall hörnen av triangeln är utanför rutan, men dessa är triviala att implementera. Lättaste sättet på x-ledet är att i draw_line-funktionen kolla ifall x-koordinaterna är utanför skärmens kanter, och i så fall endast rita den synliga delen. På y-ledet gäller samma sak, dvs. man kollar ifall punkterna hamnar utanför det synliga området och börjar rita från de första koordinaterna som är synliga. Också om draw_line-funktionen är implementerad så att det ritar punkterna från vänster till höger så måste man kolla vilkendera x-koordinaten är mindre och vilken större. Detta kan dock implementeras in i själva ritfunktion eller alternativt väljer man x1 och x2 enligt vilkendera är på vänstra sidan. Speciellt om man programmerar på assemblernivå, och man ritar med 8-bitars färgrymd så kan linjeritandet optimeras så att man fyller fyra pixlar med en instruktion genom att kopiera med 32-bitars register då det är möjligt. Att optimera inre loopen som sköter om ritandet av linjen är också den som lönar sig att optimera bäst, speciellt när det gäller gouraudshading och texturemapping, då man inte endast skriver pixlar i minnet, utan också är tvungen att utföra beräkningar för varje pixel som ritas. Om man behöver inkludera klippandet av kanterna i sin triangelrutin kan man också snabba upp ritandet med att ha två olika versioner av ritrutinen, en som inte tar klippning i beaktande och en som gör det. Då kan man före ritandet kolla ifall ytan hamnar delvis utanför rutan och då kalla på rutinen med klippning och i andra fall på den normala rutinen. Detta snabbar upp ritandet en del pga. att en rutin som inte tar klippning i beaktande är snabbare, och flesta trianglar hamnar ändå inte delvis utanför rutan. Fastän man är tvungen att göra några överlopps jämförelser för punkterna, så är det ändå förmånligare än att använda klippande rutinen för varje yta. 24

5. Diskussion Efter att man har implementerat matrisrotationer, perspektivkorrigering, sortering, ritrutinerna och de olika formerna för att minimera ritandet, har man redan grunderna för att bygga upp en fullständig 3D-motor. Något som inte ännu tagits upp är olika sätt att fylla ytor, beaktande av ljuskällor, kamera för olika kameravinklar och dylikt. Medan flesta av dessa är lätta att implementera, är de svårare att optimera väl för att fungera på svagare datorer. Speciellt i olika metoder att fylla ytor kan man behöva gå ner på assemblernivå för att få de bästa resultaten, genom att optimera för den processorfamilj och specifik processormodell man programmerar för. Man kan också konstatera att ifall man faktiskt behöver bra prestanda och precision, fastän man använder en svagare processor, så kan det vara lämpigare att utnyttja ett 3Dacceleratorchip istället för att spendera tid på att optimera alla rutiner till fullo. Att få allt att löpa smidigt med endast mjukvara kan vara tidsmässigt krävande, och ofta inte värt mödan. Vilket ändå inte betyder att man skall programmera slarvigt och använda dåliga algoritmer, för ifall man väljer fel algoritm är det omöjligt att få implementation snabb oberoende hur mycket man än försöker optimera. Det är först efter att man vält rätt algoritm och lösning för rätt ändamål som man skall behöva gå på låg nivå och optimera själva programkoden. 25

Litteraturförteckning 1. Fixed-point arithmetic, Wikipedia, ändrat 10.2.2006, http://en.wikipedia.org/wiki/fixed_point_numbers, 14.2.2006 2. Data Structures for Game Programmers, Ron Penton, 2003 3. Computer Graphics, C-version (2nd Edition), s.439-443, Donald Hearn, 1996 4. Computer Graphics, C-version (2nd Edition), s.443-447, Donald Hearn, 1996 5. Computer Graphics, C-version (2nd Edition), s.472-475, Donald Hearn, 1996 6. Quicksort, Wikipedia, ändrat 11.2.2006, http://en.wikipedia.org/wiki/quicksort, 14.2.2006 7. 3D Game Engine Architecture, David H. Eberly, 2005 8. 3Dica, Ilkka Peltonen & Kaj Mikael Björklund, kap 6.1, ändrat mars 1998, http://tfpsly.planet-d.net/docs/3dica/3dica6.htm#chap61, 14.2.2006 9. Computer Graphics, C-version (2nd Edition), s.88-92, Donald Hearn, 1996 10. 3Dica, Ilkka Peltonen & Kaj Mikael Björklund, kap 3.1, ändrat mars 1998, http://tfpsly.planet-d.net/docs/3dica/3dica3.htm#chap31, 14.2.2006 26