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.
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.
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.
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.
O terceiro vetor pode ser calculado levando o
produto vetorial entre dois outros. Um produto vetorial retorna um vetor
perpendicular a dois vetores recebidos.
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.
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 .
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))));
É 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.
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.
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
.
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.
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 .
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.
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.
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.
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.
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.
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
.
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.
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.
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.
Agora podemos criar uma matriz para girar em torno desse vetor e multiplicá-la pela nossa
transformationMatrix
.
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
, .
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
, .
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-. .
,
for
.
float width
:
for (int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; }
, .
t
. 0...1, , . .
, .
GenerateGrassVertex
, .
GenerateGrassVertex
, .
float3x3 transformMatrix
— :
transformationMatrixFacing
transformationMatrix
.
, - — . ,
Y . -,
GenerateGrassVertex
,
Y ,
forward
.
pow
t
.
t
forward
.
, , .
_BladeForward
_BladeCurve
, , .
7.
. , .
7.1
Unity .
. , , .
CGINCLUDE
, . , , , — , , .
, .
LightMode
ShadowCaster
,
ForwardBase
— Unity, .
multi_compile_shadowcaster
. , , .
Fence
; , .
7.2
, Unity , , «»
. .
ForwardBase
float
, , , . 0...1, 0 — , 1 — .
UV- _ShadowCoord?Unity ( ).
SHADOW_ATTENUATION
.
Autolight.cginc
, , .
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
- , .
, , .
ForwardBase
, .
, ; , . ,
. Unity
#if
, .
.?(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 . , .
tangentNormal
,
Y , , .
VertexOutput
,
geometryOutput
.
,
; Unity , .
ForwardBase
, .
Cull
Off
, . ,
VFACE
, .
fixed facing
, ,
, . , .
Blade Curvature Amount 1,
Z forward
,
GenerateGrassVertex
.
Z .
, , , .
toon- .
Conclusão
10x10 . , . , . , .
, Standard Assets Unity. , ., Unity
GitHub , G-.
GitHub:
. , .
: ,
, , ,
.
, ,
. ; , , .