Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Tu primer shader 3D

You have decided to start writing your own custom Spatial shader. Maybe you saw a cool trick online that was done with shaders, or you have found that the StandardMaterial3D isn't quite meeting your needs. Either way, you have decided to write your own and now you need to figure out where to start.

Este tutorial explicará cómo escribir un Spatial shader y cubrirá más temas que el tutorial CanvasItem.

Los Spatial shaders tienen más funcionalidad incorporada que los shaders CanvasItem. Lo que se espera de los shaders espaciales es que Godot ya ha proporcionado la funcionalidad para los casos de uso común y todo lo que el usuario necesita hacer en el shader es establecer los parámetros adecuados. Esto es especialmente cierto para un flujo de trabajo de PBR (renderización basada en la física).

This is a two-part tutorial. In this first part we will create terrain using vertex displacement from a heightmap in the vertex function. In the second part we will take the concepts from this tutorial and set up custom materials in a fragment shader by writing an ocean water shader.

Nota

Este tutorial asume algunos conocimientos básicos de shader como los tipos (vec2, float, sampler2D), y las funciones. Si te sientes incómodo con estos conceptos es mejor obtener una introducción suave de The Book of Shaders antes de completar este tutorial.

Dónde asignar mi material

En 3D, los objetos se dibujan usando Meshes. Las mallas son un tipo de recurso que almacena la geometría (la forma de su objeto) y los materiales (el color y cómo el objeto reacciona a la luz) en unidades llamadas "superficies". Una Malla puede tener múltiples superficies, o sólo una. Típicamente, importarías una malla de otro programa (por ejemplo, Blender). Pero Godot también tiene unos cuantos PrimitiveMeshes que te permiten añadir geometría básica a una escena sin importar las Mallas.

There are multiple node types that you can use to draw a mesh. The main one is MeshInstance3D, but you can also use GPUParticles3D, MultiMeshes (with a MultiMeshInstance3D), or others.

Typically, a material is associated with a given surface in a mesh, but some nodes, like MeshInstance3D, allow you to override the material for a specific surface, or for all surfaces.

If you set a material on the surface or mesh itself, then all MeshInstance3Ds that share that mesh will share that material. However, if you want to reuse the same mesh across multiple mesh instances, but have different materials for each instance then you should set the material on the MeshInstance3D.

For this tutorial we will set our material on the mesh itself rather than taking advantage of the MeshInstance3D's ability to override materials.

Configurando

Add a new MeshInstance3D node to your scene.

En la pestaña del inspector al lado de "Mesh" haga clic en "[empty]" y seleccione "New PlaneMesh". Luego haga clic en la imagen de un avión que aparece.

Esto agrega un PlaneMesh a nuestra escena.

Luego, en el viewport, haga clic en la esquina superior izquierda en el botón que dice "Perspectiva". Aparecerá un menú. En el medio del menú hay opciones para mostrar la escena. Selecciona 'Display Wireframe'.

Esto te permitirá ver los triángulos que componen el avión.

../../../_images/plane.png

Now set Subdivide Width and Subdivide Depth of the PlaneMesh to 32.

../../../_images/plane-sub-set.webp

You can see that there are now many more triangles in the MeshInstance3D. This will give us more vertices to work with and thus allow us to add more detail.

../../../_images/plane-sub.png

PrimitiveMeshes, like PlaneMesh, only have one surface, so instead of an array of materials there is only one. Click beside "Material" where it says "[empty]" and select "New ShaderMaterial". Then click the sphere that appears.

Ahora haz clic al lado de "Shader" donde dice "[empty]" y selecciona "New Shader".

El editor shader debería aparecer ahora y estás listo para empezar a escribir tu primer shader Espacial!

Magia Shader

../../../_images/shader-editor.webp

The new shader is already generated with a shader_type variable and the fragment() function. The first thing Godot shaders need is a declaration of what type of shader they are. In this case the shader_type is set to spatial because this is a spatial shader.

shader_type spatial;

For now ignore the fragment() function and define the vertex() function. The vertex() function determines where the vertices of your MeshInstance3D appear in the final scene. We will be using it to offset the height of each vertex and make our flat plane appear like a little terrain.

Definimos el shader de vértices así:

void vertex() {

}

Sin nada en la función vertex(), Godot usará su shader de vértices por defecto. Podemos empezar a hacer cambios fácilmente añadiendo una sola línea:

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

Añadiendo esta línea, deberías obtener una imagen como la de abajo.

../../../_images/cos.png

Bien, vamos a desempacar esto. El valor y del VERTEX se está incrementando. Y estamos pasando los componentes x y z del VERTEX como argumentos para cos y sin; eso nos da una apariencia de onda a través de los ejes x y z.

Lo que queremos lograr es el aspecto de pequeñas colinas; después de todo. cos y sin ya se parecen a las colinas. Lo hacemos escalando las entradas a las funciones cos y sin.

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.png

Esto se ve mejor, pero sigue siendo demasiado puntiagudo y repetitivo, hagámoslo un poco más interesante.

Mapa de altura con ruido

Noise(Ruido) es una herramienta muy popular para fingir el aspecto del terreno. Piense en ello como en la función del coseno, donde tiene colinas que se repiten, excepto que, con el ruido, cada colina tiene una altura diferente.

Godot provides the NoiseTexture2D resource for generating a noise texture that can be accessed from a shader.

Para acceder a una textura en un shader agregue el siguiente código cerca de la parte superior de su shader, fuera de la función vertex().

uniform sampler2D noise;

Esto te permitirá enviar una textura de ruido al shader. Ahora mira en el inspector debajo de tu material. Deberías ver una sección llamada "Shader Params". Si la abres, verás una sección llamada "noise".

Click beside it where it says "[empty]" and select "New NoiseTexture2D". Then in your NoiseTexture2D click beside where it says "Noise" and select "New FastNoiseLite".

Nota

FastNoiseLite es utilizado por NoiseTexture2D para generar un heightmap (mapa de altura).

Una vez que esté todo configurado, debería verse así.

../../../_images/noise-set.webp

Now, access the noise texture using the texture() function. texture() takes a texture as the first argument and a vec2 for the position on the texture as the second argument. We use the x and z channels of VERTEX to determine where on the texture to look up. Note that the PlaneMesh coordinates are within the [-1,1] range (for a size of 2), while the texture coordinates are within [0,1], so to normalize we divide by the size of the PlaneMesh by 2.0 and add 0.5. texture() returns a vec4 of the r, g, b, a channels at the position. Since the noise texture is grayscale, all of the values are the same, so we can use any one of the channels as the height. In this case we'll use the r, or x channel.

void vertex() {
  float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
  VERTEX.y += height;
}

Nota: xyzw es lo mismo que rgba en GLSL, así que en lugar de texture().x arriba, podríamos usar texture().r. Mira la ``Documentación OpenGL <https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Vectores>`_ para mayor detalle.

Usando este código puedes ver que la textura crea colinas de aspecto aleatorio.

../../../_images/noise.png

Ahora mismo es demasiado puntiagudo, queremos suavizar un poco las colinas. Para ello, usaremos un uniforme. Ya usaste un uniforme arriba para pasar la textura del ruido, ahora vamos a aprender cómo funcionan.

Uniforms

Las variables uniformes permiten pasar datos del juego al shader. Son muy útiles para controlar los efectos shader. Los uniformes pueden ser casi cualquier tipo de dato que se pueda usar en el shader. Para usar un uniforme, lo declaras en tu Shader usando la palabra clave uniform.

Hagamos un uniforme que cambie la altura del terreno.

uniform float height_scale = 0.5;

Godot lets you initialize a uniform with a value; here, height_scale is set to 0.5. You can set uniforms from GDScript by calling the function set_shader_parameter() on the material corresponding to the shader. The value passed from GDScript takes precedence over the value used to initialize it in the shader.

# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)

Nota

Changing uniforms in Spatial-based nodes is different from CanvasItem-based nodes. Here, we set the material inside the PlaneMesh resource. In other mesh resources you may need to first access the material by calling surface_get_material(). While in the MeshInstance3D you would access the material using get_surface_material() or material_override.

Remember that the string passed into set_shader_parameter() must match the name of the uniform variable in the Shader. You can use the uniform variable anywhere inside your Shader. Here, we will use it to set the height value instead of arbitrarily multiplying by 0.5.

VERTEX.y += height * height_scale;

Ahora se ve mucho mejor.

../../../_images/noise-low.png

Using uniforms, we can even change the value every frame to animate the height of the terrain. Combined with Tweens, this can be especially useful for animations.

Interactuar con la luz

Primero, apaga el marco de alambre. Para ello, haz clic en la parte superior izquierda del Viewport de nuevo, donde dice "Perspectiva", y selecciona "Visualización Normal".

../../../_images/normal.png

Fíjese en cómo el color de la malla se vuelve plano. Esto se debe a que la iluminación de la misma es plana. ¡Añadamos una luz!

First, we will add an OmniLight3D to the scene.

../../../_images/light.png

Se puede ver la luz que afecta al terreno, pero se ve rara. El problema es que la luz está afectando al terreno como si fuera plano. Esto se debe a que el shader de luz utiliza las normales de la Mesh para calcular la iluminación.

Los normales se almacenan en la Mesh, pero estamos cambiando la forma de la Malla en el shader, por lo que los normales ya no son correctos. Para arreglar esto, podemos recalcular los normales en el shader o usar una textura normal que corresponda a nuestro ruido. Godot hace ambas cosas fáciles para nosotros.

Puedes calcular la nueva normalidad manualmente en la función de vértice y luego sólo tienes que poner "NORMAL". Con "NORMAL", Godot hará todos los cálculos de iluminación difíciles para nosotros. Cubriremos este método en la próxima parte de este tutorial, por ahora leeremos normales de una textura.

En cambio, confiaremos en el NoiseTexture de nuevo para calcular las normales por nosotros. Lo hacemos pasando una segunda textura de ruido.

uniform sampler2D normalmap;

Set this second uniform texture to another NoiseTexture2D with another FastNoiseLite. But this time, check As Normalmap.

../../../_images/normal-set.webp

Ahora, porque este es un mapa normal y no un normal por vértice, vamos a asignarlo en la función fragment(). La función fragment() se explicará con más detalle en la siguiente parte de este tutorial.

void fragment() {
}

When we have normals that correspond to a specific vertex we set NORMAL, but if you have a normalmap that comes from a texture, set the normal using NORMAL_MAP. This way Godot will handle the wrapping of texture around the mesh automatically.

Por último, para asegurarnos de que estamos leyendo de los mismos lugares en la textura del ruido y la textura del mapa normal, vamos a pasar la posición VERTEX.xz de la función vertex() a la función fragment(). Lo hacemos con variaciones.

Sobre el vertex() define un vec2 llamado tex_position. Y dentro de la función vertex() asigna VERTEX.xz a la tex_position.

varying vec2 tex_position;

void vertex() {
  ...
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  ...
}

Y ahora podemos acceder a tex_position desde la función fragment().

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

Con los normales en su lugar la luz ahora reacciona a la altura de la malla de forma dinámica.

../../../_images/normalmap.png

Incluso podemos arrastrar la luz y la iluminación se actualizará automáticamente.

../../../_images/normalmap2.png

Aquí está el código completo de este tutorial. Puedes ver que no es muy largo ya que Godot maneja la mayoría de las cosas difíciles para ti.

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

Eso es todo para esta parte. Esperemos que ahora entiendas lo básico de los sombreadores de vértices en Godot. En la próxima parte de este tutorial escribiremos una función de fragmento para acompañar esta función de vértice y cubriremos una técnica más avanzada para convertir este terreno en un océano de olas en movimiento.