Criando um shader de grama no mecanismo Unity


Este tutorial mostrará como escrever um sombreador geométrico para gerar lâminas de grama a partir dos topos da malha de entrada e usar o mosaico para controlar a densidade da grama.

O artigo descreve o processo passo a passo de escrever um shader de grama no Unity. O shader recebe a malha de entrada e, de cada vértice da malha, gera uma folha de grama usando o shader geométrico . Por uma questão de interesse e realismo, as lâminas de grama terão um tamanho e rotação aleatórios e também serão afetadas pelo vento . Para controlar a densidade da grama, usamos mosaico para separar a malha de entrada. A grama poderá projetar e receber sombras.

O projeto finalizado é publicado no final do artigo. O arquivo sombreador gerado contém um grande número de comentários que facilitam o entendimento.

Exigências


Para concluir este tutorial, você precisará de conhecimentos práticos sobre o mecanismo do Unity e um entendimento inicial da sintaxe e da funcionalidade dos shaders.

Faça o download do rascunho do projeto (.zip) .

Começando a trabalhar


Faça o download do rascunho do projeto e abra-o no editor do Unity. Abra a cena Main e, em seguida, abra o shader Grass no seu editor de código.

Este arquivo contém um sombreador que produz a cor branca, bem como algumas funções que usaremos neste tutorial. Você notará que essas funções, juntamente com o sombreador de vértice, estão incluídas no bloco CGINCLUDE localizado fora do SubShader . O código colocado neste bloco será automaticamente incluído em todas as passagens no shader; isso será útil mais tarde, porque nosso shader terá vários passes.

Começaremos escrevendo um shader geométrico que gera triângulos de cada vértice na superfície da nossa malha.

1. Shaders geométricos


Os shaders geométricos são uma parte opcional do pipeline de renderização. Eles são executados após o sombreador de vértice (ou sombreamento de mosaico, se for utilizado mosaico) e antes de os vértices serem processados ​​para o sombreador de fragmento.


Pipeline de gráficos do Direct3D 11. Observe que neste diagrama o sombreador de fragmento é chamado de sombreador de pixel .

Os sombreadores geométricos recebem uma única primitiva na entrada e podem gerar zero, uma ou muitas primitivas. Começaremos escrevendo um shader geométrico que recebe um vértice (ou ponto ) na entrada e que alimenta um triângulo representando uma folha de grama.

 // Add inside the CGINCLUDE block. struct geometryOutput { float4 pos : SV_POSITION; }; [maxvertexcount(3)] void geo(triangle float4 IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream) { } … // Add inside the SubShader Pass, just below the #pragma fragment frag line. #pragma geometry geo 

O código acima declara um sombreador geométrico chamado geo com dois parâmetros. O primeiro, triangle float4 IN[3] , relata que será necessário um triângulo (composto por três pontos). O segundo, como TriangleStream , configura um sombreador para gerar um fluxo de triângulos, para que cada vértice use a estrutura geometryOutput para transmitir seus dados.

Dissemos acima que o shader receberá um vértice e produzirá uma folha de grama. Por que então temos um triângulo?
Será mais barato considerar um como entrada. Isso pode ser feito da seguinte maneira.

 void geo(point vertexOutput IN[1], inout TriangleStream<geometryOutput> triStream) 

No entanto, como nossa malha de entrada (neste caso GrassPlane10x10 , localizada na pasta Mesh ) possui uma topologia de triângulo , isso causará uma incompatibilidade entre a topologia de malha de entrada e a primitiva de entrada necessária. Embora isso seja permitido no DirectX HLSL, não é permitido no OpenGL , portanto, um erro será exibido.

Além disso, adicionamos o último parâmetro entre colchetes acima da declaração da função: [maxvertexcount(3)] . Ele diz à GPU que produziremos (mas não somos obrigados a fazê-lo) não mais que 3 vértices. Também fazemos com que o SubShader use um shader geométrico, declarando-o dentro do Pass .

Nosso shader geométrico ainda não está fazendo nada; para desenhar um triângulo, adicione o seguinte código dentro do shader geométrico.

 geometryOutput o; o.pos = float4(0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(-0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(0, 1, 0, 1); triStream.Append(o); 


Isso deu resultados muito estranhos. Quando você move a câmera, fica claro que o triângulo é renderizado no espaço da tela . Isso é lógico: como o sombreador geométrico é executado imediatamente antes do processamento dos vértices, ele retira a responsabilidade pelos vértices a serem exibidos no espaço de truncamento . Alteraremos nosso código para refletir isso.

 // Update the return call in the vertex shader. //return UnityObjectToClipPos(vertex); return vertex; … // Update each assignment of o.pos in the geometry shader. o.pos = UnityObjectToClipPos(float4(0.5, 0, 0, 1)); … o.pos = UnityObjectToClipPos(float4(-0.5, 0, 0, 1)); … o.pos = UnityObjectToClipPos(float4(0, 1, 0, 1)); 


Agora nosso triângulo é renderizado corretamente no mundo. No entanto, parece que apenas um é criado. De fato, um triângulo é desenhado para cada vértice de nossa malha, mas as posições atribuídas aos vértices do triângulo são constantes - elas não mudam para cada vértice recebido. Portanto, todos os triângulos estão localizados um em cima do outro.

Vamos corrigir isso, fazendo com que as posições dos vértices de saída sejam compensadas em relação ao ponto de entrada.

 // Add to the top of the geometry shader. float3 pos = IN[0]; … // Update each assignment of o.pos. o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0)); … o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0)); … o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0)); 


Por que alguns vértices não criam um triângulo?

Embora tenhamos determinado que a primitiva de entrada será um triângulo , uma folha de grama é transmitida apenas de um dos pontos do triângulo, descartando os outros dois. É claro que podemos transferir uma folha de grama dos três pontos de entrada, mas isso levará ao fato de que os triângulos vizinhos criam excessivamente folhas de grama uma sobre a outra.

Ou você pode resolver esse problema usando malhas com o tipo de pontos de topologia como malhas de entrada do sombreador geométrico.

Os triângulos agora estão desenhados corretamente e sua base está localizada no pico que os emite. Antes de GrassPlane , torne o objeto GrassPlane inativo na cena e GrassBall objeto GrassBall . Queremos que a grama gere corretamente em diferentes tipos de superfícies, por isso é importante testá-la em malhas de diferentes formas.


Até agora, todos os triângulos são emitidos em uma direção, e não para fora da superfície da esfera. Para resolver esse problema, criaremos lâminas de grama em um espaço tangente .

2. Espaço tangente


Idealmente, gostaríamos de criar lâminas de grama, definindo uma largura, altura, curvatura e rotação diferentes, sem levar em consideração o ângulo da superfície a partir da qual a lâmina de grama é emitida. Simplificando, definimos uma folha de grama em um espaço local para o vértice que a emite e depois a transformamos para que seja local na malha . Esse espaço é chamado espaço tangente .


No espaço tangente, os eixos X , Y e Z são definidos em relação ao normal e à posição da superfície (no nosso caso, os vértices).

Como qualquer outro espaço, podemos definir o espaço tangente de um vértice com três vetores: direita , frente e cima . Usando esses vetores, podemos criar uma matriz para transformar a folha de grama da tangente para o espaço local.

Você pode acessar os vetores diretamente e adicionando novos dados de vértice de entrada.

 // Add to the CGINCLUDE block. struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct vertexOutput { float4 vertex : SV_POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; … // Modify the vertex shader. vertexOutput vert(vertexInput v) { vertexOutput o; o.vertex = v.vertex; o.normal = v.normal; o.tangent = v.tangent; return o; } … // Modify the input for the geometry shader. Note that the SV_POSITION semantic is removed. void geo(triangle vertexOutput IN[3], inout TriangleStream<geometryOutput> triStream) … // Modify the existing line declaring pos. float3 pos = IN[0].vertex; 

O terceiro vetor pode ser calculado levando o produto vetorial entre dois outros. Um produto vetorial retorna um vetor perpendicular a dois vetores recebidos.

 // Place in the geometry shader, below the line declaring float3 pos. float3 vNormal = IN[0].normal; float4 vTangent = IN[0].tangent; float3 vBinormal = cross(vNormal, vTangent) * vTangent.w; 

Por que o resultado do produto vetorial é multiplicado pela coordenada da tangente w?
Ao exportar uma malha de um editor 3D, ele geralmente possui binormais (também chamados tangentes a dois pontos ) já armazenados nos dados da malha. Em vez de importar esses binormais, o Unity simplesmente toma a direção de cada binormal e os atribui à coordenada da tangente w . Isso permite economizar memória e, ao mesmo tempo, fornecer a capacidade de recriar o binormal correto. Uma discussão detalhada deste tópico pode ser encontrada aqui .

Tendo todos os três vetores, podemos criar uma matriz para a transformação entre os espaços tangente e local. Multiplicaremos cada vértice da folha de grama por essa matriz antes de passá-lo para o UnityObjectToClipPos , que espera um vértice no espaço local.

 // Add below the lines declaring the three vectors. float3x3 tangentToLocal = float3x3( vTangent.x, vBinormal.x, vNormal.x, vTangent.y, vBinormal.y, vNormal.y, vTangent.z, vBinormal.z, vNormal.z ); 

Antes de usar a matriz, transferimos o código de saída do vértice para a função, para não escrever as mesmas linhas de código repetidamente. Isso é chamado de princípio DRY ou não se repita .

 // Add to the CGINCLUDE block. geometryOutput VertexOutput(float3 pos) { geometryOutput o; o.pos = UnityObjectToClipPos(pos); return o; } … // Remove the following from the geometry shader. //geometryOutput o; //o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0)); //triStream.Append(o); //o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0)); //triStream.Append(o); //o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0)); //triStream.Append(o); // ...and replace it with the code below. triStream.Append(VertexOutput(pos + float3(0.5, 0, 0))); triStream.Append(VertexOutput(pos + float3(-0.5, 0, 0))); triStream.Append(VertexOutput(pos + float3(0, 1, 0))); 

Finalmente, multiplicamos os vértices de saída pela matriz tangentToLocal , alinhando-os corretamente com o normal do ponto de entrada.

 triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 1, 0)))); 

imagem

É mais como o que precisamos, mas não exatamente. O problema aqui é que, inicialmente, atribuímos a direção "para cima" (para cima) do eixo Y ; no entanto, no espaço tangente, a direção para cima geralmente está localizada ao longo do eixo Z. Agora vamos fazer essas alterações.

 // Modify the position of the third vertex being emitted. triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)))); 


3. Aparência de grama


Para fazer com que os triângulos pareçam mais com folhas de grama, você precisa adicionar cores e variações. Começamos adicionando um gradiente descendo do topo da folha de grama.

3.1 gradiente de cor


Nosso objetivo é permitir que o artista defina duas cores - superior e inferior e interpole entre essas duas cores que ele inclina para a base da folha da grama. Essas cores já estão definidas no arquivo shader como _TopColor e _BottomColor . Para uma amostragem adequada, você precisa passar as coordenadas UV para o shader do fragmento.

 // Add to the geometryOutput struct. float2 uv : TEXCOORD0; … // Modify the VertexOutput function signature. geometryOutput VertexOutput(float3 pos, float2 uv) … // Add to VertexOutput, just below the line assigning o.pos. o.uv = uv; … // Modify the existing lines in the geometry shader. triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 0, 1)), float2(0.5, 1))); 

Criamos coordenadas UV para uma folha de grama na forma de um triângulo, cujos dois vértices estão localizados na parte inferior esquerda e direita e a parte superior da ponta está localizada no centro na parte superior.


Coordenadas UV dos três vértices das lâminas de grama. Embora pintemos as lâminas de grama com um gradiente simples, um arranjo semelhante de texturas permite sobrepor texturas.

Agora podemos provar as cores superior e inferior no shader de fragmentos com UV e, em seguida, interpolar com o lerp . Também precisaremos modificar os parâmetros do shader de fragmento, tornando geometryOutput como entrada, e não apenas a posição do float4 .

 // Modify the function signature of the fragment shader. float4 frag (geometryOutput i, fixed facing : VFACE) : SV_Target … // Replace the existing return call. return float4(1, 1, 1, 1); return lerp(_BottomColor, _TopColor, i.uv.y); 


3.2 Direção aleatória da lâmina


Para criar variabilidade e dar à grama uma aparência mais natural, faremos com que cada lâmina de grama pareça em uma direção aleatória. Para fazer isso, precisamos criar uma matriz de rotação que gire a folha de grama uma quantidade aleatória em torno de seu eixo superior .

Existem duas funções no arquivo shader que nos ajudarão a fazer isso: rand , que gera um número aleatório a partir da entrada tridimensional, e AngleAxis3x3 , que recebe o ângulo (em radianos ) e retorna uma matriz que gira esse valor em torno do eixo especificado. A última função funciona exatamente da mesma forma que a função C # Quaternion.AngleAxis (somente AngleAxis3x3 retorna uma matriz, não um quaternion).

A função rand retorna um número no intervalo de 0 a 1; multiplicamos por 2 Pi para obter toda a gama de valores angulares.

 // Add below the line declaring the tangentToLocal matrix. float3x3 facingRotationMatrix = AngleAxis3x3(rand(pos) * UNITY_TWO_PI, float3(0, 0, 1)); 

Usamos a posição pos entrada como semente para uma rotação aleatória. Devido a isso, cada folha de grama terá sua própria rotação, constante em cada quadro.

A rotação pode ser aplicada à folha de grama multiplicando-a pela matriz tangentToLocal criada. Observe que a multiplicação da matriz não é comutativa ; a ordem dos operandos é importante .

 // Add below the line declaring facingRotationMatrix. float3x3 transformationMatrix = mul(tangentToLocal, facingRotationMatrix); … // Replace the multiplication matrix operand with our new transformationMatrix. triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0.5, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-0.5, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, 1)), float2(0.5, 1))); 


3.3 Curvatura aleatória


Se todas as lâminas de grama estiverem perfeitamente alinhadas, elas aparecerão iguais. Isso pode ser adequado para grama bem cuidada, por exemplo, em um gramado aparado, mas na natureza a grama não cresce assim. Criaremos uma nova matriz para girar a grama ao longo do eixo X , bem como uma propriedade para controlar essa rotação.

 // Add as a new property. _BendRotationRandom("Bend Rotation Random", Range(0, 1)) = 0.2 … // Add to the CGINCLUDE block. float _BendRotationRandom; … // Add to the geometry shader, below the line declaring facingRotationMatrix. float3x3 bendRotationMatrix = AngleAxis3x3(rand(pos.zzx) * _BendRotationRandom * UNITY_PI * 0.5, float3(-1, 0, 0)); 

Novamente, usamos a posição da folha de grama como uma semente aleatória, desta vez varrendo-a para criar uma semente única. Também multiplicaremos UNITY_PI por 0,5 ; isso nos dará um intervalo aleatório de 0 a 90 graus.

Novamente, aplicamos essa matriz por rotação, multiplicando tudo na ordem correta.

 // Modify the existing line. float3x3 transformationMatrix = mul(mul(tangentToLocal, facingRotationMatrix), bendRotationMatrix); 


3.4 Largura e altura


Enquanto o tamanho da folha de grama é limitado a uma largura de 1 unidade e a uma altura de 1 unidade. Adicionaremos propriedades para controlar o tamanho, bem como propriedades para adicionar variação aleatória.

 // Add as new properties. _BladeWidth("Blade Width", Float) = 0.05 _BladeWidthRandom("Blade Width Random", Float) = 0.02 _BladeHeight("Blade Height", Float) = 0.5 _BladeHeightRandom("Blade Height Random", Float) = 0.3 … // Add to the CGINCLUDE block. float _BladeHeight; float _BladeHeightRandom; float _BladeWidth; float _BladeWidthRandom; … // Add to the geometry shader, above the triStream.Append calls. float height = (rand(pos.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight; float width = (rand(pos.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth; … // Modify the existing positions with our new height and width. triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(width, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(-width, 0, 0)), float2(1, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrix, float3(0, 0, height)), float2(0.5, 1))); 


Os triângulos agora são muito mais parecidos com folhas de grama, mas também muito pouco. Simplesmente não há picos suficientes na malha de entrada para criar a impressão de um campo densamente coberto de vegetação.

Uma solução é criar uma nova malha mais densa, usando C # ou em um editor 3D. Isso funcionará, mas não nos permitirá controlar dinamicamente a densidade da grama. Em vez disso, dividiremos a malha de entrada usando mosaico .

4. Mosaico


O mosaico é um estágio opcional do pipeline de renderização, executado após o shader de vértice e antes do shader geométrico (se houver). Sua tarefa é subdividir uma superfície de entrada em muitas primitivas. O mosaico é implementado em duas etapas programáveis: sombreadores de casco e domínio .

Para shaders de superfície, o Unity possui uma implementação de mosaico embutida . No entanto, como não usamos shaders de superfície, teremos que implementar nossos próprios shaders de domínio e shell. Neste artigo, não discutirei a implementação do mosaico em detalhes e simplesmente usamos o arquivo CustomTessellation.cginc existente. Este arquivo foi adaptado do artigo Catlike Coding , que é uma excelente fonte de informações sobre a implementação de mosaico no Unity.

Se incluirmos o objeto TessellationExample na cena, veremos que ele já possui material que implementa o mosaico. Alterar a propriedade Uniforme de mosaico demonstra o efeito de subdivisão.


Implementamos mosaico no shader de grama para controlar a densidade do avião e, portanto, controlar o número de lâminas de grama geradas. Primeiro, você precisa adicionar o arquivo CustomTessellation.cginc . Vamos nos referir a ele por seu caminho relativo para o sombreador.

 // Add inside the CGINCLUDE block, below the other #include statements. #include "Shaders/CustomTessellation.cginc" 

Se você abrir CustomTessellation.cginc , notará que as vertexOutput e vertexOutput , bem como os shaders de vértice, já estão definidos nela. Não há necessidade de redefini-las em nosso shader de grama; eles podem ser excluídos.

 /*struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct vertexOutput { float4 vertex : SV_POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; }; vertexOutput vert(vertexInput v) { vertexOutput o; o.vertex = v.vertex; o.normal = v.normal; o.tangent = v.tangent; return o; }*/ 

Observe que o sombreador de vértice vert no CustomTessellation.cginc simplesmente passa a entrada diretamente para o estágio de mosaico; a função vertexOutput , chamada dentro do sombreador de domínio, assume a tarefa de criar a estrutura vertexOutput .

Agora podemos adicionar sombreadores de concha e domínio ao sombreador de grama. Também adicionaremos uma nova propriedade _TessellationUniform para controlar o tamanho da unidade - a variável correspondente a essa propriedade já foi declarada em CustomTessellation.cginc .

 // Add as a new property. _TessellationUniform("Tessellation Uniform", Range(1, 64)) = 1 … // Add below the other #pragma statements in the SubShader Pass. #pragma hull hull #pragma domain domain 

Agora, alterar a propriedade Uniforme de mosaico nos permite controlar a densidade da grama. Descobri que bons resultados são obtidos com o valor 5 .


5. O vento


Implementamos o vento amostrando a textura da distorção . Essa textura parecerá um mapa normal , apenas nele haverá apenas dois em vez de três canais. Usaremos esses dois canais como direções do vento ao longo de X e Y.


Antes de amostrar a textura do vento, precisamos criar uma coordenada UV. Em vez de usar as coordenadas de textura atribuídas à malha, aplicamos a posição do ponto de entrada. Graças a isso, se houver várias malhas de grama no mundo, será criada a ilusão de que elas fazem parte do mesmo sistema eólico. Também usamos a variável _Time shader para rolar a textura do vento ao longo da superfície da grama.

 // Add as new properties. _WindDistortionMap("Wind Distortion Map", 2D) = "white" {} _WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0) … // Add to the CGINCLUDE block. sampler2D _WindDistortionMap; float4 _WindDistortionMap_ST; float2 _WindFrequency; … // Add to the geometry shader, just above the line declaring the transformationMatrix. float2 uv = pos.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency * _Time.y; 

Aplicamos a escala e o deslocamento de _WindDistortionMap à posição e depois o deslocamos para _Time.y , dimensionado para _WindFrequency . Agora, usaremos esses UVs para provar a textura e criar uma propriedade para controlar a força do vento.

 // Add as a new property. _WindStrength("Wind Strength", Float) = 1 … // Add to the CGINCLUDE block. float _WindStrength; … // Add below the line declaring float2 uv. float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength; 

Observe que escalamos o valor amostrado da textura do intervalo 0 ... 1 para o intervalo -1 ... 1. Em seguida, podemos criar um vetor normalizado que denota a direção do vento.

 // Add below the line declaring float2 windSample. float3 wind = normalize(float3(windSample.x, windSample.y, 0)); 

Agora podemos criar uma matriz para girar em torno desse vetor e multiplicá-la pela nossa transformationMatrix .

 // Add below the line declaring float3 wind. float3x3 windRotation = AngleAxis3x3(UNITY_PI * windSample, wind); … // Modify the existing line. float3x3 transformationMatrix = mul(mul(mul(tangentToLocal, windRotation), facingRotationMatrix), bendRotationMatrix); 

Finalmente, transferimos a textura Wind (localizada na raiz do projeto) para o campo Mapa de distorção do vento do material da grama no editor Unity. Também definimos o parâmetro lado a lado da textura como 0.01, 0.01 .


Se a grama não estiver animando na janela Cena , clique no botão Alternar skybox, neblina e vários outros efeitos para ativar materiais animados.

, , , , - .


, ( ), ( ).

, , . windRotation bendRotationMatrix , .

 // Add below the line declaring float3x3 transformationMatrix. float3x3 transformationMatrixFacing = mul(tangentToLocal, facingRotationMatrix); … // Modify the existing lines outputting the base vertex positions. triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(width, 0, 0)), float2(0, 0))); triStream.Append(VertexOutput(pos + mul(transformationMatrixFacing, float3(-width, 0, 0)), float2(1, 0))); 

6.


. , , . , .

. , — , .

, . , ? tira triangular . Os três primeiros vértices se unem e formam um triângulo, e cada novo vértice forma um triângulo com os dois anteriores.


, triangle strip . .

, . , TriangleStream RestartStrip .

, maxvertexcount . #define , .

 // Add to the CGINCLUDE block. #define BLADE_SEGMENTS 3 … // Modify the existing line defining the maxvertexcount. [maxvertexcount(BLADE_SEGMENTS * 2 + 1)] 

3 maxvertexcount , .

for . : . .

, , . CGINCLUDE :

 geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float2 uv, float3x3 transformMatrix) { float3 tangentPoint = float3(width, 0, height); float3 localPosition = vertexPosition + mul(transformMatrix, tangentPoint); return VertexOutput(localPosition, uv); } 

, , VertexOutput . , , UV-. .

 // Update the existing code outputting the vertices. triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing)); triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing)); triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); 

, for . float width :

 for (int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; } 

, . t . 0...1, , . .

 // Add below the line declaring float t. float segmentHeight = height * t; float segmentWidth = width * (1 - t); 

, . GenerateGrassVertex , . GenerateGrassVertex , .

 // Add below the line declaring float segmentWidth. float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix; triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, float2(1, t), transformMatrix)); … // Add just below the loop to insert the vertex at the tip of the blade. triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); … // Remove the existing calls to triStream.Append. //triStream.Append(GenerateGrassVertex(pos, width, 0, float2(0, 0), transformationMatrixFacing)); //triStream.Append(GenerateGrassVertex(pos, -width, 0, float2(1, 0), transformationMatrixFacing)); //triStream.Append(GenerateGrassVertex(pos, 0, height, float2(0.5, 1), transformationMatrix)); 

float3x3 transformMatrix — : transformationMatrixFacing transformationMatrix .


, - — . , Y . -, GenerateGrassVertex , Y , forward .

 // Update the function signature of GenerateGrassVertex. geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float forward, float2 uv, float3x3 transformMatrix) … // Modify the Y coordinate assignment of tangentPoint. float3 tangentPoint = float3(width, forward, height); 

pow t . t forward .

 // Add as new properties. _BladeForward("Blade Forward Amount", Float) = 0.38 _BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2 … // Add to the CGINCLUDE block. float _BladeForward; float _BladeCurve; … // Add inside the geometry shader, below the line declaring float width. float forward = rand(pos.yyz) * _BladeForward; … // Add inside the loop, below the line declaring segmentWidth. float segmentForward = pow(t, _BladeCurve) * forward; … // Modify the GenerateGrassVertex calls inside the loop. triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix)); … // Modify the GenerateGrassVertex calls outside the loop. triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix)); 

, , . _BladeForward _BladeCurve , , .


7.


. , .

7.1


Unity . . , , .

CGINCLUDE , . , , , — , , .

 // Add below the existing Pass. Pass { Tags { "LightMode" = "ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma geometry geo #pragma fragment frag #pragma hull hull #pragma domain domain #pragma target 4.6 #pragma multi_compile_shadowcaster float4 frag(geometryOutput i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } 

, . LightMode ShadowCaster , ForwardBase — Unity, . multi_compile_shadowcaster . , , .

Fence ; , .


7.2


, Unity , , «» . .

 // Add to the geometryOutput struct. unityShadowCoord4 _ShadowCoord : TEXCOORD1; … // Add to the VertexOutput function, just above the return call. o._ShadowCoord = ComputeScreenPos(o.pos); 

ForwardBase float , , , . 0...1, 0 — , 1 — .

UV- _ShadowCoord?
Unity ( ). SHADOW_ATTENUATION . Autolight.cginc , , .

 #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) 

- , .

 // Add to the ForwardBase pass's fragment shader, replacing the existing return call. return SHADOW_ATTENUATION(i); //return lerp(_BottomColor, _TopColor, i.uv.y); 

, , . ForwardBase , .

 // Add to the ForwardBase pass's preprocessor directives, below #pragma target 4.6. #pragma multi_compile_fwdbase 


, ; , . , . Unity #if , .

 // Add at the end of the VertexOutput function, just above the return call. #if UNITY_PASS_SHADOWCASTER // Applying the bias prevents artifacts from appearing on the surface. o.pos = UnityApplyLinearShadowBias(o.pos); #endif 


.

?

(multisample anti-aliasing MSAA ) Unity , . , .

— , , Unity . ( ); Unity .

7.3


Implementaremos a iluminação usando um algoritmo de cálculo de iluminação difusa muito simples e comum.


N — , L — , I — . .

. , , .

Blade Curvature Amount 1 , : Y . , .

 // Add to the GenerateGrassVertex function, belowing the line declaring tangentPoint. float3 tangentNormal = float3(0, -1, 0); float3 localNormal = mul(transformMatrix, tangentNormal); 

tangentNormal , Y , , . VertexOutput , geometryOutput .

 // Modify the return call in GenerateGrassVertex. return VertexOutput(localPosition, uv, localNormal); … // Add to the geometryOutput struct. float3 normal : NORMAL; … // Modify the existing function signature. geometryOutput VertexOutput(float3 pos, float2 uv, float3 normal) … // Add to the VertexOutput function to pass the normal through to the fragment shader. o.normal = UnityObjectToWorldNormal(normal); 

, ; Unity , .

ForwardBase , .

 // Add to the ForwardBase fragment shader. float3 normal = facing > 0 ? i.normal : -i.normal; return float4(normal * 0.5 + 0.5, 1); // Remove the existing return call. //return SHADOW_ATTENUATION(i); 

Cull Off , . , VFACE , .

fixed facing , , , . , .


Blade Curvature Amount 1, Z forward , GenerateGrassVertex . Z .

 // Modify the existing line in GenerateGrassVertex. float3 tangentNormal = normalize(float3(0, -1, forward)); 

, , , . toon- .

 // Add to the ForwardBase fragment shader, below the line declaring float3 normal. float shadow = SHADOW_ATTENUATION(i); float NdotL = saturate(saturate(dot(normal, _WorldSpaceLightPos0)) + _TranslucentGain) * shadow; float3 ambient = ShadeSH9(float4(normal, 1)); float4 lightIntensity = NdotL * _LightColor0 + float4(ambient, 1); float4 col = lerp(_BottomColor, _TopColor * lightIntensity, i.uv.y); return col; // Remove the existing return call. //return float4(normal * 0.5 + 0.5, 1); 


Conclusão


10x10 . , . , . , .


, Standard Assets Unity. , .

, Unity GitHub , G-.

GitHub

:


. , .

: , , , , .

, , . ; , , .

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


All Articles