Caso prático - Criar uma galáxia na realidade mista
Antes de Microsoft HoloLens enviadas, perguntámos à nossa comunidade de programadores que tipo de aplicação gostariam de ver uma compilação de equipa interna experiente para o novo dispositivo. Mais de 5000 ideias foram partilhadas, e depois de uma sondagem de 24 horas no Twitter, o vencedor foi uma ideia chamada Galaxy Explorer.
Andy Zibits, o líder artístico do projeto, e Karim Luccin, o engenheiro gráfico da equipa, falam sobre o esforço colaborativo entre arte e engenharia que levou à criação de uma representação precisa e interativa da galáxia Via Láctea no Galaxy Explorer.
A Tecnologia
A nossa equipa - composta por dois designers, três programadores, quatro artistas, um produtor e um tester - teve seis semanas para criar uma aplicação totalmente funcional que permitiria que as pessoas aprendessem e explorassem a vastidão e a beleza da nossa Galáxia da Via Láctea.
Queríamos tirar o máximo partido da capacidade do HoloLens de compor objetos 3D diretamente no seu espaço de vida, por isso decidimos criar uma galáxia realista onde as pessoas seriam capazes de ampliar e ver estrelas individuais, cada uma nas suas próprias trajectórias.
Na primeira semana de desenvolvimento, criámos alguns objetivos para a nossa representação da Galáxia da Via Láctea: precisava de ter profundidade, movimento e sentir-se volumétrica— cheia de estrelas que ajudassem a criar a forma da galáxia.
O problema com a criação de uma galáxia animada que tinha milhares de milhões de estrelas era que o número total de elementos únicos que precisavam de ser atualizados seria demasiado grande por frame para o HoloLens animar com a CPU. A nossa solução envolveu uma mistura complexa de arte e ciência.
Nos bastidores
Para permitir que as pessoas explorem estrelas individuais, o nosso primeiro passo foi descobrir quantas partículas poderíamos compor ao mesmo tempo.
Composição de partículas
As CPUs atuais são ótimas para processar tarefas em série e até algumas tarefas paralelas ao mesmo tempo (dependendo do número de núcleos que têm), mas as GPUs são muito mais eficazes no processamento de milhares de operações em paralelo. No entanto, como normalmente não partilham a mesma memória que a CPU, a troca de dados entre a GPU da CPU<>pode tornar-se rapidamente um estrangulamento. A nossa solução era fazer uma galáxia na GPU, e tinha de viver completamente na GPU.
Iniciámos testes de stress com milhares de partículas de ponto em vários padrões. Isto permitiu-nos obter a galáxia no HoloLens para ver o que funcionou e o que não funcionou.
Criar a posição das estrelas
Um dos membros da nossa equipa já tinha escrito o código C# que geraria estrelas na sua posição inicial. As estrelas estão numa reticência e a sua posição pode ser descrita por (curveOffset, ellipseSize, elevation) onde curveOffset é o ângulo da star ao longo das reticências, reticênciasSize é a dimensão das reticências ao longo de X e Z, e elevação adequada da star dentro da galáxia. Assim, podemos criar uma memória intermédia (ComputeBuffer do Unity) que seria inicializada com cada atributo star e enviá-la na GPU onde viveria para o resto da experiência. Para desenhar esta memória intermédia, utilizamos o DrawProcedural do Unity que permite executar um sombreador (código numa GPU) num conjunto arbitrário de pontos sem ter uma malha real que represente a galáxia:
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];
…
}
Começamos com padrões circulares crus com milhares de partículas. Isto deu-nos a prova de que precisávamos de gerir muitas partículas e executá-la a velocidades de desempenho, mas não estávamos satisfeitos com a forma geral da galáxia. Para melhorar a forma, tentámos vários padrões e sistemas de partículas com rotação. Estas eram inicialmente promissoras porque o número de partículas e desempenho manteve-se consistente, mas a forma avariou-se perto do centro e as estrelas emitiam exteriormente o que não era realista. Precisávamos de uma emissão que nos permitisse manipular o tempo e fazer com que as partículas se movessem realisticamente, aproximando-se cada vez mais do centro da galáxia.
Tentámos vários padrões e sistemas de partículas que giravam, como estes.
A nossa equipa fez alguma pesquisa sobre a forma como as galáxias funcionam e fizemos um sistema de partículas personalizado especificamente para a galáxia para que pudéssemos mover as partículas em reticências com base na "teoria das ondas de densidade", o que teoriza que os braços de uma galáxia são áreas de maior densidade, mas em constante fluxo, como um engarrafamento. Parece estável e sólido, mas as estrelas estão realmente a entrar e a sair dos braços à medida que se movem ao longo das respetivas reticências. No nosso sistema, as partículas nunca existem na CPU— geramos os cartões e orientamo-los todos na GPU, pelo que todo o sistema é simplesmente o estado inicial + hora. Progrediu desta forma:
Progressão do sistema de partículas com composição de GPU
Assim que as reticências suficientes são adicionadas e estão definidas para rodar, as galáxias começaram a formar "braços" onde o movimento das estrelas converge. O espaçamento das estrelas ao longo de cada caminho elíptico foi dado alguma aleatoriedade, e cada star foi adicionado um pouco de aleatoriedade posicional. Isto criou uma distribuição muito mais natural de movimento star e forma do braço. Por fim, adicionámos a capacidade de conduzir a cor com base na distância do centro.
Criar o movimento das estrelas
Para animar o movimento geral star, precisávamos de adicionar um ângulo constante para cada moldura e fazer com que as estrelas se movessem ao longo das reticências a uma velocidade radial constante. Este é o principal motivo para utilizar curveOffset. Isto não está tecnicamente correto, pois as estrelas vão mover-se mais rápido ao longo dos lados longos das reticências, mas o movimento geral sentiu-se bem.
As estrelas movem-se mais depressa no arco longo, mais lentas nas bordas.
Com isso, cada star é totalmente descrito por (curveOffset, ellipseSize, elevation, Age) onde Age é uma acumulação do tempo total que passou desde que a cena foi carregada.
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);
}
Isto permitiu-nos gerar dezenas de milhares de estrelas uma vez no início da aplicação, depois animamos um único conjunto de estrelas ao longo das curvas estabelecidas. Uma vez que tudo está na GPU, o sistema pode animar todas as estrelas em paralelo sem custos para a CPU.
Eis o que parece ao desenhar quads brancos.
Para tornar cada quad face à câmara, utilizámos um sombreado de geometria para transformar cada star posição num retângulo 2D no ecrã que conterá a nossa textura star.
Diamantes em vez de quads.
Como queríamos limitar a sobrecarga (número de vezes que um pixel será processado) tanto quanto possível, rodámos os nossos quads para que tivessem menos sobreposição.
Adicionar clouds
Existem muitas formas de obter uma sensação volumétrica com partículas, desde a marcha de raios dentro de um volume até ao desenho do maior número possível de partículas para simular uma nuvem. A marcha de raios em tempo real seria demasiado cara e difícil de criar, por isso tentámos criar um sistema impostor utilizando um método para compor florestas em jogos , com muitas imagens 2D de árvores viradas para a câmara. Quando fazemos isto num jogo, podemos ter texturas de árvores compostas a partir de uma câmara que roda, guarda todas essas imagens e, em runtime para cada cartão de outdoor, selecione a imagem que corresponde à direção da vista. Isto não funciona tão bem quando as imagens são hologramas. A diferença entre o olho esquerdo e o olho direito faz com que precisemos de uma resolução muito maior, ou então parece simples, aliasado ou repetitivo.
Na nossa segunda tentativa, tentámos ter o maior número possível de partículas. Os melhores elementos visuais foram alcançados quando atraiu partículas de forma aditiva e as desfocou antes de as adicionar à cena. Os problemas típicos com essa abordagem estavam relacionados com quantas partículas poderíamos desenhar de uma só vez e a quantidade de área de ecrã que cobriam enquanto mantinham 60fps. Desfocar a imagem resultante para obter esta sensação de cloud foi geralmente uma operação muito dispendiosa.
Sem textura, é assim que as nuvens seriam com 2% de opacidade.
Ser aditivo e ter muitos deles significa que teríamos vários quads em cima uns dos outros, sombreando repetidamente o mesmo pixel. No centro da galáxia, o mesmo pixel tem centenas de quads em cima uns dos outros e isso teve um enorme custo ao ser feito em ecrã inteiro.
Fazer nuvens em ecrã inteiro e tentar desfocá-las teria sido uma má ideia, por isso decidimos deixar o hardware fazer o trabalho por nós.
Um pouco de contexto primeiro
Ao utilizar texturas num jogo, o tamanho da textura raramente corresponderá à área em que pretendemos utilizá-la, mas podemos utilizar diferentes tipos de filtragem de textura para fazer com que a placa gráfica interpole a cor que queremos dos pixéis da textura (Filtragem de Textura). A filtragem que nos interessa é a filtragem bilinear que calculará o valor de qualquer pixel com os 4 vizinhos mais próximos.
Com esta propriedade, vemos que cada vez que tentamos desenhar uma textura numa área duas vezes maior, desfoca o resultado.
Em vez de compormos num ecrã inteiro e perdermos esses preciosos milissegundos, poderíamos estar a gastar noutra coisa, compõemos uma pequena versão do ecrã. Em seguida, ao copiar esta textura e esticá-la por um fator de 2 várias vezes, voltamos ao ecrã inteiro enquanto desfocamos o conteúdo no processo.
x3 upscale back to full resolution.
Isto permitiu-nos obter a parte da cloud com apenas uma fração do custo original. Em vez de adicionar nuvens na resolução completa, só pintamos 1/64 dos píxeis e apenas estendemos a textura de volta à resolução completa.
Esquerda, com uma escala de 1/8 para resolução completa; e à direita, com 3 upscale com potência de 2.
Tenha em atenção que tentar passar de 1/64 do tamanho para o tamanho total de uma só vez teria um aspeto completamente diferente, uma vez que a placa gráfica ainda utilizaria 4 píxeis na nossa configuração para ensombrar uma área maior e os artefactos começarem a aparecer.
Em seguida, se adicionarmos estrelas de resolução completa com cartões mais pequenos, obteremos a galáxia completa:
Assim que estávamos no caminho certo com a forma, adicionámos uma camada de nuvens, trocámos os pontos temporários com os que pintámos no Photoshop e adicionámos alguma cor adicional. O resultado foi uma Galáxia da Via Láctea que as nossas equipas de arte e engenharia se sentiram bem e cumpriu os nossos objetivos de ter profundidade, volume e movimento, tudo sem tributar a CPU.
A nossa última Galáxia da Via Láctea em 3D.
Mais para explorar
Abrimos o código para a aplicação Galaxy Explorer e disponibilizámo-lo no GitHub para os programadores criarem.
Está interessado em saber mais sobre o processo de desenvolvimento do Galaxy Explorer? Veja todas as atualizações do nosso projeto anterior no canal do YouTube Microsoft HoloLens.
Sobre os autores
Karim Luccin é engenheiro de software e entusiasta de elementos visuais chiques. Era Engenheiro Gráfico do Galaxy Explorer. | |
Andy Zibits é um entusiasta da Arte e do espaço que geriu a equipa de modelação 3D para o Galaxy Explorer e lutou por ainda mais partículas. |