- A lua e o planeta gasoso do fundo do céu podem ter superfície, rotação e atmosfera representadas apenas com um disco circular e pixel shader, sem malha esférica real
- A UV sphere tradicional facilita o mapeamento de textura 2×1, mas também traz contorno serrilhado, perda na amostragem de mipmap, padrões repetidos, seam, distorção nos polos e limitações para o halo atmosférico
- A abordagem com disco reconstrói a posição da superfície esférica a partir do UV central e então cria, por pixel, UV esférico baseado em
asin, matriz de rotação e spin por deslocamento da coordenada u para parecer uma esfera
- O shading começa com iluminação Lambertiana usando a posição da superfície como normal; em superfícies rochosas aplica-se matriz TBN e normal map, enquanto a atmosfera aproxima brilho do halo e absorção por canal
- Texturas estáticas do Substance Designer e texturas dinâmicas renderizadas no Unreal podem ser tratadas da mesma forma, reduzindo o custo com texturas dinâmicas menores e ajuste de tiling
Objetivo visual dos planetas no skybox
- O skybox do projeto foca em criar um céu alienígena com uma lua animada e um planeta gasoso
- Os dois planetas giram, e o planeta gasoso também tem fluxos atmosféricos em movimento
- O movimento real seria sutil, mas a velocidade foi aumentada para efeito visual
- A direção de arte segue um estilo semirrealista (semi-realistic) inspirado na arte de ficção científica dos anos 1970 e 1980
Entrada de textura da superfície e restrições
- A superfície do planeta gasoso é gerada em tempo real com pixel shader e render-to-texture
- A textura da lua é criada no Substance Designer, permitindo testar vários visuais trocando a paleta de cores e a material seed
- O planeta gasoso dinâmico exige renderização da textura a cada frame, o que aumenta bastante o custo de desempenho
- Dobrar a resolução da textura quadruplica o custo de renderização
- Era necessária uma solução que funcionasse de forma consistente tanto com texturas estáticas do Substance Designer quanto com texturas renderizadas dinamicamente no Unreal
- Cubemap foi considerado no início, mas pode usar 6 vezes mais recursos do que uma textura quadrada e não é prático porque o Substance Designer não oferece suporte à geração de cubemap
Limitações observadas na UV sphere
- A UV sphere parece a escolha padrão porque facilita alinhar coordenadas com uma textura retangular 2×1
- Como só um lado da esfera é visto de cada vez, também seria possível usar uma textura quadrada tileável em vez de uma textura retangular única da esfera inteira
- Na prática, vários problemas aparecem ao mesmo tempo
- O contorno fica visivelmente serrilhado, exigindo mais subdivisões ou uma máscara no pixel shader para esconder isso
- Grande parte da superfície da esfera é vista de forma inclinada e acaba amostrando mipmaps mais baixos; em texturas dinâmicas, uma parcela considerável dos pixels gerados a cada frame vai para áreas quase invisíveis
- O tiling precisa aumentar em múltiplos inteiros para que o padrão completo se mantenha contínuo, e com apenas 2× a repetição já fica perceptível
- O tiling fracionário cria uma seam contínua do polo norte ao polo sul
- Nos polos surgem pinching de textura e distorção no mapeamento dos triângulos, lembrando a deformação de texturas tremendo como no PSX
- Como só a superfície pode ser desenhada, o halo atmosférico exige um modelo separado
- A UV sphere ainda pode servir para modelar planetas, mas para uso em skybox ela acaba exigindo muitas correções e hacks
Abordagem com disco circular e pixel shader
- Como os planetas do skybox são observados de longe e de um único ponto, não é obrigatório usar uma malha esférica 3D complexa
- Em vez disso, usa-se um disco poligonal circular preenchido, deixando o mapeamento de textura para o pixel shader
- O halo atmosférico também pode ser desenhado dentro da mesma malha
- A origem do UV do disco precisa estar exatamente no centro
- Considerando o raio do planeta como 1, as coordenadas UV precisam se estender além disso para haver espaço para desenhar a atmosfera
Reconstrução da superfície esférica
- O sistema de coordenadas usa o padrão left-handed, Y-up consistente com DirectX
- O Unreal Engine também é left-handed, mas usa Z como eixo vertical, então é preciso conferir as direções e o formato do normal map
- A equação da superfície mapeia a posição 2D do plano do disco para a posição 3D da superfície da esfera
- Primeiro, verifica-se se o pixel está dentro da superfície da esfera checando se o comprimento do vetor UV é menor que o raio
float CircleMask( float2 uv, float radius)
{
return length(uv) < radius? 1.0: 0.0;
}
- Esse método só funciona corretamente quando as coordenadas UV estão alinhadas ao centro da malha
- A posição local da superfície esférica reutiliza x e y do UV e reconstrói apenas z
float3 ReconstructSurface(float2 uv)
{
float zSquared = 1.0 - dot(uv, uv);
float z = sqrt(zSquared);
return float3(uv, z);
}
Geração de UV esférico
- O processo de mapear uma textura quadrada sobre a superfície esférica se divide em três etapas
- enrolar em um cilindro
- repetir o mesmo processo no eixo y
- deformar o resultado para uma forma circular
- A coordenada x da textura é proporcional ao ângulo de enrolamento no cilindro; calcula-se
arcsine(x) e depois remapeia-se [-π, π] para [0, 1]
- Usa-se a geratriz da largura local da esfera para aplicar pinching das coordenadas nos polos
float2 GenerateSphericalUV(float3 position)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2(0.5, 0.5);
return float2(uv);
}
Inclinação do eixo e rotação
- Para uma composição mais natural do planeta, o eixo planetário é inclinado com pitch e roll
- Em arte de ficção científica, os planetas costumam aparecer inclinados, o que deixa a composição mais dinâmica e permite mostrar calotas polares
- O yaw corresponde ao movimento de rotação e é tratado em uma etapa separada para evitar problemas de seam
- Em uma esfera por malha, normalmente o planeta inteiro é transformado por matriz, mas na abordagem com disco cada pixel define a posição da superfície, então basta usar uma matriz 3×3 simples contendo apenas a rotação
float3x3 CreateRotationMatrix(float pitch, float roll) {
float cosPitch = cos(pitch);
float sinPitch = sin(pitch);
float cosRoll = cos(roll);
float sinRoll = sin(roll);
return (float3x3)(
cosRoll, -sinRoll * cosPitch, sinRoll * sinPitch,
sinRoll, cosRoll * cosPitch, -cosRoll * sinPitch,
0.0, sinPitch, cosPitch
);
}
- O material editor do Unreal não oferece suporte a matrizes como tipo de dado, mas há maneiras de contornar isso
Tratamento de scale, seam e spin
- Ao alterar a escala da textura, a UV seam que antes ficava escondida nas bordas da textura original aparece
- Não é fácil eliminar a seam por completo, mas movê-la para trás já reduz bastante sua visibilidade
- A esfera é dividida em quadrantes com base no sinal da posição, e aplica-se um deslocamento em u nos quadrantes traseiros esquerdo e direito
float2 GenerateSphericalUV(float3 position, float spin, float scale)
{
float leftRightSign = sign(position.x);
float frontBackSign = sign(position.z);
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * frontBackSign;
float2 generatrix = float2 (generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2 (spin, 0.5);
if(frontBackSign < 0.0)
{
uv = float2 (uv.x + 1.0 * leftRightSign, uv.y);
}
return float2 (uv / scale);
}
- A seam continua ligando o polo norte ao polo sul, mas fica deslocada para trás e muito menos visível
- A rotação é feita deslocando a textura da superfície na direção da coordenada u, sem girar a própria esfera
float2 GenerateSphericalUV(float3 position, float scale, float spin)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 * scale + float2(0.5 + spin, 0.5);
return float2(uv);
}
float2 sphericalVU = GenerateSphericalUV(position, scale, time*speed)
Shading da superfície e normal map
- Para o planeta parecer redondo, o shading é essencial
- Diferente do shading padrão de motores gráficos, o planeta tem duas camadas — superfície e atmosfera — então é preciso definir manualmente a iluminação e o blending entre elas
- Com raio 1, a posição da superfície antes da rotação pode ser usada diretamente como normal da superfície
- A iluminação usa um modelo Lambertiano simples
- O objetivo não é realismo total, mas uma estética estilizada de capa de ficção científica inspirada em fotos da NASA
float LambertianLight(float3 normal, float3 lightDirection) {
float NdotL = max(dot(normal, lightDirection), 0.0);
return NdotL;
}
- Superfícies lisas como a de um gigante gasoso podem funcionar bem só com a normal simples
- Em planetas rochosos, faz mais sentido simular relevo como montanhas, cristas e crateras com normal map
- Para usar normal map, é necessária uma matriz TBN formada por Tangent, Bitangent e Normal
- Em uma esfera, esse cálculo precisa ser feito por pixel
- A matriz TBN serve para converter a normal do normal map para World Space Coordinates
Descontinuidades de UV e artefatos de mipmap
- UVs gerados no pixel shader podem apresentar problema de descontinuidade (discontinuity)
- Na seam traseira, o componente horizontal do UV deveria conectar perfeitamente 0.0 e 1.0 na borda, mas na prática surgem artefatos blocados ao longo da seam
- Essa linha está relacionada ao DDX do UV
- DDX e DDY medem a taxa de variação do UV nos eixos do espaço de tela
- O sampler de textura usa esses valores para decidir qual mipmap deve ser usado
- Derivadas de UV menores correspondem a mipmaps de alta resolução; derivadas maiores, a mipmaps de baixa resolução
- Quando os valores saltam abruptamente de um lado para o outro da seam, o DDX cresce muito e a amostragem pode cair no mipmap mais baixo, aparecendo como uma linha cinza
- No caso do planeta, isso deve ficar escondido por uma polar patch, então não é um problema grave; mas, se ficar visível, é preciso corrigir manualmente DDX e DDY da seam e passá-los ao sampler
- O exemplo de correção serve apenas para explicar a ideia; na prática, as fórmulas precisam ser derivadas de acordo com o mapeamento usado
Polar patch e correção de pinching
- A distorção nos polos é coberta com uma polar patch, como em abordagens já consolidadas
- Em planetas e luas, os polos frequentemente são cobertos de gelo e se distinguem visualmente do resto, então uma textura separada ajuda a esconder o problema e ainda acrescenta interesse visual
- A polar patch pode ser adicionada mapeando-se em um plano perpendicular ao eixo de rotação e depois rotacionando as coordenadas resultantes
float2 PolarPatchMapping(float3 position, float scale, float spin)
{
float cosSpin = cos(spin);
float sinSpin = sin(spin);
float scale = 0.4;
float2 uv = float2(position.x, position.z) * scale;
float2 spinningUV = float2(uv.x * cosSpin - uv.y * sinSpin,
uv.x * sinSpin + uv.y * cosSpin);
return spinningUV;
}
- Como o mapa UV não fica armazenado nos vértices e sim gerado por pixel, há muito mais flexibilidade para corrigir o pinching nos polos
- Uma divisão simples permite corrigir o aspect ratio da textura polar
uv = float2(uv.x, uv.y/pow(width, 1/3));
- O resultado da correção pode parecer outro tipo de artefato, mas em uma cena realista ele se integra melhor à polar patch e reduz a inconsistência visual
Aproximação do halo atmosférico
- Como a superfície do planeta não ocupa toda a área do disco, o espaço restante pode ser usado para desenhar o halo atmosférico
- Desenhar o halo no mesmo shader permite integrá-lo naturalmente com a atmosfera sobre o planeta
- Renderização atmosférica fisicamente baseada normalmente usa Rayleigh scattering e raymarching, mas isso não é usado aqui
- Em vez disso, é adotada uma aproximação parecida com o truque de iluminação de objetos dinâmicos em Quake 1 e 2
- A iluminação estática em Quake era pré-calculada em lightmaps
- Monstros dinâmicos ajustavam a cor amostrando o lightmap abaixo deles; não era fisicamente correto, mas fazia com que combinassem com o ambiente
- Do mesmo modo, a superfície do planeta é usada para aproximar a iluminação do halo atmosférico
- A normal da superfície antes do bump mapping é expandida para calcular o brilho do halo
- Remapear separadamente o brilho de cada canal permite simular de forma simples a absorção de diferentes comprimentos de onda da luz
- Como a atmosfera aparece melhor em ângulos rasantes, o componente Z da normal da superfície é remapeado
- Também se calcula a distância até a superfície da esfera para o efeito de fade do halo
- No fim, superfície e atmosfera são combinadas com alpha blending
Resultado final e uso
- Embora tenha exigido mais etapas do que o esperado, o resultado final parece uma esfera completa e oferece suporte às manipulações desejadas de textura e shader
- Um único graph do Substance Designer pode gerar a textura da superfície, sem necessidade de pós-processamento adicional
- Texturas animadas também podem ser tratadas da mesma forma
- É possível usar texturas menores para economizar desempenho
- O tiling pode ser ajustado conforme necessário
- O vídeo final pode ser visto em Video 3
1 comentários
Comentários no Hacker News
Parece que o autor descartou cubemaps rápido demais; quando usei para renderizar planetas gasosos dinâmicos em um projeto pessoal, foi a solução mais simples
Usar um cubemap não aumenta a memória em 6 vezes; é como dividir uma grande textura retangular em 6 faces retangulares menores, então o nível total de detalhe da textura é o mesmo
A vantagem é não precisar se preocupar com o esmagamento nos polos, e ser fácil criar um campo de fluxo sem emendas para animação/distorção da textura com uma função de ruído 3D ou 4D
https://www.junkship.net/News/2016/06/09/jupiter-jazz
Cada hemisfério é projetado como um disco, mas a textura pode preencher até as bordas de um quadrado, e uma parte de cada hemisfério pode ser duplicada nas bordas da textura do outro lado
Há uma diferença de escala de 1:2 entre o centro e a borda do disco, então, pelo critério de detalhe mínimo, pode-se dizer que há desperdício de pixels; mas, por ser uma projeção conforme, ela torna muito menos complicada a forma de amostrar a cor do pixel de destino em perspectivas acentuadas, e os cálculos direto/inverso também exigem só 1 divisão e algumas somas e multiplicações por ponto projetado, ficando até mais baratos que a projeção central de um cubemap
Se você precisa de um método conforme que reduza ainda mais a variação de escala e o desperdício de pixels nas bordas sem ser conceitualmente difícil demais, dá para cobrir a esfera como duas peças de couro de uma bola de beisebol usando 2 projeções de Mercator perpendiculares entre si e levemente sobrepostas
Cada peça pode ser uma textura retangular, e há até trabalhos da NOAA que propõem essa abordagem para a malha de equações diferenciais em simulações meteorológicas da Terra
A projeção mais eficiente em pixels que conheço divide a esfera em um octaedro e cobre cada octante com uma grade de pixels hexagonais baseada em “coordenadas de área esférica”
Cada octante pode ser representado em uma imagem comum de pixels quadrados como meio quadrado, isto é, um triângulo retângulo 45-45-90, produzindo resultados como https://observablehq.com/@jrus/sac-quincuncial, e também dá para usar grades hexagonais como https://observablehq.com/@jrus/sphere-resample
Porém, os detalhes ao amostrar através das bordas ficam muito mais trabalhosos do que no método com 2 projeções estereográficas, e também podem surgir artefatos de emenda
Isso exige mais matemática, mas, se você não pretende subdividir mais a superfície por outros motivos, em geral não vale tanto a pena
O esmagamento da textura nos polos é, na verdade, uma manifestação extrema de uma distorção que existe em toda a superfície
Normalmente ela só fica evidente nos polos, mas, se você subdividir a esfera de forma adaptativa em triângulos, a distorção também muda quando o nível de subdivisão muda, então pode aparecer em outros lugares
O problema é que a esfera é dividida em quadriláteros, e cada quadrilátero é representado no espaço UV por 2 triângulos de mesma área, mas, no espaço 3D, um dos triângulos — especialmente aquele cuja aresta horizontal fica mais perto do polo — é menor
Ainda assim, as UVs são interpoladas linearmente dentro do triângulo, de modo que metade da textura é comprimida e metade é esticada
No polo, a área 3D de um dos triângulos efetivamente vira 0, então só metade da textura é renderizada, e a emenda entre os triângulos fica evidente
A solução correta é calcular as coordenadas UV por pixel no pixel shader, em vez de usar interpolação linear por vértice; feito corretamente, isso também trata os polos sem emendas
Por exemplo, ao renderizar uma esfera com um polo no “norte verdadeiro”, visto de lado usa-se o polo de renderização do lado do norte verdadeiro, e visto de perto do norte verdadeiro usa-se uma renderização equatorial em 0',0’
Fiquei com vontade de revisitar displacement mapping
Não seria um substituto para o problema que o autor está tentando resolver, mas é mais simples e bem divertido
Há cerca de 25 anos, fiz um visualizador musical chamado “Eclipse” para o SoundJam, cuja entrada era uma matriz de níveis na faixa de frequências audíveis e os canais esquerdo e direito
O objetivo era algo como um sol eclipsado com emissões de coroa, e os dados da música se tornavam essas “emissões”
A frequência dos dados determinava em que ponto ao redor do disco solar eles apareceriam
Com o passar do tempo, as emissões se afastavam do sol e “esfriavam” até desaparecer em preto; sinais fortes começavam em branco e, conforme enfraqueciam, passavam por amarelo, laranja, vermelho e marrom
Eu precisava manter um array de valores de dados de som em um buffer circular grande o suficiente para cobrir o tempo até as emissões desaparecerem em preto
O “Eclipse” inteiro e a exibição das emissões acabavam sendo um displacement map, e eu pré-calculava um bitmap em que cada valor de pixel era um offset no buffer de níveis sonoros
Pixels próximos à superfície do sol tinham o offset dos dados recém-chegados, enquanto pixels mais externos tinham offsets perto da cauda do buffer que estava prestes a expirar
Como era preciso mapear emissões circulares e radiais para coordenadas cartesianas, a geração dos valores de deslocamento exigia um pouco de matemática
O loop principal recebia novos valores de som, sobrescrevia os mais antigos e então percorria o displacement map por linhas e colunas, buscava os dados de som correspondentes, mapeava para cores de uma paleta fixa e colocava no buffer de exibição
Não era tão chamativo quanto outros visualizadores, mas tinha uma beleza serena e refletia muito bem os dados da música
Era diferente dos visualizadores posteriores, que pareciam não conseguir ficar parados nem quando havia silêncio
O artigo é ótimo, mas, conforme você rola para baixo, ele carrega muitos shaders, então, se o computador não for muito potente, o navegador pode quase travar
É interessante ver como as pessoas interpretam “realismo” de maneiras tão diferentes
Em jogos de uma época em que os recursos eram muito mais limitados, essa diferença era especialmente clara; alguns jogos, só com gráficos quadradões, pixelados e de paleta reduzida, conseguiam ser mais imersivos do que grandes jogos de alto orçamento de hoje
Acho que por isso a imersão acabava sendo mais ativa
Isso me lembra este texto sobre uma abordagem parecida: https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92...
GPUs ou bibliotecas 3D não têm alguma função que varra horizontalmente cada linha de um círculo e mapeie X,Y da textura para a posição “3D” na esfera?
Uma esfera é um círculo, e algoritmos para desenhar círculos são simples; girar e projetar um milhão de vértices para triângulos parece um enorme desperdício de recursos
Bastaria varrer horizontalmente, descer uma linha e continuar desenhando para obter todas as linhas
Lembro de fazer algo assim no fim dos anos 80 para pré-calcular coisas como a lente de aumento de Second Reality
Todo o pipeline de renderização já é feito para lidar com interpolação linear
Com um único programa de shader e 1 triângulo, já dá para desenhar uma esfera com antialiasing perfeito
Se for renderizar na GPU, apesar da complexidade aparente, a abordagem do texto parece bem boa
Há uma excelente série de textos sobre a estrutura da renderização baseada em GPU: https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-...
A parte 6 trata de rasterização
Planetas impostores 2D com pixel shader sobre o fundo ajudam muito em trabalhos com muitos planetas, como um universo procedural
Por causa da memória e da largura de banda da GPU, limito planetas baseados em cubo esférico a 1 instância e desenho os corpos celestes próximos com a técnica de fundo
No espaço, há uma distância, mais ou menos no meio entre dois corpos celestes, em que ambos são renderizados como impostores; depois disso, o corpo celeste mais próximo é tratado com uma quadtree esférica
Não é perfeito, mas a ilusão é quase irrepreensível
Além disso, como está em um plano, fica fácil adicionar efeitos físicos sutis de lente de câmera para galáxias ou corpos celestes distantes
Por exemplo, efeitos como a Lua parecer maior do que realmente é ao nascer, ou uma galáxia distante parecer distorcida pela gravidade
Gosto desta página relacionada a planetas gasosos: https://emildziewanowski.com/flowfields/
Icosferas podem ser desdobradas de forma muito mais suave, e as posições dos vértices e os tamanhos dos elementos também são regulares
O desdobramento não é trivial, mas é possível