No Blog dos Desenvolvedores desta semana, veremos com atenção quais foram os desafios enfrentados pelos nossos determinados artistas gráficos para fazer com que o conteúdo de 15 anos de desenvolvimento do RuneScape ficasse com um belo visual e bom desempenho em um número sem precedentes de hardware, usando uma série de técnicas de renderização exclusivas, tanto antigas quanto novas.
Se você é um aspirante a designer gráfico ou fissurado em tecnologia, esta edição é especial para você – continue lendo!
Até agora, as edições anteriores do Blog dos Desenvolvedores só falaram de forma superficial dos novos recursos que tornam o novo cliente do RuneScape tão incrível. Esta edição tratará com mais detalhes desses recursos e de como são implementados, incluindo uma discussão sobre o motivo por trás da escolha de determinados recursos.
Um dos maiores desafios durante o desenvolvimento do NXT foi aumentar a fidelidade visual e o desempenho do novo cliente, garantindo ao mesmo tempo que o jogo não perdesse a semelhança com o RuneScape que todos adoram. E foi assim que conseguimos superá-lo.
Iluminação global
Iluminação global (GI) é o sistema que determina como os jogos e os filmes posicionam a iluminação indireta em uma cena (ou seja, reflexos). Sem iluminação indireta, todos os pixels de uma sombra estariam pretos.
Sem iluminação global
Isso é extremamente difícil de se resolver mantendo o jogo interativo e, mesmo nos dias de hoje, a maioria dos jogos usa um algoritmo de pré-processamento off-line para integrar informações geográficas em mapas de textura (ou seja, mapas de iluminação), possibilitando assim a consulta durante a execução.
A técnica clássica usada em jogos de gerações anteriores (por exemplo, Quake e Half-Life) era a radiosidade pré-integrada, mas jogos mais recentes começaram a integrar dados mais detalhados de informações geográficas, como harmônicas esféricas e visibilidade de superfície a superfície, o que permite que as informações geográficas funcionem com mapas de vetores normais de luzes em movimento.
Na vanguarda, soluções de renderização em tempo real começaram a aparecer (volumes de propagação de luz e rastreamento cônico de voxel), mas essas técnicas ainda estão em maturação e exigem tecnologias de GPU de ponta para funcionar efetivamente.
Isso é algo que realmente queremos melhorar no novo cliente, mas devido a limitações de ferramentas e o enorme tamanho do RuneScape, uma solução de informações geográficas off-line não é uma solução viável. Além disso, queríamos algo com suporte para todo o hardware que estamos visando.
Assim, demos uma olhada nas nossas ferramentas gráficas e optamos por uma versão moderna da iluminação de hemisfério, usando mapas de ambiente de irradiação por meio de harmônicas esféricas.
A iluminação de hemisfério usa uma esfera posicionada manualmente no chão de cada ambiente para definir um gradiente de cor do céu ao chão. Os vetores normais de superfície então são usados para selecionar uma cor dessa esfera. Por exemplo, se uma superfície estiver apontando para o céu, a cor da parte de cima da esfera será escolhida, e vice-versa.
Demoraria muito para pedir que os artistas criassem todas essas esferas, além de representar um custo de manutenção adicional. Portanto, optamos por uma solução programática em tempo real.
Isso envolve a renderização de uma sonda de luz (mapa de ambiente global) no alto do céu ao longo de vários quadros depois que cada quadrado do mapa é carregado, integrando harmônicas esféricas a partir dessa sonda em tempo real, o que nos proporciona um mapa de ambiente de irradiação altamente compactado na forma de coeficientes harmônicos esféricos.
Esses coeficientes então são usados pelo shader de pixel com alguns cálculos matemáticos engenhosos, que usam o vetor normal de superfície para nos dar a irradiação em cada pixel. No final das contas, o que isso produz é uma única reflexão indireta da luz do sol (iluminação de irradiação) - ou, como gostamos de dizer, iluminação de hemisfério turbinada!
Além da iluminação de irradiação, também adicionamos oclusão de ambiente à iluminação. A oclusão de ambiente simula as sombras pequenas e suaves da iluminação do ambiente com base na visibilidade da superfície. Escolhemos uma forma de oclusão de ambiente em espaço de tela (SSAO, em inglês) chamada oclusão de ambiente baseada no horizonte, que é praticamente a melhor que há.
Porém, ao contrário da maioria dos jogos que ainda aplica SSAO como pós-processamento (o que pode gerar resultados ruins e não convincentes), nós aplicamos nossa oclusão de ambiente durante a passagem de iluminação avançada apenas na iluminação ambiente indireta. Assim, os pixels que estão iluminados diretamente não exibem muita oclusão de ambiente, criando uma cena mais realista. Também executamos nossa passagem de SSAO na resolução máxima, proporcionando resultados mais estáveis.
As seguintes capturas de tela mostram nossas soluções de irradiação e oclusão de ambiente combinadas. Esperamos que você concorde que os resultados representam uma melhoria significativa em relação ao antigo cliente Java.
Iluminação global estilo Java
Nova iluminação global do NXT
Iluminação global estilo Java
Nova iluminação global do NXT
HDR, Renderização com Correção de Gama e Mapeamento de Tons
Outro motivo que faz com que o cliente Java fique tão sem graça é que ele não pode exibir toda a amplitude de cores e intensidades de luzes da cena. Para melhorar essa situação, precisamos primeiro nos certificar de que a placa de vídeo está realizando todos os cálculos de luz no espaço linear.
Todas as placas de vídeo realizam seus cálculos usando operações matemáticas de alta precisão em ponto flutuante, mas para tirar o maior proveito disso, os números que entram nas equações de iluminação durante a execução do shader também devem estar em espaço linear. Portanto, precisávamos nos certificar de que as texturas do jogo - que são armazenadas em sRGB quando salvas no Photoshop - fossem convertidas para espaço linear antes de serem usadas pelos shaders.
O mesmo processo também é aplicado em outros parâmetros definidos pelos artistas, como as cores de luz e neblina. A maioria das placas de vídeo é capaz de fazer a conversão de sRGB para espaço linear no hardware, mas quando esse recurso não estiver disponível, também temos uma forma de fazer isso manualmente. Isso assegura que a alta faixa dinâmica da iluminação não seja poluída por dados não lineares e que a iluminação da cena permaneça consistente. Isso também ajuda a evitar que as áreas muito iluminadas fiquem totalmente brancas e percam os detalhes.
O próximo componente exigido para obter uma renderização completa em HDR (alta faixa dinâmica) é assegurar que estamos armazenando os resultados desses cálculos de iluminação em espaço linear nas texturas fora da tela, que podem elas mesmas preservar a linearidade usando formatos de ponto flutuante. Porém, texturas em ponto flutuante podem exigir muitos recursos, então tentamos sempre usar um formato compactado quando um deles estiver disponível.
A cereja do bolo é o mapeamento de tons, que é um processo que mapeia uma faixa de cores em outra. No nosso caso, isso significa converter os resultados de iluminação linear em HDR para uma faixa que o monitor consiga exibir, pois os monitores só são capazes de exibir valores de baixa faixa dinâmica (LDR).
Sem o mapeamento de tons, uma conversão simples de faixa dinâmica alta para baixa resultaria em uma perda significativa de informações de iluminação, criando uma aparência indesejável. Portanto, tivemos que trabalhar incansavelmente com os artistas para avaliar as diferentes fórmulas de mapeamento de tons, para alcançar um resultado que combinasse bem com a aparência atual do RuneScape e preservasse uma boa faixa dinâmica de intensidades de cor e luz. Isso foi alcançado com o mapeamento de tons fílmico.
Sombras em Tempo Real
Na maioria dos jogos que são capazes de calcular a iluminação global off-line, as sombras projetadas em uma geometria de cena estática a partir de fontes de luz dominantes (luz do sol) geralmente não são incluídas neste processo de integração de luz (as sombras fazem parte da iluminação global, mas não vamos nos ater a isso aqui). Mais uma vez, isso não era possível para nós, então optamos por uma solução totalmente dinâmica. O desafio de desenvolver qualquer sistema de sombras totalmente dinâmico em tempo real é atingir um bom equilíbrio entre qualidade e desempenho.
Para ter tanto qualidade quanto desempenho, a melhor técnica para renderizar sombras em tempo real em placas de vídeo modernas é baseada em mapeamento de sombras. Porém, há dois grandes problemas com qualquer algoritmo de mapeamento de sombras: suavização projetiva e de perspectiva, principalmente devido à falta de resolução no mapa de sombras. Sem resolução suficiente, vários texels do mapa de sombra podem corresponder a um único pixel na tela, gerando uma série de problemas de suavização.
Optamos por um esquema de mapa de sombras em cascata dividido em paralelo, onde a cena visível é segmentada e cada segmento pertence a uma única cascata de sombra, nos dando assim uma proporção de pixels maior entre o mapa de texels de sombra e o espaço da tela. Isso melhora muito a suavização de perspectiva que ficaria visível se fosse usado apenas um mapa de sombra para a cena inteira. Porém, o lado negativo é que a cena precisa ser renderizada de novo para cada cascata do mapa de sombra, o que aumenta muito o número de objetos desenhados a cada quadro e pode piorar muito o desempenho do cliente.
Precisávamos de um ataque em várias frentes para aliviar a explosão no número de draw calls. Há vários níveis de remoção de visibilidade aplicados a cada passagem de renderização de cascata de sombra, incluindo aqueles baseados na área de visão, na distância, na área do mapa de sombra e no volume de projeção de sombra. Além disso, podemos remover a visibilidade de mais objetos fazendo diversas suposições sobre a cena.
Por exemplo, não renderizamos as partes rasas do terreno no mapa de sombras, já que é improvável que projetem sombras na cena, e só atualizamos as cascatas distantes em quadros alternados, reduzindo assim o número médio de draw calls.Além disso, para reduzir ainda mais o problema de suavização do mapa de sombra, para as cascatas distantes abrangerem uma parte maior da cena visível, usamos um algoritmo geralmente conhecido como "corte de cubo unitário", que tenta encaixar a projeção ortográfica do mapa de sombra nos projetores/recebedores visíveis. Isso pode melhorar o uso do mapa de sombra significativamente em muitas cenas.
Precisamos nos assegurar de que cada draw call use a menor quantidade de recursos possível. Para fazer isso, vários truques são utilizados: a desativação da gravação de cores, shaders de vértice reduzidos, shaders de pixels nulos e formatos de vértice mínimo. Isso tudo ajuda a minimizar a utilização da placa de vídeo durante a geração do mapa de sombras.
O último elemento em termos de qualidade e desempenho para qualquer técnica de mapeamento de sombra é a filtragem usada para produzir sombras com bordas suaves. Tiramos o melhor proveito da filtragem PCF para mapas de sombra da placa de vídeo, o que resulta em sombras com interpolação suave. Isso - juntamente com kernels de filtro multi-tap e funções de busca de textura especiais para reduzir o uso do registro para fins gerais - nos permite obter sombras com bordas suaves e alto desempenho.
Se você chegou até aqui, provavelmente compreende como é uma verdadeira arte Jedi renderizar mapas de sombra em tempo real, mas os resultados valem a pena!
Iluminação Diferida Indexada por Luz
Os ambientes do RuneScape têm muitas luzes! O novo cliente faz todos os cálculos de iluminação por pixel sem nenhuma integração de luz off-line, o que significa que tratamos todas as luzes como dinâmicas. Uma abordagem moderna para cenas iluminadas dinamicamente é o sombreamento totalmente diferido. Porém, o processo totalmente diferido tem suas desvantagens e não seria uma solução viável se quiséssemos dar suporte para máquinas com especificações baixas.
Assim, mantendo nosso processo de renderização de iluminação avançada, a abordagem padrão de iluminação com 8-16 luzes por objeto não era suficiente devido ao tamanho dos grupos geométricos individuais. Tivemos que ser criativos de novo!
Optamos por uma solução conhecida como iluminação diferida indexada por luz, que é um meio termo entre a totalmente diferida e a iluminação avançada. Ela dá suporte para quatro luzes distintas por pixel e se encaixa bem no nosso processo de renderização de iluminação avançada. Isso resolveu praticamente todos os nossos problemas de luz para grandes grupos geométricos estáticos, permitindo ainda ter suporte para MSAA e fórmulas de iluminação diferentes para variações futuras.
Muitas luzes pontuais
Dispersão de Luz Atmosférica
Com distâncias de renderização maiores, sabíamos que a neblina do Java não daria certo. Começamos removendo completamente a neblina baseada em distância e substituindo-a por uma técnica de dispersão de luz atmosférica baseada em física, que tinha uma excelente aparência, mas não estava permitindo esconder as margens do mundo. Então decidimos combinar a antiga neblina baseada em distância com a nova dispersão de luz atmosférica, criando uma solução híbrida que aprimora a neblina antiga. O resultado final dá uma aparência muito mais natural à cena, principalmente com distâncias de renderização mais altas, e também ajuda a criar uma percepção de profundidade melhor.
Renderização de Água
Um dos principais atrativos do nosso novo sistema de efeitos é a nova renderização de água. Assim como praticamente todo o resto, esse shader foi recriado do zero, mas também tomamos a ousada decisão de voltar ao antigo conjunto de dados de água do Java para corrigir diversos problemas que ficaram no sistema que herdamos do HTML5. Ainda usamos os dados de água do HTML5 para criar os reflexos de plano em tempo real, mas a geometria da água agora é montada a partir do conjunto de dados do Java. Isso significa que os artistas não precisaram refazer todos os trechos de água, reduzindo muito o tempo de desenvolvimento.
O próprio shader é construído a partir de vários elementos para obter a aparência final. Os dois principais componentes de qualquer sistema de renderização de água são o suporte para reflexos e refrações em tempo real, então não podíamos deixar de incluí-los. Além disso, asseguramos que a iluminação da água interagisse corretamente com nosso sistema de sombras, então reflexos especulares diretos são ocultados onde as sombras são projetadas. O efeito de onda também foi melhorado alterando a forma como os mapas de vetores normais de água são amostrados em áreas onde há pouca ou nenhuma distorção, reduzindo os efeitos quadriculados que geralmente vemos em efeitos de água. Finalmente, com o antigo conjunto de dados, tivemos acesso a informações melhoradas da profundidade do terreno submarino, permitindo fazer o desaparecimento gradual de vários componentes (como neblina e distorção de ondas) à medida que a água se aproxima do litoral.
Espero que todos concordem que os resultados são de dar água na boca!
Interações entre Sombras e Iluminação na Água
Técnicas de Redução de Draw Calls
Embora não seja exatamente um recurso, seria um erro não mencionar um dos principais motivos para podermos obter um melhor desempenho do que em Java, mesmo renderizando muito mais. Já fiz uma breve menção sobre como conseguimos reduzir significativamente as draw calls da renderização do mapa de sombras, mas mais importante ainda foi reduzir as draw calls das passagens de iluminação avançada e profundidade de cena.
A coisa mais custosa que um mecanismo de jogo pode fazer em termos de tempo de CPU/GPU é enviar objetos para serem desenhados na tela. O custo é duplo: não só o processamento dos vértices e pixels pela placa de vídeo, como também o envio de draw calls durante o preenchimento do buffer de comandos da placa de vídeo.
Já mencionei a remoção de draw calls com base na área de visão e na distância de objetos, mas como as draw calls de iluminação avançada são significativamente mais custosas do que as de sombras simples, precisamos ir um pouco além.
A maior economia de draw calls vem do nosso sistema de agrupamento geométrico dinâmico. Quando os objetos são carregados, eles são colocados em grupos que compartilham o mesmo material e são costurados juntos para que possam ser renderizados com uma única draw call. Porém, há um lado negativo nisso: é preciso ter um sistema de atlas de texturas complexo para que todas as texturas desses objetos agrupados possam ser acessadas a partir de uma única página de textura. Isso acaba aumentando o tamanho dos vértices dos modelos por causa das informações extras necessárias para acessar a textura do objeto em um atlas, além de aumentar o número de instruções para o shader de pixel. No entanto, esse custo adicional é pequeno se comparado aos ganhos de desempenho obtidos com a redução das draw calls através de agrupamento. O mesmo sistema de agrupamento é usado por todas as passagens de renderização para reduzir ainda mais o número de draw calls.
Cada cor representa um único agrupamento de draw call.
A peça final do nosso quebra-cabeça de redução de draw calls é exclusivo e inovador: nosso sistema de remoção por oclusão. Ao contrário das soluções de remoção por oclusão que exigem processamento de cena off-line para gerar conjuntos potencialmente visíveis em determinados pontos do mundo (ou uma geometria oclusiva personalizada para soluções em tempo real), nossa abordagem não requer nenhuma das duas opções. Nossa solução surgiu devido à necessidade, já que não era viável processar a enorme quantidade de ambientes do RuneScape off-line, nem gerar geometrias oclusivas em proxy para uma solução on-line por causa de recursos de arte limitados. Portanto, desenvolvemos uma técnica híbrida que utiliza um rasterizador em software no lado da CPU para realizar consultas de oclusão, mas em vez de gerar os dados de profundidade de cena no lado da CPU, transferimos dados de buffer de profundidade de cena da GPU gerados em quadros anteriores para alimentar as consultas de oclusão em software no lado da CPU. As consultas de releitura e oclusão em software do buffer de profundidade da GPU possuem um custo fixo alto, mas em cenas com alta complexidade de profundidade, a redução de draw calls proporcionada pelo oclusão é alta o bastante para vermos importantes ganhos no desempenho, principalmente em máquinas com drivers gráficos de baixo desempenho.
Divirta-se!
Espero que este Blog dos Desenvolvedores tenha sido informativo, mostrando o grande desafio que enfrentamos para lhe oferecer um cliente com gráficos e desempenho melhores que o Java para conteúdos com 15 anos de idade.
Acreditamos que as decisões que tomamos durante este projeto criaram uma forte base para que o RuneScape possa proporcionar gráficos e desempenho ainda melhores por muitos anos.
Mod Lordgit
Programador Gráfico Principal