Exploração do shader de areia do jogo Journey

Entre os muitos jogos independentes lançados nos últimos 10 anos, um dos meus favoritos é definitivamente o Journey . Graças à sua estética deslumbrante e bela trilha sonora, o Journey se tornou um exemplo de excelência em quase todos os aspectos do desenvolvimento.

Sou desenvolvedor de jogos e artista técnico, por isso fiquei muito intrigado com a maneira como a areia foi renderizada. Não é apenas bonito, mas também diretamente relacionado à jogabilidade básica e à jogabilidade como um todo. A jornada é literalmente construída de areia e, sem um efeito tão surpreendente, o jogo em si simplesmente não poderia existir.


Neste artigo de dois posts, prestarei homenagem ao legado da Journey , ensinando como recriar exatamente a mesma renderização em areia usando shaders. Independentemente da necessidade de dunas de areia no seu jogo, esta série de tutoriais permitirá que você aprenda a recriar estética específica no seu próprio jogo. Se você deseja recriar o lindo shader de areia usado no Journey , primeiro precisa entender como ele foi construído. E, embora pareça extremamente complexo, na verdade consiste em vários efeitos relativamente simples. Essa abordagem para escrever shaders é necessária para se tornar um artista técnico de sucesso. Portanto, espero que você faça essa jornada comigo, na qual não apenas exploramos a criação de shaders, mas também aprendemos a combinar estética e jogabilidade.

Análise de areia no Journey


Este artigo, como muitas outras tentativas de recriar a renderização em areia do Journey , é baseado em um relatório do GDC que o engenheiro líder da empresa John Edwards, intitulado " Renderização em areia em viagem ". Nesta palestra, John Edwards fala sobre todas as camadas de efeitos adicionadas às dunas de areia do Journey para obter a aparência certa.


O relatório é muito útil, mas no contexto deste tutorial, muitas das limitações e decisões tomadas por John Edwards não são importantes. Vamos tentar recriar os shaders de areia, remanescentes do shampoo Journey , principalmente por referências visuais.

Vamos começar com uma malha 3D simples de uma duna perfeitamente lisa. A credibilidade da renderização da areia depende de dois aspectos: iluminação e grãos. Uma maneira interessante de refletir a luz da areia é fornecida por um modelo de iluminação modificado. No contexto da codificação de sombreador, o modelo de iluminação determina sombras e realces com base nas propriedades do modelo e nas condições de iluminação da cena.

No entanto, tudo isso não é suficiente para criar a ilusão de realismo. O problema é que a areia simplesmente não pode ser modelada com superfícies planas. Grãos de areia devem ser considerados. É por isso que existem dois efeitos separados que funcionam diretamente com o normal na superfície , que podem ser usados ​​para simular pequenas partículas de areia na superfície da duna.

O diagrama abaixo mostra todos os efeitos que aprenderemos neste tutorial. Do ponto de vista técnico, os cálculos normais são realizados antes do processamento da iluminação. Para facilitar o estudo, os efeitos serão descritos em uma ordem diferente.


Cor difusa

O efeito mais simples do shader de areia é sua cor difusa , que descreve aproximadamente o componente opaco da aparência geral. A cor difusa é calculada com base na cor real do objeto e nas condições de iluminação. Uma esfera pintada de branco não será perfeitamente branca em todos os lugares, porque a cor difusa depende do incidente de luz nela. As cores difusas são calculadas usando um modelo matemático que aproxima o reflexo da luz de uma superfície. Graças a um relatório de John Edwards com o GDC, sabemos exatamente a equação usada, que ele chama de refletância difusa do contraste ; baseia-se no conhecido modelo de reflexões de Lambert .



Antes e depois de aplicar a equação

Areia normal

A geometria original é completamente suave. Para compensar isso, a superfície normal do modelo é alterada usando uma técnica chamada mapeamento de resposta . Permite usar uma textura para simular geometria mais complexa.



Iluminação de borda

Cada nível de viagem usa uma paleta de cores limitada. Por isso, é bastante difícil entender onde uma duna termina e outra começa. Para aumentar a legibilidade, é utilizada a técnica de pequeno destaque do que é visível apenas ao longo da borda da duna. Isso se chama iluminação de aro e existem várias maneiras de implementá-lo. Para este tutorial, escolhi um método baseado nas reflexões de Fresnel que modela as reflexões em superfícies polidas nos chamados ângulos de incidência .



Espelho reflexo do oceano

Um dos aspectos mais agradáveis da jogabilidade do Journey é a capacidade de "surfar" nas dunas de areia. Provavelmente é por isso que a empresa de jogos queria que a areia parecesse mais um líquido do que um sólido. Para isso, foi utilizada uma forte reflexão, que pode ser encontrada frequentemente em shaders de água. John Edwards chama esse efeito de oceano especular , e no tutorial nós o implementamos usando a reflexão de Blinn-Fong .



Reflexo brilho

A adição de um componente especular do oceano ao shader de areia proporciona uma aparência mais fluida. No entanto, ele ainda não permite que um dos aspectos visuais mais importantes da areia seja transmitido: reflexões que ocorrem aleatoriamente. Nas dunas reais, esse efeito ocorre porque cada grão de areia reflete a luz em sua direção e muitas vezes um desses raios refletidos entra em nosso olho. Tal reflexo de brilho (reflexo de reflexos) ocorre mesmo em locais onde a luz direta do sol não cai; complementa o oceano especular e aprimora o senso de credibilidade.



Ondas de areia

Alterar as normais nos permitiu simular o efeito de pequenos grãos de areia cobrindo a superfície da duna. Nas dunas do mundo real, as ondas causadas pelo vento costumam aparecer. Sua forma varia dependendo da inclinação e posição de cada duna em relação à direção do vento. Potencialmente, esses padrões podem ser criados por meio de uma textura protuberante, mas, neste caso, será impossível alterar a forma das dunas em tempo real. A solução proposta por John Edwards é semelhante a uma técnica chamada sombreamento triplanar : utiliza quatro texturas diferentes, misturadas dependendo da posição e inclinação de cada duna.



Jornada Sand Shader Anatomy


O Unity tem muitos modelos de shader para você começar. Como estamos interessados ​​em materiais que podem receber iluminação e projetar sombras, precisamos começar com o shader de superfície (shader de superfície).

Todos os shaders de superfície são executados em dois estágios. Primeiro, é chamada uma função de superfície que coleta as propriedades da superfície que precisa ser renderizada, por exemplo, seu albedo , rugosidade , propriedades do metal , transparência e direção normal . Todas essas propriedades são transferidas para a função de iluminação , que leva em consideração a influência de fontes externas de luz e calcula o sombreamento e a iluminação.

Função de superfície


Vamos começar com o que se torna o núcleo da nossa função de superfície, chamada no código de surf abaixo. As únicas propriedades que precisamos definir são a cor da areia e a normal da superfície . O normal de um modelo 3D é um vetor que indica a posição da superfície. Os vetores normais são usados ​​pela função de iluminação para calcular como a luz será refletida. Eles geralmente são calculados durante a importação da malha. No entanto, eles podem ser modificados para simular uma geometria mais complexa. É aqui que os efeitos normais da areia e das ondas de areia distorcem a norma da areia para simular sua rugosidade.

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; float3 N = float3(0, 0, 1); N = RipplesNormal(N); N = SandNormal (N); o.Normal = N; } 

Ao escrever normais para o.Normal eles devem ser expressos no espaço tangente . Isso significa que o vetor é selecionado em relação à superfície do modelo 3D. Ou seja, float3(0, 0, 1) na verdade significa que nenhuma alteração é realmente feita no modelo 3D normal.

Ambas as funções, RipplesNormal e SandNormal recebem o vetor normal e o modificam. Mais tarde veremos como isso pode ser feito.

Função de iluminação


É na função de iluminação que todos os outros efeitos são implementados. O código abaixo mostra como cada componente individual é calculado em funções separadas (cor difusa, iluminação da borda, especular do oceano e reflexão de brilho). Então todos eles são combinados.

 #pragma surface surf Journey fullforwardshadows float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi) { float3 diffuseColor = DiffuseColor (); float3 rimColor = RimLighting (); float3 oceanColor = OceanSpecular (); float3 glitterColor = GlitterSpecular (); float3 specularColor = saturate(max(rimColor, oceanColor)); float3 color = diffuseColor + specularColor + glitterColor; return float4(color * s.Albedo, 1); } 

O método de combinar componentes é bastante arbitrário e nos permite alterá-lo para estudar possibilidades artísticas.

Normalmente, reflexões especulares se acumulam sobre cores difusas. Como aqui não temos uma, mas três reflexões especulares ( luz do aro , especular do oceano e especular do brilho ), precisamos ter mais cuidado para não deixar a areia tremeluzente. Como a luz do aro e a especular do oceano fazem parte do mesmo efeito, podemos escolher apenas o valor máximo deles. O especular de brilho é adicionado separadamente porque esse componente cria areia tremeluzente.

Parte 2. Cor Difusa


Na segunda parte do post, vamos nos concentrar no modelo de iluminação usado no jogo e nisso. como recriá-lo no Unity.

Na parte anterior, lançamos as bases para o que gradualmente se transformará em nossa versão do shader de areia Journey. Como mencionado anteriormente, a função de iluminação é usada em sombreadores de superfície para calcular o efeito da iluminação, para que sombras e realces apareçam na superfície. Descobrimos que o Journey tem vários efeitos que se enquadram nessa categoria. Começaremos com o efeito mais básico (e mais simples) encontrado no núcleo deste shader: sua iluminação difusa ( iluminação difusa / difusa).


Por enquanto, omitimos todos os outros efeitos e componentes, focando na iluminação da areia .

A função de iluminação que DiffuseColor na parte anterior do post chamada LightingJourney simplesmente delega o cálculo da cor difusa da areia a uma função chamada DiffuseColor .

 float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi) { // Lighting properties float3 L = gi.light.dir; float3 N = s.Normal; // Lighting calculation float3 diffuseColor = DiffuseColor(N, L); // Final color return float4(diffuseColor, 1); } 

Devido ao fato de que cada efeito é independente e armazenado em sua própria função, nosso código será mais modular e limpo.

Reflexão de Lambert


Antes de criar iluminação difusa "como no Journey", é bom ver como é a função de iluminação difusa "básica". A técnica mais simples de sombreamento para materiais foscos é chamada de refletância lambertiana . Este modelo aproxima a aparência da maioria das superfícies não brilhantes e não metálicas. É nomeado após o cientista enciclopédico suíço Johann Heinrich Lambert , que propôs seu conceito em 1760.

O conceito de reflexão de Lambert é baseado em uma idéia simples: o brilho de uma superfície depende da quantidade de luz incidente nela . Geometricamente, isso pode ser mostrado no diagrama abaixo, onde a esfera é iluminada por uma fonte de luz remota. Embora as áreas vermelha e verde da esfera recebam a mesma quantidade de iluminação, suas áreas de superfície são significativamente diferentes. Se a luz na região vermelha for distribuída por uma área maior, isso significa que cada unidade do quadrado vermelho recebe menos luz que verde.


Teoricamente, a reflexão de Lambert depende do ângulo relativo entre a superfície e a luz incidente . Do ponto de vista matemático, dizemos que essa é uma função do normal para a superfície e a direção da iluminação . Essas quantidades são expressas usando dois vetores de comprimento unitário (chamados vetores unitários ) N e L . Os vetores únicos são uma maneira padrão de especificar direções no contexto da codificação de sombreador.

O valor de N e L
Normal à superfície N É um vetor unitário direcionado para longe da própria superfície.

Por analogia, podemos assumir que a direção da iluminação L aponta da fonte de luz e segue na direção em que a luz está se movendo. Mas não é assim: a direção da iluminação é um único vetor apontando na direção da direção da qual a luz veio.

Isso pode ser confuso, especialmente se você é novo na criação de shaders. No entanto, graças a essa notação, as equações se tornam mais simples.

Reflexão de Lambert na Unidade
Antes do Shader padrão do Unity 5, a reflexão de Lambert era o modelo padrão para sombrear as superfícies iluminadas.

Você ainda pode acessá-lo no Inspetor de materiais: no sombreador herdado, ele é chamado de Difuso .

Se você escrever seu próprio sombreador de superfície, a reflexão Lambert estará disponível como uma função de iluminação chamada Lambert :

 #pragma surface surf Lambert fullforwardshadows 

Sua implementação pode ser encontrada na função LightingLambert definida no arquivo CGIncludes\Lighting.cginc .

Reflexão de Lambert e clima
A reflexão de Lambert é um modelo bastante antigo, mas fornece uma compreensão de conceitos complexos, como sombreamento de superfície. Também pode ser usado para explicar muitos outros fenômenos. Por exemplo, o mesmo diagrama explica por que é mais frio nos pólos do planeta do que no equador.

Observando com mais atenção, podemos ver que a superfície recebe a quantidade máxima de iluminação quando seu normal é paralelo à direção da iluminação. E vice-versa: não há luz se dois vetores unitários forem perpendiculares um ao outro.


Obviamente, o ângulo entre N e L crítico para a reflexão de acordo com Lambert. Além disso, o brilho é máximo e igual a 100% quando o ângulo é 0 e mínimo ( 0% ) quando o ângulo tende a 90 circ . Se você conhece a álgebra vetorial , pode entender que uma quantidade representando a reflexão de Lambert I é igual a N cdotL onde está o operador  cdot chamado de produto escalar .

(1)

$$ display $$ \ begin {equação *} I = N \ cdot L \ end {equação *} $$ display $$


O produto escalar é uma medida da “coincidência” de dois vetores em relação um ao outro e varia no intervalo de +1 (para dois vetores idênticos) a 1 (para dois vetores opostos). Um produto escalar é a base do sombreamento, que examinei em detalhes no tutorial Modelos de renderização e iluminação com base física .

Implementação


E para N e para L Você pode acessar facilmente os recursos de iluminação do shader de superfície através de s.Normal e gi.light.dirin . Para simplificar, iremos renomeá-los no código shader para N e L

 float3 DiffuseColor(float3 N, float3 L) { float NdotL = saturate( dot(N, L) ); return NdotL; } 

saturate função saturate limita o valor de 0 antes 1 . No entanto, como o produto escalar está na faixa de 1 antes +1 , precisaremos trabalhar apenas com seus valores negativos. É por isso que a reflexão de Lambert é frequentemente implementada da seguinte forma:

 float NdotL = max(0, dot(N, L) ); 

Reflexão de contraste da luz ambiente


Embora o reflexo de Lambert oculte bem a maioria dos materiais, ele não é fisicamente preciso nem foto-realista. Nos jogos mais antigos, os shaders de Lambert eram usados ​​extensivamente. Os jogos que usam essa técnica geralmente parecem antigos, porque podem inadvertidamente reproduzir a estética de jogos antigos. Se você não se esforçar para isso, a reflexão de Lambert deve ser evitada e usar uma tecnologia mais moderna.

Um desses modelos é o Modelo de Reflexão de Oren-Nayyar , originalmente descrito no artigo Generalização do Modelo de Reflexão de Lambert , publicado em 1994 por Michael Oren e Sri C. Nayyar. O modelo de Oren-Nayyar é uma generalização da reflexão de Lambert e é especialmente projetado para superfícies rugosas. Inicialmente, os desenvolvedores do Journey queriam usar a reflexão Oren-Nayyar como base para seu shader de areia. No entanto, essa idéia foi abandonada devido aos altos custos de computação.

Em seu relatório de 2013, o artista técnico John Edwards explica que o modelo de reflexão criado para a areia Journey foi baseado em uma série de tentativas e erros.Os desenvolvedores pretendiam não recriar a renderização fotorrealista do deserto, mas dar vida a uma estética concreta e imediatamente reconhecível.

Segundo ele, o modelo de sombreamento resultante corresponde a esta equação:

2)

$$ display $$ \ begin {equação *} I = 4 * \ left (\ left (N \ odot \ left [1, 0,3, 1 \ right] \ right) \ cdot L \ right) \ end {equação *} $$ display $$


onde  odot - produto elemento - elemento de dois vetores.

 float3 DiffuseColor(float3 N, float3 L) { Ny *= 0.3; float NdotL = saturate(4 * dot(N, L)); return NdotL; } 

Modelo de reflexão (2) John Edwards chama contraste difuso ; portanto, usaremos esse nome em todo o tutorial.

A animação abaixo mostra a diferença no sombreamento de Lambert (esquerda) e contraste difuso do Journey (direita).



Qual é o significado de 4 e 0,3?
Embora o contraste difuso não tenha sido projetado para ser fisicamente preciso, ainda podemos tentar entender o que ele faz.

Na sua essência, ele ainda usa a reflexão de Lambert. A primeira diferença óbvia é que o resultado geral é multiplicado por 4 . Isso significa que todos os pixels que foram normalmente recebidos 25 % a iluminação agora brilhará como se estivesse recebendo 100 % iluminação. Multiplicando tudo por 4 de acordo com Lambert, o sombreamento fraco se torna muito mais forte e a região de transição entre a escuridão e a luz é menor. Nesse caso, a sombra se torna mais nítida.

Efeito da multiplicação do componente y na direção normal 0 , 3 explicar é muito mais difícil. À medida que os componentes do vetor mudam, a direção geral em que ele aponta muda. Reduzindo o valor do componente y para tudo 30 % do seu valor original, a reflexão do contraste difuso faz com que as sombras se tornem mais verticais.

Nota: um produto escalar mede diretamente o ângulo entre dois vetores apenas se ambos tiverem comprimento 1 . A alteração feita reduz o comprimento normal N que não é mais um vetor de unidade.

De tons de cinza a cores


Todas as animações mostradas acima têm tons de cinza, porque mostram os valores de seu modelo de reflexão, variando no intervalo de 0 antes 1 ". Podemos adicionar cores facilmente usando NdotL como o coeficiente de interpolação entre duas cores: uma para sombra total e a outra para areia totalmente iluminada.

 float3 _TerrainColor; float3 _ShadowColor; float3 DiffuseColor(float3 N, float3 L) { Ny *= 0.3; float NdotL = saturate(4 * dot(N, L)); float3 color = lerp(_ShadowColor, _TerrainColor, NdotL); return color; } 

Parte 3. Areia normal


Na terceira parte, vamos nos concentrar na criação de mapas normais que transformam modelos 3D suaves em dunas de areia.

Na parte anterior do tutorial, implementamos a iluminação difusa da areia Journey. Ao usar apenas esse efeito, as dunas do deserto parecerão bastante chatas e chatas.


Um dos efeitos mais intrigantes da Journey é a granulação da areia. Olhando para qualquer tela, parece-nos que as dunas não são lisas e homogêneas, mas criadas a partir de milhões de grãos microscópicos de areia.


Esse efeito pode ser alcançado usando uma técnica chamada mapeamento de relevo , que permite que a luz ricocheteie em uma superfície plana como se fosse mais complexa. Veja como esse efeito altera a aparência da renderização:



Pequenas diferenças podem ser vistas com o aumento:



Lidamos com mapas normais


A areia consiste em inúmeros grãos de areia, cada um com sua própria forma e composição (veja abaixo). Cada partícula individual reflete a iluminação em uma direção potencialmente aleatória. Uma maneira de obter esse efeito é criar um modelo 3D contendo todos esses grãos microscópicos de areia. Mas, devido ao número incrível de polígonos necessários, essa abordagem não é viável.

Mas há outra solução que é frequentemente usada para simular uma geometria mais complexa em comparação com um modelo 3D real. Cada vértice ou face do modelo 3D está associado a um parâmetro chamado direção normal . Este é um vetor de comprimento unitário usado para calcular a reflexão da luz na superfície de um modelo 3D. Ou seja, para simular areia, você precisa simular essa distribuição aparentemente aleatória de grãos de areia e, portanto, como eles afetam as normais da superfície.


Isso pode ser feito de inúmeras maneiras. O mais simples é criar uma textura que mude a direção das normais originais do modelo das dunas.

Normal à superfície N no caso geral, é calculado pela geometria do modelo 3D. No entanto, você pode modificá-lo usando o mapa normal . Mapas normais são texturas que permitem simular geometrias mais complexas, alterando a orientação local das normais para a superfície. Essa técnica é freqüentemente chamada de mapeamento de resposta .

Alterar as normais é uma tarefa bastante simples que pode ser executada na função de surf do shader de superfície . Essa função usa dois parâmetros, um dos quais é uma struct chamada SurfaceOutput . Ele contém todas as propriedades necessárias para renderizar uma parte de um modelo 3D, desde a cor ( o.Albedo ) até a transparência ( o.Alpha ). Outro parâmetro que ele contém é a direção normal ( o.Normal ), que pode ser reescrita para alterar a maneira como a luz é refletida no modelo.

De acordo com a documentação do Unity sobre Surface Shaders , todas as normais gravadas na estrutura o.Normal devem ser expressas no espaço tangente :

 struct SurfaceOutput { fixed3 Albedo; // diffuse color fixed3 Normal; // tangent space normal, if written fixed3 Emission; half Specular; // specular power in 0..1 range fixed Gloss; // specular intensity fixed Alpha; // alpha for transparencies }; 

Assim, podemos relatar que os vetores unitários devem ser expressos no sistema de coordenadas em relação à malha normal. Por exemplo, ao escrever para o.Normal valores de float3(0, 0, 1) normal permanecerão inalterados.

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; o.Normal = float3(0, 0, 1); } 

Isso ocorre porque o vetor float3(0, 0, 1) é realmente um vetor normal expresso em relação à geometria do modelo 3D.

Portanto, para alterar o normal para a superfície no shader de superfície , basta escrever um novo vetor na função de superfície em o.Normal :

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; o.Normal = ... // change the normal here } 

No restante do post, criaremos a aproximação inicial, que complicaremos na sexta parte do tutorial.

Areia normal


A parte mais problemática é entender como os grãos de areia mudam normalmente para a superfície. Embora individualmente cada grão de areia possa espalhar a luz em qualquer direção, no geral, algo mais acontece. Qualquer abordagem fisicamente precisa deve estudar a distribuição de vetores normais na superfície da areia e modelá-la matematicamente. Esses modelos realmente existem, mas a solução apresentada em nosso tutorial é muito mais simples e, ao mesmo tempo, muito eficaz.

Em cada ponto do modelo, um vetor de unidade aleatório é amostrado da textura. Então, o normal para a superfície se inclina uma certa quantidade em relação a esse vetor. Com a criação correta de uma textura aleatória e a seleção de uma quantidade apropriada de mistura, podemos mudar o normal para a superfície de forma a criar uma sensação de granulação, sem perder a curvatura geral das dunas.

Valores aleatórios podem ser amostrados usando uma textura preenchida com cores aleatórias. Os componentes R, G e B de cada pixel são usados ​​como componentes X, Y e Z do vetor normal. Os componentes de cores estão na faixa  left[0,1 right] , então eles precisam ser convertidos em um intervalo  left[1,+1 right] . Então o vetor resultante é normalizado para que seu comprimento seja igual a 1 .


Crie texturas aleatórias
Existem muitas maneiras de gerar texturas aleatórias. Para obter o efeito desejado, o mais importante é a distribuição geral de vetores aleatórios que podem ser amostrados a partir da textura.

Na imagem acima, cada pixel é completamente aleatório. Não há direção geral (cor) que prevalece na textura, porque cada valor tem a mesma probabilidade que todos os outros. Essa textura nos dá um tipo de areia que espalha luz em todas as direções.

Durante uma palestra no GDC, John Edwards deixou claro que a textura aleatória usada para a areia em Journey era gerada a partir de uma distribuição gaussiana. Isso garante que a direção predominante coincida com o normal para a superfície.

Os vetores aleatórios precisam ser normalizados?
A imagem que usei para amostrar vetores aleatórios foi gerada usando um processo completamente aleatório. Não apenas cada pixel é gerado individualmente: os componentes R, G e B de um pixel também são independentes um do outro. Ou seja, no caso geral, os vetores amostrados dessa textura não terão garantia de comprimento igual a 1 .

Obviamente, você pode gerar uma textura na qual cada pixel ao converter de  l e f t [ 0 , 1, r i g h t ]  em  l e f t [ - 1 , + 1 r i g h t ]  e de fato terá que ter um comprimento 1 . No entanto, dois problemas surgem aqui.

, . -, mip-, .

, .

Implementação


Na parte anterior do tutorial, nos familiarizamos com o conceito de "mapas normais" quando ele apareceu no primeiro esboço da função de superfície surf . Recordando o diagrama mostrado no começo do artigo, você pode ver que são necessários dois efeitos para recriar a renderização da areia Journey. O primeiro ( normais de areia ) que consideramos nesta parte do artigo, e o segundo ( ondas de areia ) que estudamos na sexta parte.

 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; float3 N = float3(0, 0, 1); N = RipplesNormal(N); // Covered in Journey Sand Shader #6 N = SandNormal (N); // Covered in this article o.Normal = N; } 

Na seção anterior, introduzimos o conceito de mapeamento de resposta, que nos mostrou que parte do efeito exigirá amostragem da textura (é chamada no código uv_SandTex).

O problema com o código acima é que, para cálculos, você precisa saber a verdadeira posição do ponto que estamos desenhando. De fato, você precisa de uma coordenada UV para provar a textura , que determina de qual pixel ler. Se o modelo 3D que usamos é relativamente plano e tem uma conversão de UV, podemos usá-lo para provar uma textura aleatória.

 N = WavesNormal(IN.uv_SandTex.xy, N); N = SandNormal (IN.uv_SandTex.xy, N); 

Ou você também pode usar a posição no mundo ( IN.worldPos) do ponto renderizado.

Agora, finalmente, podemos nos concentrar em SandNormalsua implementação. Como afirmado anteriormente nesta parte, a idéia é coletar um pixel de uma textura aleatória e usá-lo (após converter em um vetor de unidade) como um novo normal.

 sampler2D_float _SandTex; float3 SandNormal (float2 uv, float3 N) { // Random vector float3 random = tex2D(_SandTex, uv).rgb; // Random direction // [0,1]->[-1,+1] float3 S = normalize(random * 2 - 1); return S; } 

Como zoom textura aleatória?
UV- 3D- , . , .

, Unity . , _SandText_ST . Unity ( ) _SandTex .

_SandText_ST : . , Tiling Offset :


, TRANSFORM_TEX :

 sampler2D_float _SandTex; float4 _SandTex_ST; float3 SandNormal (float2 uv, float3 N) { // Random vector float3 random = tex2D(_SandTex, TRANSFORM_TEX(uv, _SandTex)).rgb; // Random direction // [0,1]->[-1,+1] float3 S = normalize(random * 2 - 1); return S; } 

Incline os normais


O trecho de código mostrado acima funciona, mas não produz resultados muito bons. A razão para isso é simples: se retornarmos um normal completamente aleatório, mas essencialmente perdermos a sensação de curvatura. De fato, a direção do normal é usada para calcular como a luz deve ser refletida da superfície, e seu principal objetivo é sombrear o modelo de acordo com sua curvatura.

A diferença pode ser vista nas imagens abaixo. Acima, as normais das dunas são completamente aleatórias, e é impossível entender onde uma termina e outra começa. Abaixo, apenas o normal do modelo é usado, pelo que obtemos uma superfície muito lisa.



Ambas as soluções não nos convêm. Precisamos de algo no meio. Uma direção aleatória amostrada de uma textura deve ser usada para inclinar o normal em uma certa quantidade, como mostrado abaixo:


A operação descrita no diagrama é chamada slerp , que significa interpolação linear esférica (interpolação linear esférica). O Slerp funciona exatamente como o lerp, com uma exceção - ele pode ser usado para interpolar com segurança entre vetores unitários, e o resultado da operação serão outros vetores unitários.

Infelizmente, a implementação correta do slerp é bastante cara. E para um efeito, pelo menos com base no acaso, é ilógico usá-lo.

Mostre-me a equação slerp
, p0 e p1 , . Então slerp :

(1)

slerp(p0,p1,t)=sin[(1t)Ω]sin(Ω)p0+sin(tΩ)sin(Ω)p1


Ωp0 e p1 , :

(2)

Ω=cos1(p0p1)



É importante observar que, se usarmos a interpolação linear tradicional , o vetor resultante será muito diferente:


A operação Lerp entre dois vetores de unidade separados nem sempre cria outros vetores de unidade. De fato, isso nunca acontece, a menos que o coeficiente seja1 ou0 0 .

Apesar disso, ao normalizar o resultado do lerp, obtemos um vetor unitário, surpreendentemente próximo do resultado gerado pelo slerp:

 float3 nlerp(float3 n1, float3 n2, float t) { return normalize(lerp(n1, n2, t)); } 

Essa técnica, chamada nlerp , fornece uma aproximação aproximada do slerp. Seu uso foi popularizado por Casey Muratori , um dos desenvolvedores do The Witness . Se você estiver interessado em aprender mais sobre este tópico, recomendo os artigos Noções básicas sobre Slerp. Então não o use por Jonathan Blow e Math Magician - Lerp, Slerp e Nlerp .

Graças ao nlerp, agora podemos inclinar com eficiência vetores normais para um lado aleatório, amostrados de _SandTex:

 sampler2D_float _SandTex; float _SandStrength; float3 SandNormal (float2 uv, float3 N) { // Random vector float3 random = tex2D(_SandTex, uv).rgb; // Random direction // [0,1]->[-1,+1] float3 S = normalize(random * 2 - 1); // Rotates N towards Ns based on _SandStrength float3 Ns = nlerp(N, S, _SandStrength); return Ns; } 

O resultado é mostrado abaixo:



O que vem a seguir


Na próxima parte, consideraremos as reflexões tremeluzentes, graças às quais as dunas se parecerão com o oceano.

Agradecimentos


O videogame Journey foi desenvolvido pela Thatgamecompany e publicado pela Sony Computer Entertainment . Está disponível para PC ( Epic Store ) e PS4 ( PS Store ).

Modelos 3D de fundos de dunas e opções de iluminação são criados por Jiadi Deng .

Um modelo 3D do personagem de Journey foi encontrado no fórum FacePunch (agora fechado).

Pacote Unity


Se você deseja recriar esse efeito, o pacote completo do Unity pode ser baixado do Patreon . Inclui tudo o que você precisa, de sombreadores a modelos 3D.

Source: https://habr.com/ru/post/pt476448/


All Articles