Breaking waves in 3D

Breaking waves

Mer tid för att hålla på med Three.js och WebGL. Det primära målet är att få en djupare förståelse för shaders genom att bara hålla på lite med det. Om du vill veta grunderna om shaders i three.js rekommenderas det att du läser mer om det online. Men i det här inlägget ska jag försöka förklara några av de saker jag har lärt mig under processen hittills. Jag vet att det är som ett dagis för ”riktiga” CG-programmerare, men historien upprepar sig ofta inom detta område. Så låt oss låtsas att vi är coola ett tag.

Vågen

Man börjar med att göra en vågmodell, och ett enkelt plan som havet i fjärran, i 3D Studio Max. Senare målar man vågen med vertexfärger. Dessa färger användes i skuggan för att efterlikna vågens genomskinlighet. Kunde ha använt riktig SubSurfaceScattering, men den här enkla lösningen fungerar bra. Vertex-färgerna blandas med vanliga diffusa och omgivande färger. En normal karta applicerades sedan med ett kaklat brusmönster för rippel. Normalerna för denna textur uppdateras inte i förhållande till vertex-animeringen, men det är bra. Utöver det blandas en skumstruktur med vitt perlinbrus med den diffusa färgen.

Jag gillar alltid ”bakom scenen”-delen som visar genomgångarna i en rendering med alla lager separerade och slutligen blandade, så här är en av mina egna så att du kan se de olika stegen:

Animera med vertex shader

Vågen är ett statiskt nät. För den vingliga rörelsen över vågen, animerar man hörn i vertex shader. En rörelse längs y-axeln som skapar en vågig rörelse och en längs x-axeln för några mindre krusningar. Du har förmodligen sett detta ofta, eftersom det är ett enkelt sätt att animera något med bara ett tidsvärde som passerat i varje bildruta.

vertexPosition.y += sin( (vertexPosition.x/waveLength) + theta )*maxDiff;

Tidsförskjutningen (theta) tillsammans med x-positionen går in i en sin-funktion som genererar en periodisk cykel med ett värde mellan -1 och 1. Multiplicera det med en faktor och du animerar hörnen upp och ner. För att få fler toppar längs nätet, dela positionen med våglängd.

En sidoanteckning: Man kan försöka använda vertextextursökningar (stöds i ANGLE-versionen) för verkliga vertexförskjutningar. Med det kan man få mer unika och dynamiska animationer.

Animera med fragmentskuggning

Vågen rör sig faktiskt inte, det är bara uv-kartläggningen. Samma tidsförskjutning som används i vertex shader är här ansvarig för att ”skjuta” uv-kartan framåt. man kan också redigera UV-kartläggningen i sitt 3D-program och modifiera rörelsen. I det här fallet måste toppen av den brytande delen av vågen gå i motsatt riktning, så man roterar bara uv-mappningen 180 grader för dessa rörelser. Den här lösningen fungerar bra så länge man inte tittar över vågkanten och ser sömmen. Men jag tror att man i teorin skulle kunna göra en skenbar övergång mellan de två riktningarna. Man kan också använda olika offset-värden för olika texturer, som en Quake-himmel med parallaxade lager av moln.

Slåss om z med Depth Buffer

Det gäller att vara medveten om djupbufferten, eller z-bufferten, som lagrar djupvärdet för varje fragment. Förutom ljusberäkning, eftereffekter och annat, så används dessa data för att bestämma om ett fragment ligger framför eller bakom andra fragment. För oss som kommer från Flash 3D (pre Molehill) är detta en ny teknik i pipelinen (även om vissa experiment som detta använder den). Nu, när den mäktiga GPU:n är ansvarig så har vi inga flimrande sorteringsproblem eller hur?

Nej, artefakter visas fortfarande. Vi har samma problem som med trianglar, men nu på fragmentnivå. I grund och botten så händer det när två primitiver (triangel, linje eller punkt) har samma värde i bufferten, med resultatet att det ibland dyker upp och ibland inte. Anledningen är att buffertens upplösning är för liten i det specifika området, vilket leder till att z-värden tar till samma djupkartvärde.

Det kan vara lite svårt att föreställa sig betydelsen av upplösning, men jag ska försöka förklara, med betoning på att försöka, eftersom jag själv precis har lärt mig detta. Bufferten är icke-linjär. Mängden tillgängliga siffror är relaterad till z-positionen, när- och fjärrplanet. Det finns fler nummer att använda, eller högre upplösning, närmare ögat/kameran, och färre tilldelade nummer när avståndet ökar.

Saker på avstånd kommer att dela ett kortare utbud av tillgängliga nummer än sakerna precis framför dig. Det är ganska så användbart, du behöver förmodligen mer detaljer precis framför dig där detaljerna är större. Den totala mängden nummer, eller kalla det precision, är olika beroende på grafikkortet med möjliga 16 bitars, 24 eller 32 bitars buffertar. Lägger helt enkelt till mer utrymme för data.

För att undvika dessa artefakter så har vi några tekniker att vara medvetna om, och här kommer nybörjarläxan den här gången. Genom att anpassa fjärr- och närplanen till kameraobjektet kan du bestämma var upplösningen ska fokuseras. Tidigare använde jag near=0,001 och far=”Some large number”. Detta gör att upplösningen blir mycket hög mellan 0,0001 och ”ett annat litet antal”.

(Det finns en formel för detta, men det är inte viktigt just nu) Således var djupbuffertupplösningen för låg när den närmaste triangeln ritades upp. Så det enkla att komma ihåg är: Ställ in nära-värdet så långt du kan för att din lösning ska fungera. Värdet på det bortre planet är inte lika viktigt eftersom skalans olinjära karaktär. Så länge närvärdet är relativt litet vill säga.

Ett annat problem med ocklusionsberäkningar är när man använder färgvärden med alfa. I de fallen får man ibland flera ”lager” av primitiver, men bara ett värde i djupbufferten. Det kan hanteras genom att blanda färgen med den som redan finns lagrad i utdata (frame buffer). En annan lösning är att ställa in depthTest till false på materialet, men det är uppenbarligen inte möjligt i de fall vi behöver att något ligger bakom något annat. Man kan också rita upp scenen framifrån och bakifrån eller i olika pass, med det genomskinliga föremålet sist, men jag har inte använt den tekniken än.

Breaking waves

Kollisionsdetektering

Jag använde först originalnätet för kollisionsdetektering. Det gick ganska så snabbt i Chrome men smärtsamt långsamt i Firefox. Itererande arrayer verkar skilja sig mycket i prestanda mellan webbläsare. Ett snabbt tips: När du använder kollisionsdetektering, så använd separata dolda maskor med endast de som behövs för att utföra jobbet.

Dimma, stänk och sprites

All dimma och bakvatten är instanser av objekttypen: Sprite. En snabbare och lättare typ av föremål, alltid pekade på dig som en skylt. Jag har sagt det förut, men en viktig aspekt här är att använda en objektpool så att man kan återanvända varje sprite som skapas. För att hela tiden skapa nya kommer att drabba prestanda och minnesallokering.

Jag är inte riktigt nöjd med den slutliga lösningen av dessa partiklar, men det är tillräckligt bra för ett snabbt experiment som detta.

Lär dig Javascript

Som nykomling i Javascript-världen lär man sig fortfarande de uppenbara sakerna. Som konsolfelsökning. Det är en stor fördel att ändra variabler i realtid via konsolen i Chrome. Till exempel, att ställa in kamerans position utan att behöva ett användargränssnitt eller byta program.

Och det slutar fortfarande med att jag skriver nästan all min kod i en enda stor fil. Med alla ramar och byggnadsverktyg runt omkring så är jag inte riktigt säker på var jag ska börja. Det är en dålig ursäkt, jag vet, men samtidigt, när jag gör ett experiment så vet jag sällan vart jag är på väg, så att skriva ”korrekt” OOP och strukturerad kod är inte huvudmålet.

Än en gång, tack för att du läste! Och dela gärna dina tankar med mig, jag uppskattar verkligen feedback, speciellt korrigeringar eller påpekande av missförstånd som kan få mig att göra saker på ett bättre sätt.

Lämna ett svar

Din e-postadress kommer inte publiceras.