Projekt i TNM084, Procedurella bilder Inledning Kursen TNM084, Procedurella Bilder innehåller ett examinerande projekt. Målet med projektet är att utöka förståelsen för hur man kan producera syntetiska bilder utifrån procedurellal metoder. Efter en del funderande kom jag fram till att jag ville rendera animerade vattendroppar som rör sig över en yta. Vattendropparnas rörelse simuleras inte fysikalsikt korrekt utan det enda som har haft betydelse för mig projektet har varit det visuella intrycket utav resultatet samt att det skall gå att köra i rimligt interaktiva bilduppdateringshastigheter. Projektet är skrivet i OpenGL Shading Language [1] och ATIs RenderMonkey [2]. Teori Projektet består helt kort i 1. animering av vätskedroppar och 2. rendering av dessa på ett trovärdigt sätt med sedan tidigare kända tekniker. Vi kommer nedan att ta upp o titta lite på de tekniker som använts. Vätska Tanken med projektet var att rendera vätska som rör sig över en yta i realtid utan att för den skull behöva bry sig om fysiken i det hela. Den teknik som används är i mångt och mycket hämtad från Ines Stuppachers[3] forskning om vattendroppar i realtid där vätskan modelleras som vätskepartiklar. Mängden vätskepartiklar i varje punkt av en yta lagras i form av en 2Dtextur där varje pixel {0.0 1.0} motsvarar hur mycket vätska som finns på den positionen. Vätskan kommer sedan förflytta sig nedåt om antalet vätskepartiklar i punkten p i,j överstiger tröskeln t. Det kommer att tillföras vätska till punkten p i,j om antalet vätskepartiklar i punkten p i,j+1 överstiger tröskeln t. Detta är själva grundformeln i teorin och allt annat ugår ifrån detta. Modellen har utökats till att även ta punkterna p i+1,j och p i-1,j i beaktande. Mängden vätska som förflyttas är väldigt viktig för hur resultatet blir. Förflyttar vi all vätska så kommer vätskedropparna att åka ned över ytan obehindrat. Alla som har tittat på vätskedroppar i verkliga världen vet att på många ytor kommer små vätskedroppar stanna kvar efter de stora. Väljer vi istället att förflytta så mycket vätska att antalet vätskepartiklar är mindre än t men fortfarande lämnar lite kvar efter förflyttningen kommer det att bildas enstaka vätskepartiklar efter de stora. Detta kommer att ge något som är rimligt likt det vi ser i verkliga världen. Lågpassfiltrering Det faktum att den 2D-textur som representerar antalet vätskepartiklar har begränsad upplösning och vi vill använda ifsatser för tröskelvärdena gör att vi kan drabbas av ganska extrema aliasing-effekter eftersom if och step-funktioner skapar godtyckligt höga
frekvenser[4]. Detta löses inte genom att använda mjukare smoothstep utan istället väljer vi att lågpassfiltrera bilden[5] för att plocka bort de höga frekvenserna. 1 1 1 1 9 1 1 1 1 1 1 Filterkärna för lågpassfiltrering Normalmapping Normal maps är ett känt begrepp inom datorgrafik [6] och går ut på att skapa extra detaljer (mjuka kanter, sprickor, etc) utan att tillföra trianglar till meshen. Vi kommer att betrakta vår lågpassfiltrerade bild som en heightmap och generera en normal map ifrån den. Vi genererar normalmappen med ett enkelt sobelfilter[5]. Efter filtreringen viktas och skalas vektorn till vår nya normal map. 1 0 1 1 2 1 2 0 2 0 0 0 1 0 1 1 2 1 Sobeloperatorer i x- och y-led. I normalmappen har vi nu normalerna till våra vätsketextur definierade i tangentspace. Detta innebär att det krävs en transformation till varje polygons tangent space när vi skall använda normalmappen[7]. Reflektioner För att på ett visuellt korrekt sätt rendera vätska kommer vi behöva någon form av reflektion mellan ytan och omgivningen. Vi använder vår view direction v och ytans normal n kan vi skriva reflektionsriktningen: = 2 Med hjälp av reflektionsriktningen kan vi sedan slå i en cubemap efter den reflekterade intensiteten.[6] Refraktioner När ljus övergår gränsen mellan ämnen med olika brytningsindex kommer ljuset att brytas. Brytningen sker enligt Snells lag[8]. För att ytterliggare öka trovärdigheten hos vätskerenderingen kommer refraktioner att tas i beaktande mellan vätska och luft. Detta görs genom att låta den transparanta delen, dvs den underliggande ytan textur-tabellslagning förskjutas något. Implementation Implementationen är gjord i GLSL. Vi kommer att använda två olika texturer för att lagra våra vätskepartiklars positioner. Detta krävs eftersom vi inte kan förflytta partiklarna i samma textur som vi läser dem ifrån. Skulle det misstaget göras skulle vi börja räkna på partiklar som redan är förflyttade, osv. Vi använder två förgenererade varianter utav perlin noise[4] med olika frekvenser. En med högfrekvent noise och en med lägre frekvenser. Pass 1 I detta pass kopierar vi vätskepartiklarna från en textur till en annan (från heightmappen i pass 2). Vi kan även använda vätskepartiklarnas orginaltextur för att på såsätt pumpa in ny vätska från orginaltexturen. I detta pass har vi också möjlighet att rensa ut (torka) en del av den vätska som finns på ytan. Pass 2 Här gör vi all förflyttning utav vätskepartiklarna. Vi låter tröskelvärdet t ändras beroende på
resultatet utav en tabellslagning i en noisetextur. Vi varierar även mängden vatten som förflyttas i sidled efter tabellslagning i en noisetextur. Resultatet av detta pass är en heightmap som beskriver antalet vätskepartiklar i varje texel. Pass 3 Vi utför en enkel lågpassfiltrering utav vår heightmap och renderar till en lågpassfiltrerad heightmap Normal map utav vattenpartiklarna Pass 5 Detta är renderingspasset. Vi börjar med att i vertexshadern ta fram inversen utav tangentspace-matrisen[7]. Detta görs genom att anta att matrisen är ortonormerad vilket leder till att transponatet är lika med inversen [9]. Bilden visar en lågpassfiltrerad heightmap Pass 4 I detta pass tar vi fram en normal map av vätskepartiklarna från vår heightmap. Resultatet kan ses nedan. I fragmentshadern renderar vi sedan vätskan genom att kombinera heightmappen med normalmappen. Reflektionenn i varje fragment beräknas och mixas med vätskans färg. Ytan som vätskan befinner sig på slås upp beroende på heightmappen för att efterlikna refraktion och mixas sedan med vätskan beroende på heightmappen. Utdata utav denna rendering är vårat färdiga resultat. Resultat Målet med detta projekt var att göra något som efterliknar hur vätska rör sig över en yta och nedan ser vi resultatet i stillbilder. Vätskan är färgad röd för att efterlikna blod.
Resultatbild 1 Resultatbild 3 Diskussion och reflektioner Det har aldrig varit något mål i sig att vätskans rörelse ska basera sig på fysikaliskt riktiga modeller. Däremot har inspirationen hämtats från fysiken, både vad gäller vätskansrörelse och utseende. Refraktioner har inte gjorts fysikaliskt korrekt utan är istället en förändrad tebellslagning beroende på mängden vätskepartiklar. Detta innebär att brytningsindex, vinkel mellan yta och betraktare osv. inte har tagits med i beräkningarna. Det viktiga var att något som i alla fall liknar brytning utförs. Resultatbild 2 Det förekommer fortfarande problem med aliasing i blodet, något som skulle kunna avhjälpas med ytterliggare lågpassfiltreringar. Detta genom ytterliggare ett pass eller utökad filterkärna. Ett annat alternativ vore att undersöka resultatet utav smoothstepfunktioner istället för if-satserna. Detta har av tidsbrist inte gjorts.
Det finns vissa problem med reflektionerna och jag kan inte annat än säga att jag misstänker att något inte riktigt går rätt till där. Just nu är reflektionerna övertygande nog men de borde definitivt få sig en genomarbetning. Optimering utav algoritmerna och en rejäl genomgång med detta i åtanke är något som borde göras. Det är såpass mycket operationer, tabellslagningar och variabeldefinitioner gjorda av experimentlust och tidspress att det utan tvekan går att pressa upp FPSen utan att det visuella resultatet blir lidande. Även om projektet är generellt för vätskor tänker jag mig ofta blodigare tillämpningar. Att i ett spel kunna slänga ut lite blod på väggar, golv och tak hade väl få spelutvecklare tackat nej till? (Bortsett från tyskar och barntillåtna spel). Hade det inte varit läckrare att låta blodet sakta sipra ned istället för att slänga på en tråkig dekal ovanpå väggen? förflyttning bero på vad som är nedåt i world space skulle vi kunna få vätskan att påverkas av ytans orientering och gravitationen. Detta skulle kräva ytterliggare arbete om vätskepartiklarna skulle beräknas på en krökt yta, dvs. polygoner ligger i samma plan. Ska vi t.om. ta med interpolerade per-vertex-normaler i beräkningen? Riktning och ytans heightmap Grundtanken här är att genom att utöka ovanstående diskussion om riktningar till att ta med heightmappen till ytan som vätskepartiklarna rör sig på skulle man kunna få vätskan att rinna utefter detaljerade ytor på ett mer realistiskt sätt. Som ett exempel kan man tänka sig att vätska som rinner ner över en stenmur antagligen kommer att befinna sig så mycket som möjligt i sprickor och fogar mellan stenarna. Vidareutveckling Att sätta målen för högt är inte ovanligt och detta är inget undantag. På grund av prioriteringar har alla häftiga features som planerats och kommit upp under projektets gång inte kommit med. Olika färger Heightmappen är RGBA och behandlas redan som en vektor med fyra värden fast endast ett används. Genom att använda hela vektorn skulle man kunna låta vätskan byta färg gradvis. T.ex. genom att pump in ny vätska på initialstället och sedan låta den blanda sig. Riktning Just nu rör sig vätskan i en fast riktning i texturen och resultatet är beroende på texturens orientering. Genom låta vätskans I vissa fall här kanske jag är ute o cycklar vad gäller det tekniska eller att det blir för tungt beräkningsmässigt. Kanske skulle det visa sig att det inte fungerar men det är roligt att fundera och ännu roligare att försöka. Körinstruktioner För att slippa bråka med opengl i allmänhet och shader-inläsning samt FBOer i synnerhet så valde jag den smärtfria vägen och använde en färdig utvecklingsmiljö. AMD/ATIs utvecklingsmiljö för snabb shaderprototyping heter Rendermonkey och finns att hämta på deras hemsida. Jag har använt version 1.81 av deras program och är mycket nöjd. För att köra mitt projekt måste ni göra följande:
1. Installera rendermonkey. 2. Öppna projektet och använd de texturer jag använt på rätt ställen i projektet. 3. Renderingen startas genom att trycka på den mycket väluttänkta ikonen med den lilla brinande kulan. [8] Halliday, Resnick, Walker. 2005 Fundamentals of Physics, 7 th Edition. Snells Law, sida 906. [9] Wikipedia om Orthogonal Matrix - http://en.wikipedia.org/wiki/orthogonal_matri x Jag har implementerat och testkört på följande utrustning: Intel Core 2 Duo > 2.0ghz, 2gb ram, Geforce 9800GTX+ i Windows Vista. Referenser [1] OpenGL Shading Language - http://www.opengl.org/documentation/glsl/ [2] AMD/ATIs RenderMonkey - http://ati.amd.com/developer/rendermonkey/ [3] Ines Stuppacher, Peter Supan. - Rendering of Water Drops in Real-Time, CESCG-2007. [4] David S. Ebert, F. Kenton Musgrave, Darwyn Peachey, Ken Perlin and Steven Worley. 2003 - Texturing & Modelling, A Procedural Approach, Third Edition. Academic Press. Aliasing Sida 57. [5] Rafael C. Gonzalez, Richard E. Woods. 2008 Digital Image Processing, Third Edition. Pearson Prentice Hall. Sida 152, Smoothing Spatial Filters och Sobel Operators Sida 167. [6] Tomas Akenine-Möller, Eric Haines, Naty Hoffman. 2008 Real-Time Rendering, Third Edition. A K Peters, LTD. Normal mapping Sida 187, Cube Maps Sida 171. [7] Søren Dreijer - Bump Mapping Using CG, Third Edition. http://www.blacksmithstudios.dk/projects/downloads/bumpmapping_ using_cg.php