Estudo de caso - Criando uma galáxia em realidade misturada

Antes de Microsoft HoloLens enviado, perguntamos à nossa comunidade de desenvolvedores que tipo de aplicativo eles gostariam de ver uma compilação de equipe interna experiente para o novo dispositivo. Mais de 5.000 ideias foram compartilhadas, e após uma pesquisa de 24 horas no Twitter, o vencedor foi uma ideia chamada Galaxy Explorer.

Andy Zibits, líder de arte do projeto, e Karim Luccin, engenheiro gráfico da equipe, 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

Nossa equipe - composta por dois designers, três desenvolvedores, quatro artistas, um produtor e um testador - teve seis semanas para construir um aplicativo totalmente funcional que permitiria que as pessoas aprendessem e explorassem a vastidão e a beleza da nossa Galáxia Via Láctea.

Queríamos aproveitar ao máximo a capacidade do HoloLens de renderizar objetos 3D diretamente em seu espaço de vida, então decidimos que queríamos criar uma galáxia realista onde as pessoas pudessem ampliar e ver estrelas individuais, cada uma em suas próprias trajetórias.

Na primeira semana de desenvolvimento, criamos algumas metas para nossa representação da Galáxia via láctea: ela precisava ter profundidade, movimento e sensação volumétrica— cheia de estrelas que ajudariam a criar a forma da galáxia.

O problema com a criação de uma galáxia animada que tinha bilhões de estrelas era que o grande número de elementos únicos que precisam de atualização seria muito grande por quadro para o HoloLens animar usando a CPU. Nossa solução envolveu uma complexa mistura de arte e ciência.

Nos bastidores

Para permitir que as pessoas explorassem estrelas individuais, nosso primeiro passo foi descobrir quantas partículas poderíamos renderizar ao mesmo tempo.

Renderizando partículas

As CPUs atuais são ótimas para processar tarefas serial e até algumas tarefas paralelas ao mesmo tempo (dependendo de quantos núcleos eles têm), mas as GPUs são muito mais eficazes no processamento de milhares de operações em paralelo. No entanto, como eles geralmente não compartilham a mesma memória que a CPU, a troca de dados entre a GPU da CPU<>pode se tornar rapidamente um gargalo. Nossa solução era fazer uma galáxia na GPU, e ela tinha que viver completamente na GPU.

Começamos testes de estresse com milhares de partículas de ponto em vários padrões. Isso nos permitiu colocar a galáxia no HoloLens para ver o que funcionou e o que não funcionou.

Criando a posição das estrelas

Um dos membros da nossa equipe já havia escrito o código C# que geraria estrelas em sua posição inicial. As estrelas estão em uma elipse e sua posição pode ser descrita por (curveOffset, ellipseSize, elevation) onde curveOffset é o ângulo da star ao longo da elipse, elipseSize é a dimensão da elipse ao longo de X e Z, e elevação da elevação adequada do star dentro da galáxia. Assim, podemos criar um buffer (ComputeBuffer do Unity) que seria inicializado com cada atributo star e enviá-lo na GPU onde ele viveria para o resto da experiência. Para desenhar esse buffer, usamos DrawProcedural do Unity , que permite executar um sombreador (código em uma GPU) em um 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 brutos com milhares de partículas. Isso nos deu a prova de que precisávamos que pudéssemos gerenciar muitas partículas e executá-la em velocidades de desempenho, mas não estávamos satisfeitos com a forma geral da galáxia. Para melhorar a forma, tentamos vários padrões e sistemas de partículas com rotação. Estes foram inicialmente promissores porque o número de partículas e desempenho permaneceu consistente, mas a forma quebrou perto do centro e as estrelas estavam emitindo externamente 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, cada vez mais perto do centro da galáxia.

Tentamos vários padrões e sistemas de partículas que giravam, como estes.

Tentamos vários padrões e sistemas de partículas que giravam, como estes.

Nossa equipe fez algumas pesquisas sobre 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", que teoriza que os braços de uma galáxia são áreas de maior densidade, mas em fluxo constante, como um engarrafamento. Parece estável e sólido, mas as estrelas estão realmente entrando e saindo dos braços enquanto se movem ao longo de suas respectivas reticências. Em nosso sistema, as partículas nunca existem na CPU— geramos os cartões e orientamos todos eles na GPU, portanto, todo o sistema é simplesmente estado inicial + tempo. Ele progrediu da seguinte maneira:

Progressão do sistema de partículas com renderização de GPU

Progressão do sistema de partículas com renderização de GPU

Uma vez que reticências suficientes são adicionadas e são definidas para girar, 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 recebeu alguma aleatoriedade, e cada star tem um pouco de aleatoriedade posicional adicionada. Isso criou uma distribuição muito mais natural de movimento star e forma do braço. Por fim, adicionamos a capacidade de conduzir a cor com base na distância do centro.

Criando o movimento das estrelas

Para animar o movimento geral star, precisávamos adicionar um ângulo constante para cada quadro e fazer com que as estrelas se movessem ao longo de suas reticências a uma velocidade radial constante. Esse é o principal motivo para usar curveOffset. Isso não está tecnicamente correto, pois as estrelas se moverão mais rápido ao longo dos lados longos das reticências, mas o movimento geral se sentiu bem.

As estrelas se movem mais rápido no arco longo, mais lento nas bordas.

As estrelas se movem mais rápido no arco longo, mais lento nas bordas.

Com isso, cada star é totalmente descrito por (curveOffset, elipseSize, elevação, Idade) em que Idade é um acúmulo do tempo total que se 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);
  
}

Isso nos permitiu gerar dezenas de milhares de estrelas uma vez no início do aplicativo, então animamos um único conjunto de estrelas ao longo das curvas estabelecidas. Como tudo está na GPU, o sistema pode animar todas as estrelas em paralelo sem custo para a CPU.

Aqui está a aparência ao desenhar quadriciclos brancos.

Aqui está a aparência ao desenhar quadriciclos brancos.

Para tornar cada quad face da câmera, usamos um sombreador de geometria para transformar cada star posição em um retângulo 2D na tela que conterá nossa textura star.

Diamantes em vez de quadriciclos.

Diamantes em vez de quadriciclos.

Como queríamos limitar o excesso dedraw (número de vezes que um pixel será processado) tanto quanto possível, giramos nossos quadriciclos para que eles tivessem menos sobreposição.

Adicionando nuvens

Há muitas maneiras de ter uma sensação volumétrica com partículas, desde raios que marcham dentro de um volume até o desenho do maior número possível de partículas para simular uma nuvem. A marcha de raios em tempo real seria muito cara e difícil de criar, então primeiro tentamos criar um sistema impostor usando um método para renderizar florestas em jogos, com muitas imagens 2D de árvores voltadas para a câmera. Quando fazemos isso em um jogo, podemos ter texturas de árvores renderizadas de uma câmera que gira, salva todas essas imagens e, em runtime para cada outdoor cartão, selecione a imagem que corresponde à direção do modo de exibição. Isso não funciona tão bem quando as imagens são hologramas. A diferença entre o olho esquerdo e o olho direito o torna para que precisemos de uma resolução muito maior, ou então parece plana, aliasa ou repetitiva.

Em nossa segunda tentativa, tentamos ter o maior número possível de partículas. Os melhores visuais foram obtidos quando desenhamos partículas aditivamente e as desfocamos antes de adicioná-las à cena. Os problemas típicos com essa abordagem estavam relacionados a quantas partículas poderíamos desenhar em um único momento e quanta área de tela eles cobriram enquanto ainda mantinham 60fps. Desfocar a imagem resultante para obter essa sensação de nuvem geralmente foi uma operação muito cara.

Sem textura, é assim que as nuvens seriam com opacidade de 2%.

Sem textura, é assim que as nuvens seriam com opacidade de 2%.

Ser aditivo e ter muitos deles significa que teríamos vários quadriciclos em cima um do outro, sombreando repetidamente o mesmo pixel. No centro da galáxia, o mesmo pixel tem centenas de quadras em cima um do outro e isso teve um custo enorme ao ser feito em tela inteira.

Fazer nuvens em tela inteira e tentar desfocá-las teria sido uma má ideia, então, em vez disso, decidimos deixar o hardware fazer o trabalho para nós.

Um pouco de contexto primeiro

Ao usar texturas em um jogo, o tamanho da textura raramente corresponderá à área em que queremos usá-la, mas podemos usar um tipo diferente de filtragem de textura para obter o cartão gráfico para interpolar a cor desejada dos pixels da textura (Filtragem de Textura). A filtragem que nos interessa é a filtragem bilinear que calculará o valor de qualquer pixel usando os quatro vizinhos mais próximos.

Original antes da filtragem

Resultado após a filtragem

Usando essa propriedade, vemos que cada vez que tentamos desenhar uma textura em uma área duas vezes maior, ela desfoca o resultado.

Em vez de renderizar em uma tela inteira e perder esses preciosos milissegundos que poderíamos estar gastando em outra coisa, renderizamos para uma versão minúscula da tela. Em seguida, copiando essa textura e esticando-a por um fator de 2 várias vezes, voltamos para a tela inteira enquanto desfocamos o conteúdo no processo.

escala de upscale x3 de volta à resolução total.

escala de upscale x3 de volta à resolução total.

Isso nos permitiu obter a parte de nuvem com apenas uma fração do custo original. Em vez de adicionar nuvens na resolução completa, pintamos apenas 1/64 dos pixels e apenas ampliamos a textura de volta à resolução total.

À esquerda, com um upscale de 1/8 a resolução completa; e à direita, com 3 upscale usando a potência de 2.

À esquerda, com um upscale de 1/8 a resolução completa; e à direita, com 3 upscale usando a potência de 2.

Observe que tentar ir de 1/64 do tamanho para o tamanho total de uma só vez seria completamente diferente, pois o gráfico cartão ainda usaria 4 pixels em nossa configuração para sombrear uma área maior e artefatos começam a aparecer.

Então, se adicionarmos estrelas de resolução total com cartas menores, teremos a galáxia completa:

Resultado quase final da renderização de galáxias usando estrelas de resolução completa

Uma vez que estávamos no caminho certo com a forma, adicionamos uma camada de nuvens, trocamos os pontos temporários com os que pintamos no Photoshop e adicionamos alguma cor adicional. O resultado foi uma Galáxia via láctea que nossas equipes de arte e engenharia se sentiram bem e atingiram nossas metas de ter profundidade, volume e movimento, tudo sem tributar a CPU.

Nossa galáxia via láctea final em 3D.

Nossa galáxia via láctea final em 3D.

Mais para explorar

Disponibilizamos o código de software livre para o aplicativo galaxy Explorer e o disponibilizamos no GitHub para os desenvolvedores criarem.

Interessado em saber mais sobre o processo de desenvolvimento do Galaxy Explorer? Confira todas as atualizações de nosso projeto anteriores no canal Microsoft HoloLens do YouTube.

Sobre os autores

Foto de Karim Luccin em sua mesa Karim Luccin é engenheiro de software e entusiasta de visuais sofisticados. Ele era o Engenheiro gráfico da Galaxy Explorer.
Foto do líder de arte Andy Zibits Andy Zibits é um líder de arte e entusiasta do espaço que gerenciou a equipe de modelagem 3D para o Galaxy Explorer e lutou por ainda mais partículas.

Confira também