Crear un sombreador de césped en el motor de Unity


Este tutorial le mostrará cómo escribir un sombreador geométrico para generar briznas de hierba desde la parte superior de la malla entrante y utilizar la teselación para controlar la densidad de la hierba.

El artículo describe el proceso paso a paso de escribir un sombreador de césped en Unity. El sombreador recibe la malla entrante, y de cada vértice de la malla genera una brizna de hierba utilizando el sombreador geométrico . En aras del interés y el realismo, las briznas de hierba tendrán un tamaño y rotación aleatorios , y también se verán afectados por el viento . Para controlar la densidad de la hierba, utilizamos teselación para separar la malla entrante. La hierba podrá proyectar y recibir sombras.

El proyecto terminado se publica al final del artículo. El archivo de sombreador generado contiene una gran cantidad de comentarios que facilitan la comprensión.

Requisitos


Para completar este tutorial, necesitará conocimientos prácticos sobre el motor de Unity y una comprensión inicial de la sintaxis y la funcionalidad de los sombreadores.

Descargue el borrador del proyecto (.zip) .

Llegar al trabajo


Descargue el borrador del proyecto y ábralo en el editor de Unity. Abra la escena Main y luego abra el sombreador Grass en su editor de código.

Este archivo contiene un sombreador que produce color blanco, así como algunas funciones que usaremos en este tutorial. Notará que estas funciones junto con el sombreador de vértices están incluidas en el bloque CGINCLUDE ubicado fuera de SubShader . El código colocado en este bloque se incluirá automáticamente en todos los pases en el sombreador; Esto será útil más tarde porque nuestro sombreador tendrá varias pasadas.

Comenzaremos escribiendo un sombreador geométrico que genere triángulos a partir de cada vértice en la superficie de nuestra malla.

1. Shaders geométricos


Los sombreadores geométricos son una parte opcional de la canalización de renderizado. Se ejecutan después del sombreador de vértices (o sombreador de teselación si se utiliza la teselación) y antes de que se procesen los vértices para el sombreador de fragmentos.


Direct3D Graphics Pipeline 11. Observe que en este diagrama el sombreador de fragmentos se llama sombreador de píxeles .

Los sombreadores geométricos reciben una primitiva única en la entrada y pueden generar cero, una o muchas primitivas. Comenzaremos escribiendo un sombreador geométrico que reciba un vértice (o punto ) en la entrada, y que alimente un triángulo que represente una brizna de hierba.

 // 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 

El código anterior declara un sombreador geométrico llamado geo con dos parámetros. El primer triangle float4 IN[3] informa que tomará un triángulo (que consta de tres puntos) como entrada. El segundo, como TriangleStream , configura un sombreador para generar una secuencia de triángulos para que cada vértice use la estructura geometryOutput para transmitir sus datos.

Dijimos anteriormente que el sombreador recibirá un vértice y generará una brizna de hierba. ¿Por qué entonces obtenemos un triángulo?
Será menos costoso tomar un como entrada. Esto se puede hacer de la siguiente manera.

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

Sin embargo, dado que nuestra malla entrante (en este caso GrassPlane10x10 , ubicada en la carpeta Mesh ) tiene una topología de triángulo , esto provocará una falta de coincidencia entre la topología de malla entrante y la primitiva de entrada requerida. Aunque esto está permitido en DirectX HLSL, no está permitido en OpenGL , por lo que se mostrará un error.

Además, agregamos el último parámetro entre corchetes sobre la declaración de la función: [maxvertexcount(3)] . Él le dice a la GPU que sacaremos (pero no estamos obligados a hacerlo) no más de 3 vértices. También hacemos que SubShader use un sombreador geométrico al declararlo dentro de Pass .

Nuestro sombreador geométrico no está haciendo nada todavía; Para dibujar un triángulo, agregue el siguiente código dentro del sombreador 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); 


Esto dio resultados muy extraños. Cuando mueve la cámara, queda claro que el triángulo se representa en el espacio de la pantalla . Esto es lógico: dado que el sombreador geométrico se ejecuta inmediatamente antes de procesar los vértices, le quita al sombreador de vértices la responsabilidad de que los vértices se muestren en el espacio de truncamiento . Cambiaremos nuestro código para reflejar esto.

 // 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)); 


Ahora nuestro triángulo se representa correctamente en el mundo. Sin embargo, parece que solo se crea uno. De hecho, se dibuja un triángulo para cada vértice de nuestra malla, pero las posiciones asignadas a los vértices del triángulo son constantes ; no cambian para cada vértice entrante. Por lo tanto, todos los triángulos están ubicados uno encima del otro.

Arreglaremos esto haciendo que las posiciones de vértice salientes se desplacen en relación con el punto entrante.

 // 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 qué algunos vértices no crean un triángulo?

Aunque hemos determinado que la primitiva entrante será un triángulo , una brizna de hierba se transmite solo desde uno de los puntos del triángulo, descartando los otros dos. Por supuesto, podemos transferir una brizna de hierba desde los tres puntos entrantes, pero esto conducirá al hecho de que los triángulos vecinos crean excesivamente briznas de hierba una encima de la otra.

O puede resolver este problema tomando mallas que tengan el tipo de puntos de topología como mallas entrantes del sombreador geométrico.

Los triángulos ahora se dibujan correctamente, y su base se encuentra en el pico que los emite. Antes de continuar, haga que el objeto GrassPlane inactivo en la escena y GrassBall objeto GrassBall . Queremos que el césped se genere correctamente en diferentes tipos de superficies, por lo que es importante probarlo en mallas de diferentes formas.


Hasta ahora, todos los triángulos se emiten en una dirección, y no hacia afuera de la superficie de la esfera. Para resolver este problema, crearemos briznas de hierba en un espacio tangente .

2. Espacio tangente


Idealmente, nos gustaría crear briznas de hierba estableciendo un ancho, altura, curvatura y rotación diferentes, sin tener en cuenta el ángulo de la superficie desde la que se emite la brizna de hierba. En pocas palabras, definimos una brizna de hierba en un espacio local para el vértice que la emite , y luego la transformamos para que sea local a la malla . Este espacio se llama espacio tangente .


En el espacio tangente, los ejes X , Y y Z se definen en relación con la posición normal de la superficie (en nuestro caso, los vértices).

Como cualquier otro espacio, podemos definir el espacio tangente de un vértice con tres vectores: derecho , adelante y arriba . Usando estos vectores, podemos crear una matriz para convertir la brizna de hierba de la tangente al espacio local.

Puede acceder a los vectores a la derecha y hacia arriba agregando nuevos datos 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; 

El tercer vector puede calcularse tomando el producto del vector entre otros dos. Un producto vectorial devuelve un vector perpendicular a dos vectores entrantes.

 // 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 qué el resultado del producto vectorial se multiplica por la coordenada de la tangente w?
Al exportar una malla desde un editor 3D, generalmente tiene binormales (también llamados tangentes a dos puntos ) ya almacenados en los datos de la malla. En lugar de importar estos binormales, Unity simplemente toma la dirección de cada binormal y los asigna a la coordenada de la tangente w . Esto le permite ahorrar memoria y, al mismo tiempo, ofrece la posibilidad de recrear el binormal correcto. Una discusión detallada de este tema se puede encontrar aquí .

Teniendo los tres vectores, podemos crear una matriz para la transformación entre tangentes y espacios locales. Multiplicaremos cada vértice de la brizna de hierba por esta matriz antes de pasarlo a UnityObjectToClipPos , que espera un vértice en el espacio 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 la matriz, transferimos el código de salida del vértice a la función para no escribir las mismas líneas de código una y otra vez. Esto se llama el principio DRY , o no 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 los vértices de salida por la matriz tangentToLocal , alineándolos correctamente con la normalidad de su punto 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)))); 

imagen

Esto es más parecido a lo que necesitamos, pero no del todo correcto. El problema aquí es que inicialmente asignamos la dirección "arriba" (arriba) del eje Y ; sin embargo, en el espacio tangente, la dirección hacia arriba generalmente se encuentra a lo largo del eje Z. Ahora haremos estos cambios.

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


3. Apariencia de hierba


Para que los triángulos se vean más como briznas de hierba, debes agregar colores y variaciones. Comenzamos agregando un gradiente que baja desde la parte superior de la brizna de hierba.

3.1 gradiente de color


Nuestro objetivo es permitir que el artista establezca dos colores: superior e inferior, e interpolar entre estos dos colores, inclinar a la base de la brizna de hierba. Estos colores ya están definidos en el archivo de sombreado como _TopColor y _BottomColor . Para su muestreo adecuado, debe pasar las coordenadas UV al sombreador de fragmentos.

 // 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))); 

Creamos coordenadas UV para una brizna de hierba en forma de triángulo, cuyos dos vértices de la base se encuentran en la parte inferior izquierda y derecha, y el vértice de la punta se encuentra en el centro en la parte superior.


Coordenadas UV de los tres vértices de las briznas de hierba. Aunque pintamos las briznas de hierba con un degradado simple, una disposición similar de texturas le permite superponer texturas.

Ahora podemos muestrear los colores superior e inferior en el fragment shader con UV y luego interpolarlos con lerp . También necesitaremos modificar los parámetros del sombreador de fragmentos, haciendo geometryOutput como entrada, y no solo la posición de 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 Dirección aleatoria de la cuchilla


Para crear variabilidad y darle al césped un aspecto más natural, haremos que cada brizna se vea en una dirección aleatoria. Para hacer esto, necesitamos crear una matriz de rotación que rote la brizna de hierba una cantidad aleatoria alrededor de su eje superior .

Hay dos funciones en el archivo de sombreador que nos ayudarán a hacer esto: rand , que genera un número aleatorio a partir de la entrada tridimensional, y AngleAxis3x3 , que recibe el ángulo (en radianes ) y devuelve una matriz que gira este valor alrededor del eje especificado. La última función funciona exactamente igual que la función C # Quaternion.AngleAxis (solo AngleAxis3x3 devuelve una matriz, no un cuaternión).

La función rand devuelve un número en el rango 0 ... 1; lo multiplicamos por 2 Pi para obtener el rango completo de valores angulares.

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

Utilizamos la posición de entrada como semilla para una rotación aleatoria. Debido a esto, cada brizna de hierba tendrá su propia rotación, constante en cada cuadro.

La rotación se puede aplicar a la brizna de hierba multiplicándola por la matriz tangentToLocal creada. Tenga en cuenta que la multiplicación de matrices no es conmutativa ; El orden de los operandos es 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 Doblado aleatorio hacia adelante


Si todas las briznas de hierba están perfectamente alineadas, aparecerán igual. Esto puede ser adecuado para césped bien cuidado, por ejemplo, en un césped recortado, pero en la naturaleza el césped no crece así. Crearemos una nueva matriz para rotar el césped a lo largo del eje X , así como una propiedad para controlar esta rotación.

 // 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)); 

Nuevamente usamos la posición de la brizna de hierba como una semilla aleatoria, esta vez barriendo para crear una semilla única. También multiplicaremos UNITY_PI por 0.5 ; Esto nos dará un intervalo aleatorio de 0 ... 90 grados.

Nuevamente aplicamos esta matriz a través de la rotación, multiplicando todo en el orden correcto.

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


3.4 Ancho y alto


Mientras que el tamaño de la brizna de hierba se limita a un ancho de 1 unidad y una altura de 1 unidad. Agregaremos propiedades para controlar el tamaño, así como propiedades para agregar variación aleatoria.

 // 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))); 


Los triángulos ahora son mucho más como briznas de hierba, pero también muy poco. Simplemente no hay suficientes picos en la malla entrante para crear la impresión de un campo densamente cubierto.

Una solución es crear una nueva malla más densa, ya sea usando C # o en un editor 3D. Esto funcionará, pero no nos permitirá controlar dinámicamente la densidad del césped. En su lugar, dividiremos la malla entrante mediante teselación .

4. Teselación


La teselación es una etapa opcional de la canalización de representación, realizada después del sombreador de vértices y antes del sombreador geométrico (si lo hay). Su tarea es subdividir una superficie entrante en muchas primitivas. La teselación se implementa en dos pasos programables: sombreadores de casco y dominio .

Para los sombreadores de superficie, Unity tiene una implementación de teselación incorporada . Sin embargo, dado que no usamos sombreadores de superficie, tendremos que implementar nuestros propios sombreadores de shell y dominio. En este artículo, no discutiré la implementación de teselación en detalle, y simplemente usaremos el archivo CustomTessellation.cginc existente. Este archivo está adaptado del artículo de Catlike Coding , que es una excelente fuente de información sobre la implementación de teselación en Unity.

Si incluimos el objeto TessellationExample en la escena, veremos que ya tiene material que implementa la teselación. Cambiar la propiedad Uniforme de teselación demuestra el efecto de subdivisión.


Implementamos teselación en el sombreador de hierba para controlar la densidad del plano y, por lo tanto, para controlar la cantidad de briznas de hierba generadas. Primero debe agregar el archivo CustomTessellation.cginc . Nos referiremos a él por su ruta relativa al sombreador.

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

Si abre CustomTessellation.cginc , notará que las vertexOutput vertexInput y vertexOutput , así como los sombreadores de vértices, ya están definidos en él. No es necesario redefinirlos en nuestro sombreador de hierba; Pueden ser eliminados.

 /*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; }*/ 

Tenga en cuenta que el sombreador de vértices vert en CustomTessellation.cginc simplemente pasa la entrada directamente a la etapa de teselación; La función vertexOutput , llamada dentro del sombreador de dominio, asume la tarea de crear la estructura vertexOutput .

Ahora podemos agregar sombreadores de shell y dominio al sombreador de hierba. También agregaremos una nueva propiedad _TessellationUniform para controlar el tamaño de la unidad: la variable correspondiente a esta propiedad ya se ha declarado en 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 

Ahora, cambiar la propiedad Uniforme de teselación nos permite controlar la densidad del césped. Encontré que se obtienen buenos resultados con un valor de 5 .


5. El viento


Implementamos el viento muestreando la textura de distorsión . Esta textura se verá como un mapa normal , solo en ella habrá solo dos en lugar de tres canales. Utilizaremos estos dos canales como direcciones del viento a lo largo de X e Y.


Antes de muestrear la textura del viento, necesitamos crear una coordenada UV. En lugar de usar las coordenadas de textura asignadas a la malla, aplicamos la posición del punto entrante. Gracias a esto, si hay varias mallas de hierba en el mundo, se creará la ilusión de que todas son parte del mismo sistema eólico. También utilizamos la variable incorporada _Time shader para desplazar la textura del viento a lo largo de la superficie de la hierba.

 // 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 la escala y el desplazamiento de _WindDistortionMap a la posición, y luego lo _Time.y a _Time.y , escalado a _WindFrequency . Ahora usaremos estos rayos UV para tomar muestras de la textura y crear una propiedad para controlar la fuerza del viento.

 // 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; 

Tenga en cuenta que escalamos el valor muestreado de la textura del intervalo 0 ... 1 al intervalo -1 ... 1. A continuación, podemos crear un vector normalizado que denote la dirección del viento.

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

Ahora podemos crear una matriz para rotar alrededor de este vector y multiplicarlo por nuestra matriz de 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 la textura Wind (ubicada en la raíz del proyecto) al campo Mapa de distorsión del viento del material de hierba en el editor de Unity. También establecemos el parámetro de mosaico de la textura en 0.01, 0.01 .


Si el césped no se está animando en la ventana de Escena , haga clic en el botón Alternar skybox, niebla y otros efectos para habilitar materiales 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.


Ahora las hojas individuales de hierba están definidas por un triángulo. A grandes distancias, esto no es un problema, pero cerca de la brizna de hierba se ven muy rígidos y geométricos, en lugar de orgánicos y vivos. Arreglaremos esto construyendo briznas de hierba a partir de varios triángulos y doblándolos a lo largo de la curva .

Cada brizna de hierba se dividirá en varios segmentos . Cada segmento tendrá una forma rectangular y constará de dos triángulos, con la excepción del segmento superior: será un triángulo que denota la punta de la brizna de hierba.

Hasta ahora, hemos dibujado solo tres vértices, creando un solo triángulo. ¿Cómo, entonces, si hay más vértices, el sombreador geométrico sabe cuáles unir y formar triángulos? La respuesta está en la estructura de datos.despojar triángulo . Los primeros tres vértices se unen y forman un triángulo, y cada nuevo vértice forma un triángulo con los dos anteriores.


Una brizna de hierba subdividida, representada como una franja triangular y creaba un vértice a la vez. Después de los primeros tres vértices, cada nuevo vértice forma un nuevo triángulo con los dos vértices anteriores.

Esto no solo es más eficiente en términos de uso de memoria, sino que también le permite crear de forma fácil y rápida secuencias de triángulos en su código. Si quisiéramos crear varias franjas de triángulos, podríamos llamar a RestartStrip para la TriangleStreamfunción . Antes de comenzar a dibujar más vértices desde el sombreador geométrico, necesitamos aumentarlo . Usaremos el diseño para permitir que el autor del sombreador controle el número de segmentos y calcule el número de vértices mostrados a partir de él.

maxvertexcount#define

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

Inicialmente, establecemos el número de segmentos en 3 y actualizamos maxvertexcountpara calcular el número de vértices en función del número de segmentos.

Para crear una brizna de hierba segmentada, usamos un ciclo for. Cada iteración del bucle agregará dos vértices : izquierdo y derecho . Después de completar la punta, agregamos el último vértice en la punta de la brizna de hierba.

Antes de hacer esto, será útil mover parte de la posición informática de los vértices de las hojas de hierba del código a la función, porque usaremos este código varias veces dentro y fuera del bucle. Agregue lo CGINCLUDEsiguiente al bloque :

 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); } 

Esta función realiza las mismas tareas porque pasa los argumentos que pasamos previamente VertexOutputpara generar los vértices de la brizna de hierba. Al obtener una posición, altura y ancho, transforma correctamente el vértice utilizando la matriz transmitida y le asigna una coordenada UV. Actualizaremos el código existente para que la función funcione correctamente.

 // 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)); 

La función comenzó a funcionar correctamente y estamos listos para mover el código de generación de vértices al bucle for. Agregue lo float widthsiguiente debajo de la línea :

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

Anunciamos un ciclo que se ejecutará una vez para cada segmento de brizna de hierba. Dentro del bucle, agregue una variable t. Esta variable almacenará un valor en el rango 0 ... 1, que indica qué tan lejos nos hemos movido a lo largo de la brizna de hierba. Usamos este valor para calcular el ancho y la altura del segmento en cada iteración del bucle.

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

Al subir una brizna de hierba, la altura aumenta y el ancho disminuye. Ahora podemos agregar llamadas al bucle GenerateGrassVertexpara agregar vértices a la secuencia de triángulos. También agregaremos una llamada GenerateGrassVertexfuera del bucle para crear la punta de la brizna de hierba.

 // 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)); 

Eche un vistazo a la línea con la declaración float3x3 transformMatrix: aquí seleccionamos una de las dos matrices de transformación: tomamos transformationMatrixFacinglos vértices de la base y transformationMatrixtodos los demás.


Las briznas de hierba ahora se dividen en muchos segmentos, pero la superficie de la brizna todavía es plana: los nuevos triángulos aún no están involucrados. Vamos a añadir una brizna de hierba curvatura, cambiando la posición del vértice de la Y . Primero, necesitamos modificar la función GenerateGrassVertexpara que obtenga un desplazamiento en Y , que llamaremos 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); 

Para calcular el desplazamiento de cada vértice, sustituimos un powvalor en la función t. Después de subir ta una potencia, su efecto sobre el desplazamiento hacia adelante será no lineal y convertirá la brizna de hierba en una curva.

 // 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)); 

Este es un código bastante extenso, pero todo el trabajo se realiza de manera similar al ancho y la altura de la brizna de hierba. Con valores más bajos _BladeForwardy _BladeCurveobtenemos un césped ordenado y bien cuidado, y valores más grandes darán el efecto contrario.


7. Iluminación y sombras.


Como paso final para completar el sombreador, agregaremos la capacidad de proyectar y recibir sombras. También agregaremos iluminación simple desde la fuente principal de luz direccional.

7.1 Sombras de fundición


Para proyectar sombras en Unity, debe agregar una segunda pasada al sombreador. Este pasaje será utilizado por las fuentes de luz creadoras de sombras en la escena para representar la profundidad de la hierba en su mapa de sombras . Esto significa que el sombreador geométrico tendrá que ser lanzado en el pasaje de sombra, para que las hojas de hierba puedan proyectar sombras.

Dado que el sombreador geométrico está escrito dentro de bloques CGINCLUDE, podemos usarlo en cualquier paso del archivo. Cree un segundo pase que use los mismos sombreadores que el primero, con la excepción del sombreador de fragmentos: definiremos uno nuevo en el que escribiremos una macro que procese la salida.

 // 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 } 

Además de crear un nuevo sombreador de fragmentos, hay un par de diferencias importantes en este pasaje. La etiqueta LightModeimporta ShadowCaster, no ForwardBase: esto le dice a Unity que este pasaje debe usarse para representar el objeto en mapas de sombras. También hay una directiva de preprocesador aquí multi_compile_shadowcaster. Asegura que el sombreador compila todas las opciones necesarias para proyectar sombras.

Haz que el objeto del juego esté Fence activo en la escena; entonces obtenemos una superficie sobre la cual las briznas de hierba pueden proyectar una sombra.


7.2 Obteniendo sombras


Después de que Unity renderiza el mapa de sombras desde el punto de vista de la fuente de luz que crea la sombra, lanza un pasaje que "recoge" las sombras en la textura del espacio de la pantalla . Para muestrear esta textura, necesitaremos calcular las posiciones de los vértices en el espacio de la pantalla y transferirlos al sombreador de fragmentos.

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

En el sombreador de fragmentos del pasaje, ForwardBasepodemos usar una macro para obtener un valor que floatindique si la superficie está en sombras o no. Este valor está en el rango 0 ... 1, donde 0 es sombreado completo, 1 es iluminación completa.

¿Por qué la coordenada UV del espacio de la pantalla se llama _ShadowCoord? Esto no cumple con las convenciones de nomenclatura anteriores.
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); 

Finalmente, necesitamos hacer que el sombreador esté configurado correctamente para recibir sombras. Para hacer esto, agregaremos una ForwardBasedirectiva de preprocesador al pase para que compile todas las opciones de sombreador necesarias.

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


Al acercar la cámara, podemos observar artefactos en la superficie de las briznas de hierba; son causadas por el hecho de que las hojas individuales de hierba proyectan sombras sobre sí mismas. Podemos solucionar esto aplicando un desplazamiento lineal o moviendo las posiciones de los vértices en el espacio de truncamiento ligeramente lejos de la pantalla. Utilizaremos la macro Unity para esto y la incluiremos en el diseño #ifpara que la operación se realice solo en la ruta de la sombra.

 // 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 


Después de aplicar el desplazamiento de sombra lineal, los artefactos de sombra en forma de rayas desaparecen de la superficie de los triángulos.

¿Por qué hay artefactos a lo largo de los bordes de las hojas de hierba sombreadas?

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

— , , Unity . ( ); Unity .

7.3 Iluminación


Implementaremos la iluminación utilizando un algoritmo de cálculo de iluminación difusa muy simple y común.


... donde N es lo normal a la superficie, L es la dirección normalizada de la fuente principal de iluminación direccional, e I es la iluminación calculada. En este tutorial no implementaremos iluminación indirecta.

Por el momento, las normales no están asignadas a los vértices de las briznas de hierba. Al igual que con las posiciones de vértice, primero calculamos las normales en el espacio tangente y luego las convertimos a locales.

Cuando la cantidad de curvatura de la hoja es 1 , todas las hojas de hierba en el espacio tangente se dirigen en una dirección: directamente opuestas al eje Y. Como primer paso de nuestra solución, calculamos lo normal, suponiendo que no haya curvatura.

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

tangentNormal, definido como directamente opuesto al eje Y , se transforma mediante la misma matriz que usamos para convertir los puntos tangentes al espacio local. Ahora podemos pasarlo a una función VertexOutput, y luego a una estructura 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); 

Note que antes de la conclusión, transformamos lo normal en espacio mundial ; La unidad transmite a los sombreadores la dirección de la fuente principal de luz direccional en el espacio mundial, por lo que esta transformación es necesaria.

Ahora podemos visualizar las normales en el fragmento de sombreador ForwardBasepara verificar el resultado de nuestro trabajo.

 // 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); 

Como Cullse asigna un valor en nuestro sombreador Off, se representan ambos lados de la brizna de hierba. Para que lo normal se dirija en la dirección correcta, usamos un parámetro auxiliar VFACEque agregamos al sombreador de fragmentos.

El argumento fixed facingdevolverá un número positivo si mostramos la cara frontal de la superficie, y un número negativo si es lo contrario. Usamos esto en el código anterior para voltear lo normal si es necesario.


Cuando la cantidad de curvatura de la cuchilla es mayor que 1, la posición Z tangente de cada vértice se desplazará por la cantidad que se forwardpasa a la función GenerateGrassVertex. Usaremos este valor para escalar proporcionalmente el eje Z de las normales.

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

Finalmente, agregue el código al sombreador de fragmentos para combinar las sombras, la iluminación direccional y la iluminación ambiental. Recomiendo estudiar información más detallada sobre la implementación de iluminación personalizada en sombreadores en mi tutorial sobre sombreadores de 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); 


Conclusión


En este tutorial, el césped cubre un área pequeña de 10x10 unidades. Para que el sombreador cubra grandes espacios abiertos mientras mantiene un alto rendimiento, se deben introducir optimizaciones. Puede aplicar la teselación en función de la distancia, de modo que se eliminen menos briznas de hierba de la cámara. Además, a largas distancias, en lugar de hojas individuales de hierba, se pueden dibujar grupos de hojas de hierba usando un solo cuadrilátero con una textura superpuesta.


, Standard Assets Unity. , .

, Unity GitHub , G-.

GitHub

:


Sin interoperabilidad, los efectos gráficos pueden parecer estáticos o sin vida para los jugadores. Este tutorial ya es muy largo, por lo que no agregué una sección sobre la interacción de los objetos del mundo con la hierba.

Una implementación ingenua de hierbas interactivas contendría dos componentes: algo en el mundo del juego que puede transmitir datos al sombreador para decirle con qué parte de la hierba se está interactuando y codificar en el sombreador para interpretar estos datos. Aquí

se muestra un ejemplo de cómo se puede implementar esto con agua . Se puede adaptar para trabajar con hierba; en lugar de dibujar ondas en el lugar donde está el personaje, puedes girar la brizna de hierba hacia abajo para simular los efectos de los pasos.

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


All Articles