Crie um efeito de distribuição de cores no Unity


Este efeito foi inspirado no episódio de Powerpuff Girls . Eu queria criar o efeito da propagação da cor em um mundo preto e branco, mas implementá-lo nas coordenadas do espaço mundial , para ver como a cor pinta objetos , e não apenas espalhar-se na tela, como em um desenho animado.

Criei o efeito no novo Lightweight Rendering Pipeline do mecanismo Unity, um exemplo interno do pipeline Scriptable Rendering Pipeline. Todos os conceitos se aplicam a outros pipelines, mas algumas funções ou matrizes internas podem ter nomes diferentes. Também usei a nova pilha de pós-processamento, mas no tutorial vou omitir uma descrição detalhada de suas configurações, porque ela é descrita muito bem em outros manuais, por exemplo, neste vídeo .



O efeito do pós-processamento em escala de cinza


Apenas para referência, é assim que uma cena se parece sem efeitos de pós-processamento.


Para esse efeito, usei o novo pacote de pós-processamento do Unity 2018, que pode ser baixado do gerenciador de pacotes. Se você não sabe como usá-lo, recomendo este tutorial .

Eu escrevi meu próprio efeito estendendo as classes PostProcessingEffectSettings e PostProcessEffectRenderer escritas em C #, cujo código-fonte pode ser visto aqui . Na verdade, eu não fiz nada de particularmente interessante com esses efeitos no lado da CPU (no código C #), exceto que adicionei um grupo de propriedades gerais ao Inspector, por isso não explicarei como fazer isso no tutorial. Espero que meu código fale por si.

Vamos passar para o código do sombreador e começar com o efeito de escala de cinza. No tutorial, não modificaremos o arquivo shaderlab, as estruturas de entrada e o shader de vértice, para que você possa ver o código fonte aqui . Em vez disso, cuidaremos do shader de fragmento.

Para converter uma cor em uma escala de cinza, reduzimos o valor de cada pixel para um valor de luminância que descreve seu brilho . Isso pode ser feito considerando o produto escalar do valor de cor da textura da câmera e o vetor ponderado , que descreve a contribuição de cada canal de cor para o brilho geral da cor.

Por que usamos produtos escalares? Não esqueça que os produtos escalares são calculados da seguinte maneira:

dot(a, b) = a x * b x + a y * b y + a z * b z

Nesse caso, multiplicamos cada canal do valor da cor por peso . Em seguida, adicionamos esses produtos para reduzi-los a um único valor escalar. Quando a cor RGB possui os mesmos valores nos canais R, G e B, a cor fica cinza.

Aqui está a aparência do código shader:

 float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0); 

Se o sombreador base estiver configurado corretamente, o efeito de pós-processamento deverá colorir a tela inteira em escala de cinza.




Renderizando efeito de cor no espaço do mundo


Como esse é um efeito de pós-processamento, não temos informações sobre a geometria da cena no sombreador de vértices. No estágio de pós-processamento, a única informação que temos é a imagem renderizada pela câmera e o espaço das coordenadas truncadas para amostrá-la. No entanto, queremos que o efeito de coloração se espalhe pelos objetos, como se estivesse acontecendo no mundo, e não apenas em uma tela plana.

Para desenhar esse efeito na geometria da cena, precisamos das coordenadas do espaço mundial de cada pixel. Para passar das coordenadas do espaço de coordenadas truncadas para as coordenadas do espaço mundial , precisamos realizar uma transformação do espaço de coordenadas .

Normalmente, para ir de um espaço de coordenadas para outro, é necessária uma matriz que defina a transformação do espaço de coordenadas A para o espaço B. Para ir de A para B, multiplicamos o vetor no espaço de coordenadas A por essa matriz de transformação. No nosso caso, realizaremos a seguinte transição: o espaço das coordenadas truncadas (espaço do clipe) -> ver espaço (ver espaço) -> espaço do mundo (espaço do mundo) . Ou seja, precisamos da matriz clip-to-view-space e da matriz view-to-world-space que o Unity fornece.

No entanto, as coordenadas da unidade do espaço de coordenadas truncadas não têm um valor z que determina a profundidade do pixel ou a distância da câmera. Precisamos desse valor para passar do espaço de coordenadas truncadas para o espaço de espécies. Vamos começar com isso!

Obtendo o valor do buffer de profundidade


Se o pipeline de renderização estiver ativado, ele desenha uma textura na viewport que armazena os valores z em uma estrutura chamada buffer de profundidade . Podemos amostrar esse buffer para obter o valor z ausente do nosso espaço de coordenadas de coordenadas truncadas!

Primeiro, verifique se o buffer de profundidade é realmente renderizado clicando na seção "Adicionar dados adicionais" da câmera no Inspetor e verificando se a caixa "Requer textura de profundidade" está marcada. Verifique também se a opção Permitir MSAA está ativada para a câmera. Não sei por que esse efeito precisa ser verificado, mas é. Se o buffer de profundidade for desenhado, no depurador de quadros, você deverá ver o estágio "Depth Prepass" .

Crie um amostrador _CameraDepthTexture no arquivo hlsl

 TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture); 

Agora vamos escrever a função GetWorldFromViewPosition e, por enquanto, vamos usá-la para verificar o buffer de profundidade . (Mais tarde expandiremos para obter uma posição no mundo.)

 float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; } 

No sombreador de fragmento, desenhe o valor da amostra de textura de profundidade.

 float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0); 

É assim que meus resultados se parecem quando há apenas uma planície montanhosa na cena (desliguei todas as árvores para simplificar ainda mais o teste dos valores do espaço mundial). Seu resultado deve ser semelhante. Os valores em preto e branco descrevem a distância da geometria à câmera.


Aqui estão algumas etapas que você pode executar se encontrar problemas:

  • Verifique se a câmera possui a renderização em profundidade ativada.
  • Verifique se a câmera possui o MSAA ativado.
  • Tente alterar o plano próximo e distante da câmera.
  • Verifique se os objetos que você espera ver no buffer de profundidade usam um sombreador com um passe de profundidade. Isso garante que o objeto seja atraído para o buffer de profundidade. Todos os shaders padrão no LWRP fazem isso.

Obtendo valor no espaço mundial


Agora que temos todas as informações necessárias para o espaço de coordenadas truncadas , vamos transformar no espaço de espécies e depois no espaço do mundo .

Observe que as matrizes de transformação necessárias para essas operações já estão na biblioteca SRP. No entanto, eles estão contidos na biblioteca C # do mecanismo do Unity, então os inseri no sombreador na função Render do script ColorSpreadRenderer :

 sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse); 

Agora vamos estender nossa função GetWorldFromViewPosition.

Primeiro, precisamos obter a posição na viewport multiplicando a posição no espaço de coordenadas truncado pelo InverseProjectionMatrix . Também precisamos fazer um pouco mais de magia vodu com uma posição na tela, relacionada à forma como o Unity armazena sua posição no espaço de coordenadas truncadas.

Finalmente, podemos multiplicar a posição na janela de exibição por ViewToWorldMatrix para obter a posição no espaço do mundo .

 float3 GetWorldFromViewPosition (VertexOutput i) { //    float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; //      float4 result = mul(unity_InverseProjectionMatrix, float4(2*i.screenPos-1.0, z, 1.0)); float3 viewPos = result.xyz / result.w; //      float3 worldPos = mul(unity_ViewToWorldMatrix, float4(viewPos, 1.0)); return worldPos; } 

Vamos fazer uma verificação para garantir que as posições no espaço global estejam corretas. Para fazer isso, escrevi um sombreador que retorna apenas a posição de um objeto no espaço do mundo ; esse é um cálculo bastante simples, baseado em um sombreador regular, cuja correção pode ser confiável. Desative o efeito do pós-processamento e faça uma captura de tela deste shader de teste para o espaço mundial . Meu depois de aplicar o shader na superfície da terra na cena é assim:


(Observe que os valores no espaço mundial são muito maiores que 1,0, portanto, não se preocupe, pois essas cores fazem algum sentido; em vez disso, verifique se os resultados são os mesmos para as respostas "verdadeiras" e "calculadas".) Em seguida, voltemos ao teste o objeto é material comum (e não o material de teste do espaço mundial) e, em seguida, ative o efeito de pós-processamento novamente. Meus resultados são assim:


Isso é completamente semelhante ao shader de teste que escrevi, ou seja, é provável que os cálculos do espaço mundial estejam corretos!

Desenhando um círculo no espaço do mundo


Agora que temos posições no espaço do mundo , podemos desenhar um círculo de cores na cena! Precisamos definir o raio dentro do qual o efeito desenhará cor. Lá fora, o efeito renderizará a imagem em escala de cinza. Para defini-lo, você precisa ajustar os valores do raio do efeito ( _MaxSize ) e do centro do círculo (_Center). Defino esses valores na classe C # ColorSpread para que sejam visíveis no inspetor. Vamos expandir nosso shader de fragmento forçando-o a verificar se o pixel atual está dentro do raio do círculo :

 float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i); // ,      .  //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= _MaxSize? 0 : 1; //   float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); //   float luminance = dot(fullColor.rgb, float3(0.2126729, 0.7151522, 0.0721750)); float3 greyscale = luminance.xxx; // ,       float3 color = (1-blend)*fullColor + blend*greyscale; return float4(color, 1.0); } 

Finalmente, podemos desenhar a cor com base em se ela está dentro de um raio no espaço do mundo . É assim que o efeito base se parece!




Adicionando efeitos especiais


Vou examinar mais algumas técnicas usadas para fazer a cor se espalhar pelo chão. Há muito mais para o efeito total, mas o tutorial já se tornou muito grande, portanto, nos limitaremos ao mais importante.

Animação de ampliação do círculo


Queremos que o efeito se espalhe pelo mundo, isto é, como se estivesse crescendo. Para fazer isso, você precisa alterar o raio, dependendo da hora.

_StartTime indica a hora em que o círculo deve começar a crescer. No meu projeto, usei um script adicional que permite clicar em qualquer lugar da tela para iniciar o crescimento do círculo; nesse caso, a hora de início é igual à hora em que o mouse foi clicado.

_GrowthSpeed ​​define a velocidade do aumento do círculo.

 //           float timeElapsed = _Time.y - _StartTime; float effectRadius = min(timeElapsed * _GrowthSpeed, _MaxSize); //  ,      effectRadius = clamp(effectRadius, 0, _MaxSize); 

Também precisamos atualizar a verificação de distância para comparar a distância atual com o raio crescente do efeito , e não com _MaxSize.

 // ,         //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= effectRadius? 0 : 1; //     ... 

Aqui está como deve ser o resultado:


Adicionando ao raio do ruído


Eu queria que o efeito fosse mais como um borrão de tinta, não apenas um círculo crescente. Para fazer isso, vamos adicionar ruído ao raio do efeito, para que a distribuição seja desigual.

Primeiro, precisamos provar a textura no espaço do mundo . As coordenadas UV do i.screenPos estão localizadas no espaço da tela e, se fizermos uma amostra com base nelas, a forma do efeito se moverá com a câmera; então vamos usar as coordenadas no espaço do mundo . Adicionei o parâmetro _NoiseTexScale para controlar a escala da amostra de textura de ruído , porque as coordenadas no espaço do mundo são muito grandes.

 //          float2 worldUV = worldPos.xz; worldUV *= _NoiseTexScale; 

Agora vamos provar a textura do ruído e adicionar esse valor ao raio do efeito. Usei a escala _NoiseSize para ter mais controle sobre o tamanho do ruído.

 //     float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r; effectRadius -= noise * _NoiseSize; 

A seguir, como são os resultados após alguns ajustes:




Em conclusão


Você pode acompanhar as atualizações dos tutoriais no meu Twitter e no Twitch eu gasto codificação de streams! (Além disso, eu transmito jogos de vez em quando, então não se surpreenda se você me ver sentada de pijama e jogando Kingdom Hearts 3.)

Agradecimentos:

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


All Articles