Dela via


Fallstudie – Skapa en galax i mixad verklighet

Innan Microsoft HoloLens levereras frågade vi vår utvecklarcommunity vilken typ av app de vill se en erfaren intern teamversion för den nya enheten. Mer än 5000 idéer delades, och efter en 24-timmars Twitter-undersökning var vinnaren en idé som heter Galaxy Explorer.

Andy Zibits, konstledare i projektet, och Karim Luccin, teamets grafiktekniker, talar om det samarbete mellan konst och teknik som ledde till skapandet av en korrekt, interaktiv representation av Vintergatan-galaxen i Galaxy Explorer.

Tech-tekniken

Vårt team - bestående av två designers, tre utvecklare, fyra konstnärer, en producent och en testare - hade sex veckor på sig att bygga en fullt fungerande app som skulle göra det möjligt för människor att lära sig om och utforska storheten och skönheten i vår Vintergatan Galaxy.

Vi ville dra full nytta av HoloLens förmåga att återge 3D-objekt direkt i ditt vardagsrum, så vi bestämde oss för att vi ville skapa en realistisk galax där människor skulle kunna zooma in nära och se enskilda stjärnor, var och en på sina egna banor.

Under den första veckan av utveckling, kom vi upp med några mål för vår representation av Vintergatan Galaxy: Det behövde ha djup, rörelse och känsla volumetric-full av stjärnor som skulle bidra till att skapa formen på galaxen.

Problemet med att skapa en animerad galax som hade miljarder stjärnor var att det stora antalet enskilda element som behöver uppdateras skulle vara för stort per ram för HoloLens att animera med hjälp av CPU. Vår lösning innebar en komplex blandning av konst och vetenskap.

I bakgrunden

För att låta människor utforska enskilda stjärnor var vårt första steg att ta reda på hur många partiklar vi kunde återge samtidigt.

Återgivning av partiklar

Aktuella processorer är bra för bearbetning av serieuppgifter och upp till några parallella uppgifter samtidigt (beroende på hur många kärnor de har), men GPU:er är mycket effektivare vid bearbetning av tusentals åtgärder parallellt. Men eftersom de vanligtvis inte delar samma minne som processorn kan utbyte av data mellan CPU<>GPU snabbt bli en flaskhals. Vår lösning var att göra en galax på GPU, och den var tvungen att leva helt på GPU.

Vi började stresstester med tusentals punktpartiklar i olika mönster. Detta gjorde det möjligt för oss att få galaxen på HoloLens att se vad som fungerade och vad som inte fungerade.

Skapa stjärnornas position

En av våra teammedlemmar hade redan skrivit C#-koden som skulle generera stjärnor på deras ursprungliga position. Stjärnorna är på en ellips och deras position kan beskrivas av (curveOffset, ellipsSize, elevation) där curveOffset är vinkeln på star längs ellipsen, ellipsSize är dimensionen av ellipsen längs X och Z, och höja rätt höjning av star inom galaxen. Därför kan vi skapa en buffert (Unitys ComputeBuffer) som initieras med varje star attribut och skicka den på GPU:n där den skulle finnas för resten av upplevelsen. För att rita den här bufferten använder vi Unitys DrawProcedural som gör det möjligt att köra en skuggning (kod på en GPU) på en godtycklig uppsättning punkter utan att ha ett faktiskt nät som representerar galaxen:

CPU:

GraphicsDrawProcedural(MeshTopology.Points, starCount, 1);

GPU:

v2g vert (uint index : SV_VertexID)
{

 // _Stars is the buffer we created that contains the initial state of the system
 StarDescriptor star = _Stars[index];
 …

}

Vi började med råa cirkulära mönster med tusentals partiklar. Detta gav oss det bevis vi behövde för att vi skulle kunna hantera många partiklar och köra det i högpresterande hastigheter, men vi var inte nöjda med galaxens övergripande form. För att förbättra formen försökte vi olika mönster och partikelsystem med rotation. Dessa var ursprungligen lovande eftersom antalet partiklar och prestanda förblev konsekvent, men formen bröts ner nära mitten och stjärnorna avgav utåt vilket inte var realistiskt. Vi behövde ett utsläpp som skulle göra det möjligt för oss att manipulera tiden och få partiklarna att röra sig realistiskt, loopa allt närmare mitten av galaxen.

Vi försökte olika mönster och partikelsystem som roterade, som dessa.

Vi försökte olika mönster och partikelsystem som roterade, som dessa.

Vårt team gjorde viss forskning om hur galaxer fungerar och vi gjorde ett anpassat partikelsystem specifikt för galaxen så att vi kunde flytta partiklarna på ellipser baserade på "densitetsvågteori", som teoretiserar att armarna i en galax är områden med högre densitet men i konstant flöde, som en trafikstockning. Det verkar stabilt och fast, men stjärnorna rör sig faktiskt in och ut ur armarna när de rör sig längs sina respektive ellipser. I vårt system finns partiklarna aldrig på PROCESSORn – vi genererar korten och orienterar dem alla på GPU:n, så hela systemet är helt enkelt initialt tillstånd + tid. Den fortskred så här:

Progression av partikelsystem med GPU-återgivning

Progression av partikelsystem med GPU-återgivning

När tillräckligt många ellipser har lagts till och är inställda på att rotera, började galaxerna bilda "armar" där stjärnornas rörelse konvergerar. Stjärnornas avstånd längs varje elliptisk väg fick viss slumpmässighet, och varje star fick lite positions slumpmässighet tillagd. Detta skapade en mycket mer naturlig fördelning av star rörelse och armform. Slutligen lade vi till möjligheten att köra färg baserat på avståndet från mitten.

Skapa stjärnornas rörelse

För att animera den allmänna star rörelse behövde vi lägga till en konstant vinkel för varje ram och för att få stjärnor att röra sig längs sina ellipser med konstant radiell hastighet. Detta är den främsta orsaken till att använda curveOffset. Detta är inte tekniskt korrekt eftersom stjärnor kommer att röra sig snabbare längs de långa sidorna av ellipserna, men den allmänna rörelsen kändes bra.

Stjärnor rör sig snabbare på den långa bågen, långsammare på kanterna.

Stjärnor rör sig snabbare på den långa bågen, långsammare på kanterna.

Med det beskrivs varje star fullständigt av (curveOffset, ellipsSize, elevation, Age) där Ålder är en ackumulering av den totala tid som har passerat sedan scenen lästes in.

float3 ComputeStarPosition(StarDescriptor star)
{

  float curveOffset = star.curveOffset + Age;
  
  // this will be coded as a “sincos” on the hardware which will compute both sides
  float x = cos(curveOffset) * star.xRadii;
  float z = sin(curveOffset) * star.zRadii;
   
  return float3(x, star.elevation, z);
  
}

Detta gjorde det möjligt för oss att generera tiotusentals stjärnor en gång i början av programmet, sedan animerade vi en enda uppsättning stjärnor längs de etablerade kurvorna. Eftersom allt finns på GPU:n kan systemet animera alla stjärnor parallellt utan kostnad för processorn.

Så här ser det ut när du ritar vita fyrhjulingar.

Så här ser det ut när du ritar vita fyrhjulingar.

För att göra varje quad ansikte kameran, använde vi en geometri skuggning för att omvandla varje star position till en 2D rektangel på skärmen som kommer att innehålla vår star textur.

Diamanter i stället för fyrhjulingar.

Diamanter i stället för fyrhjulingar.

Eftersom vi ville begränsa övertrassering (antal gånger en pixel bearbetas) så mycket som möjligt roterade vi våra quads så att de skulle ha mindre överlappning.

Lägga till moln

Det finns många sätt att få en volymtrisk känsla med partiklar – från ray marching inuti en volym till att rita så många partiklar som möjligt för att simulera ett moln. Realtid ray marsch skulle bli för dyrt och svårt att skriva, så vi först försökte bygga en bedragare system med hjälp av en metod för att återge skogar i spel- med en hel del 2D-bilder av träd mot kameran. När vi gör detta i ett spel kan vi ha texturer av träd renderade från en kamera som roterar runt, sparar alla dessa bilder och vid körning för varje skyltkort väljer du den bild som matchar visningsriktningen. Detta fungerar inte lika bra när bilderna är hologram. Skillnaden mellan det vänstra ögat och det högra ögat gör det så att vi behöver en mycket högre upplösning, annars ser det bara platt, alias eller repetitivt ut.

På vårt andra försök försökte vi ha så många partiklar som möjligt. De bästa visuella objekten uppnåddes när vi additivt ritade partiklar och suddade ut dem innan vi lade till dem i scenen. De typiska problemen med den metoden var relaterade till hur många partiklar vi kunde rita vid en enda tidpunkt och hur mycket skärmområde de täckte samtidigt som de behöll 60fps. Att sudda ut den resulterande bilden för att få den här molnkänslan var vanligtvis en mycket kostsam åtgärd.

Utan textur skulle molnen se ut så här med 2 % opacitet.

Utan textur skulle molnen se ut så här med 2 % opacitet.

Att vara additiv och ha många av dem innebär att vi skulle ha flera fyrhjulingar ovanpå varandra och upprepade gånger skugga samma pixel. I mitten av galaxen har samma pixel hundratals fyrhjulingar ovanpå varandra och detta hade en enorm kostnad när det gjordes på helskärmsläge.

Att göra helskärmsmoln och försöka sudda ut dem hade varit en dålig idé, så istället bestämde vi oss för att låta maskinvaran göra jobbet åt oss.

Lite kontext först

När du använder texturer i ett spel kommer texturstorleken sällan att matcha det område som vi vill använda det i, men vi kan använda olika typer av texturfiltrering för att få grafikkortet att interpolera den färg vi vill ha från texturens bildpunkter (texturfiltrering). Filtreringen som intresserar oss är bilinearfiltrering som beräknar värdet för valfri pixel med hjälp av de 4 närmaste grannarna.

Original före filtrering

Resultat efter filtrering

Med den här egenskapen ser vi att varje gång vi försöker rita en struktur till ett område som är dubbelt så stort suddas resultatet ut.

Istället för att rendera till en helskärm och förlora de dyrbara millisekunderna vi kan spendera på något annat, renderar vi till en liten version av skärmen. Sedan, genom att kopiera den här strukturen och sträcka ut den med en faktor 2 flera gånger, kommer vi tillbaka till helskärmsläge samtidigt som innehållet i processen suddas ut.

x3 uppskalning tillbaka till full upplösning.

x3 uppskalning tillbaka till full upplösning.

Detta gjorde det möjligt för oss att hämta molndelen med bara en bråkdel av den ursprungliga kostnaden. I stället för att lägga till moln i den fullständiga upplösningen målar vi bara 1/64 bildpunkter och sträcker bara ut texturen till full upplösning.

Vänster, med en uppskalning från 1/8: e till fullständig upplösning; och höger, med 3 uppskalning med kraften 2.

Vänster, med en uppskalning från 1/8: e till fullständig upplösning; och höger, med 3 uppskalning med kraften 2.

Observera att försök att gå från 1/64: e av storleken till full storlek på en go skulle se helt annorlunda ut, eftersom grafikkortet fortfarande skulle använda 4 bildpunkter i vår konfiguration för att skugga ett större område och artefakter börjar visas.

Sedan, om vi lägger till full upplösning stjärnor med mindre kort, får vi hela galaxen:

Nästan slutresultatet av galaxrendering med hjälp av stjärnor med full upplösning

När vi var på rätt spår med formen lade vi till ett lager moln, bytte ut de tillfälliga prickarna med de vi målade i Photoshop och lade till ytterligare färg. Resultatet blev en Vintergatan Galaxy våra konst-och ingenjörsteam både kändes bra om och det uppfyllde våra mål att ha djup, volym och rörelse- allt utan att beskatta CPU.

Vår sista Vintergatan Galaxy i 3D.

Vår sista Vintergatan Galaxy i 3D.

Mer att utforska

Vi har öppen källkod för koden för Galaxy Explorer-appen och gjort den tillgänglig på GitHub för utvecklare att bygga vidare på.

Vill du veta mer om utvecklingsprocessen för Galaxy Explorer? Kolla in alla våra tidigare projektuppdateringar på Microsoft HoloLens YouTube-kanal.

Om författarna

Bild av Karim Luccin vid sitt skrivbord Karim Luccin är programvarutekniker och entusiast inom visuell information. Han var grafiktekniker för Galaxy Explorer.
Foto av konst bly Andy Zibits Andy Zibits är en konstledare och rymdentusiast som ledde 3D-modelleringsteamet för Galaxy Explorer och kämpade för ännu fler partiklar.

Se även