Shader não é mágico. Escrevendo shaders no Unity. Vertex Shaders

Olá pessoal! Meu nome é Grigory Dyadichenko e sou o fundador e CTO da Foxsys Studios. Hoje vamos falar sobre shaders de vértice. O artigo examinará a prática do ponto de vista do Unity, exemplos muito simples, bem como muitos links para o estudo de informações sobre shaders no Unity. Se você é bom em escrever shaders, não encontrará nada novo para si. Quem quer começar a escrever shaders no Unity, bem-vindo ao gato.



Pouco de teoria



Para uma melhor compreensão do processo de sombreador, vamos dar uma olhada em um pouco de teoria. Um shader de vértice ou shader de vértice é um estágio programável de um shader que trabalha com vértices individuais. Os vértices, por sua vez, armazenam vários atributos que são processados ​​por essa parte do shader para obter atributos convertidos na saída.

Exemplos onde shaders de vértice são usados





Deformação de objetos - ondas realistas, o efeito de ondulações da chuva, deformação quando uma bala atinge, tudo isso pode ser feito com sombreadores de vértice e parecerá mais realista do que a mesma coisa feita através do Bump Mapping na parte fragmentada do shader. Uma vez que esta é uma mudança na geometria. Os shaders de nível 3.0 sobre esse assunto têm uma técnica chamada Mapeamento de posicionamento, pois agora eles têm acesso a texturas na parte do vértice do shader.



Animação de objetos. Os jogos parecem mais animados e interessantes quando as plantas reagem a um personagem ou as árvores balançam ao vento. Para isso, também são utilizados shaders de vértice.



Desenho de iluminação ou estilizado. Em muitos jogos, do ponto de vista do estilo, não é a iluminação pbr que parece muito mais interessante, mas a estilização. Ao mesmo tempo, não faz sentido calcular nada na parte do fragmento.



Skinning. No momento determinado nos mecanismos de jogo, esse problema está resolvido, mas, no entanto, é útil entender os sombreadores de vértices para entender como ele funciona.

Exemplos simples de trabalho com vértices




Eu não quero que isso aconteça, como nas lições antigas sobre como desenhar uma coruja, então vamos por etapas. Crie um shader de superfície padrão. Isso pode ser feito com o botão direito do mouse na Project View ou no painel superior da guia Assets. Crie-> Shader-> Shader de superfície padrão.

E temos um padrão em branco.

Sombreador de superfície
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows

// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0

sampler2D _MainTex;

struct Input
{
float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)

void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = ca;
}
ENDCG
}
FallBack "Diffuse"
}

Como ele funciona e, em geral, o analisaremos em detalhes no artigo após a prática básica, além de o entendermos parcialmente durante a implementação dos shaders. Por enquanto, deixe algumas das coisas permanecerem como dadas. Em resumo, não há mágica (em termos de como os parâmetros são conectados etc.) Apenas para determinadas palavras-chave, a unidade gera código para você, para não escrevê-lo do zero. Portanto, esse processo não é óbvio o suficiente. Você pode ler mais sobre o sombreador de superfície e suas propriedades no Unity aqui. docs.unity3d.com/Manual/SL-SurfaceShaders.html

Removeremos tudo supérfluo dele para que não distraia, pois, no momento, não é necessário. E obtenha um shader tão curto.

Shader simplificado
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;

void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}




Apenas a cor no modelo com iluminação. Nesse caso, a Unity é responsável pelo cálculo da iluminação.

Primeiro, adicione o efeito mais simples dos exemplos do Unity. A extrusão é normal e, no seu exemplo, analisaremos como ela funciona.

Para fazer isso, adicione o modificador vertex: vert à linha #pragma surface surf Standard fullforwardshadows line. Se passarmos inout appdata_full v como parâmetro para uma função, então, em essência, essa função é um modificador de vértice. Na sua essência, faz parte do sombreador de vértices, criado pela unidade geradora de código, que executa o processamento preliminar dos vértices. Também no bloco Propriedades , adicione o campo _Amount aceitando valores de 0 a 1. Para usar o campo _Amount no shader, também precisamos defini-lo lá. Na função, simplesmente mudaremos para o normal, dependendo de _Amount , onde 0 é a posição padrão do vértice (deslocamento zero) e 1 é o deslocamento exatamente para o normal.

SimpleVertexExtrusionShader
Shader "Custom/SimpleVertexExtrusionShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Amount ("Extrusion Amount", Range(0,1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;
float _Amount;

void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amount;
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}


Você pode perceber uma característica importante dos shaders. Embora o sombreador seja executado a cada quadro, o resultado obtido durante a operação do sombreador não é armazenado na malha, mas é usado apenas para renderização. Portanto, é impossível se relacionar com as funções do sombreador, bem como com a atualização nos scripts. Eles são aplicados a todos os quadros sem alterar os dados da malha, mas simplesmente modificando a malha para renderização adicional.

Por exemplo, uma das maneiras mais fáceis de criar uma animação é usar o tempo para alterar a amplitude. A unidade possui variáveis internas , uma lista completa das quais podem ser encontradas aqui docs.unity3d.com/Manual/SL-UnityShaderVariables.html Nesse caso, escreveremos um novo sombreador com base no nosso sombreador anterior. Em vez de _Amount, vamos criar o valor flutuante _Amplitude e usar a variável interna do Unity _SinTime . _SinTime é o seno do tempo e, portanto, leva valores de -1 a 1. No entanto, não esqueça que todas as variáveis ​​de tempo internas nos shaders de unidade são vetores float4 . Por exemplo, _SinTime é definido como (sin (t / 8), sin (t / 4), sin (t / 2), sin (t)) , onde t é a hora. Portanto, pegamos o componente z para que a animação seja mais rápida. E temos:

SimpleVertexExtrusionWithTime
Shader "Custom/SimpleVertexExtrusionWithTime"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_Amplitude ("Extrusion Amplitude", float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert

#pragma target 3.0

struct Input
{
float4 color : COLOR;
};

fixed4 _Color;
float _Amplitude;

void vert (inout appdata_full v)
{
v.vertex.xyz += v.normal * _Amplitude * (1 - _SinTime.z);
}
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = _Color;
o.Albedo = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}




Então, esses eram exemplos simples. É hora de desenhar uma coruja!

Deformação de objetos





Eu já escrevi um artigo inteiro sobre o assunto de um efeito de deformação com uma análise detalhada da matemática do processo e da lógica do pensamento ao desenvolver esse efeito habr.com/en/post/435828 Esta será a nossa coruja.

Todos os sombreadores do artigo estão escritos em hlsl. Na verdade, essa linguagem possui sua própria documentação volumosa, que muitos esquecem e se perguntam de onde vêm metade das funções com fio, embora estejam definidas no HLSL docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl- funções intrínsecas

Mas, de fato, os shaders de superfície em uma unidade são um tópico grande e volumoso por si só. Além disso, você nem sempre quer mexer com a iluminação do Unity. Às vezes, você precisa trapacear e escrever o shader mais rápido que possui apenas o conjunto certo de efeitos predefinidos. Na unidade, você pode escrever shaders de nível inferior.

Shaders de baixo nível





De acordo com a boa e velha tradição de trabalhar com shaders, a seguir atormentaremos o coelho de Stanford.

Em geral, o chamado Unity ShaderLab é essencialmente uma visualização de um inspetor com campos de materiais e alguma simplificação dos shaders de escrita.

Veja a estrutura geral do shaderlab shader:

Estrutura geral do shader
Shader "MyShaderName"
{
Properties
{
//
}
SubShader // ( )
{
Pass
{
//
}
//
}
//
FallBack "VertexLit" // ,
}


Diretivas de compilação, como
#pragma vertex vert
frag de fragmento #pragma
determine qual shader funciona para compilar como shaders de vértice e fragmento, respectivamente.

Digamos que tomemos um dos exemplos mais comuns - um sombreador para exibir a cor dos normais:

SimpleNormalVisualization
Shader "Custom/SimpleNormalVisualization"
{
Properties
{
}
SubShader
{
Pass
{
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + 0.5;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return fixed4 (i.color, 1);
}
ENDCG
}
}
FallBack "VertexLit"
}




Nesse caso, na parte do vértice, escrevemos o valor normal convertido na cor do vértice e na parte do pixel usamos essa cor como a cor do modelo.

A função UnityObjectToClipPos é uma função auxiliar do Unity (do arquivo UnityCG.cginc ) que converte os vértices do objeto na posição associada à câmera. Sem ele, um objeto, quando entrar na visibilidade da câmera (frustrum), será desenhado nas coordenadas da tela, independentemente da posição da transformação. Como inicialmente as posições dos vértices são apresentadas nas coordenadas do objeto. Apenas valores relativos ao seu pivô.

Este bloco.
struct v2f {
float4 pos : SV_POSITION;
fixed3 color : COLOR0;
};

Essa é a definição da estrutura que será processada na parte do vértice e transferida para a parte do fragmento. Nesse caso, é determinado que dois parâmetros são obtidos da malha - a posição do vértice e a cor do vértice. Você pode ler mais sobre quais dados podem ser lançados em uma unidade neste link docs.unity3d.com/Manual/SL-VertexProgramInputs.html

Esclarecimentos importantes. Os nomes dos atributos da malha não importam. Ou seja, digamos que no atributo de cor, você pode escrever o vetor de desvio da posição original (dessa maneira, às vezes, eles produzem um efeito quando o personagem está andando, de modo que a grama "repele"). Como esse atributo será processado depende inteiramente do seu sombreador.

Conclusão



Obrigado pela atenção! É problemático escrever alguns efeitos complexos sem uma parte fragmentária; por esse motivo, discutiremos artigos semelhantes em artigos separados. Espero que, durante este artigo, tenha ficado um pouco mais claro como o código dos shaders de vértice é escrito em geral e onde você pode encontrar informações para estudo, uma vez que os shaders são um tópico muito profundo.

Em artigos futuros, analisaremos os outros tipos de shaders, efeitos individuais, e tentarei descrever minha lógica de pensamento ao criar efeitos novos ou complexos.

Também foi criado um repositório onde todos os resultados desta série de artigos serão adicionados ao github.com/Nox7atra/ShaderExamples.Espero que essas informações sejam úteis para iniciantes que estão iniciando sua jornada no estudo deste tópico.

Alguns links úteis (incluindo fontes):


www.khronos.org/opengl/wiki/Vertex_Shader
docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-reference
docs.unity3d.com/en/current/Manual/SL-Reference.html
docs.unity3d.com/Manual/GraphicsTutorials.html
www.malbred.com/3d-grafika-3d-redaktory/sovremennaya-terminologiya-3d-grafiki/vertex-shader-vershinnyy-sheyder.html
3dpapa.ru/accurate-displacement-workflow

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


All Articles