Volver al Blog
Game Development5 de agosto de 202610 min de lectura

Shaders Personalizados en Godot. Mi Experiencia con 28 Shaders

DR

Doug Ribeiro

10 min de lectura

Cuando empecé a construir mi RPG táctico en Godot 4, un mundo 3D con sprites de personajes 2D, tenía una lista mental clara de cosas que no sabía hacer. Los shaders estaban en la cima de esa lista. No cerca de la cima. En la mismísima cima, subrayado dos veces. Sabía que los shaders eran programas que se ejecutaban en la GPU. Sabía que controlaban cómo se veían los píxeles. Más allá de eso, tenía un miedo difuso a las transformaciones de vértices y la suposición de que los shaders eran para especialistas con formación en programación gráfica, no para desarrolladores de aplicaciones que construían interfaces Angular para ganarse la vida.

Para febrero de 2026, cerca de un año después de empezar, el proyecto tenía 28 shaders personalizados. Lluvia con refracción y profundidad. Acumulación de nieve sobre superficies. Terreno destructible que revela una subsuperficie chamuscada debajo. Un ciclo dinámico de día/noche que cambia la temperatura de color en toda la escena. Shaders de resaltado de unidades que pulsan e indican el rango de movimiento. No contraté a un programador gráfico. Lo aprendí solo, principalmente a través de prueba y error, la documentación de Godot y mucho tiempo navegando ShaderToy.

Este post es el relato honesto de ese camino. Qué fue más difícil de lo esperado, qué fue sorprendentemente accesible y cómo se compara el entorno de shaders de Godot con GLSL puro.

El Lenguaje de Shaders de Godot vs GLSL Puro

Godot usa su propio lenguaje de shaders, que está basado en GLSL pero no es GLSL. Tiene los mismos tipos fundamentales. Vec2, vec3, vec4, sampler2D, mat4. Y las mismas funciones matemáticas. Pero añade built-ins específicos de Godot, oculta el boilerplate detrás de una estructura más limpia e integra directamente con el pipeline de materiales y renderizado.

Para un principiante, esto es una ventaja genuina. Escribir directamente contra la API gráfica, OpenGL o Vulkan, significa que gestionas tú mismo los vertex buffers, los uniform buffers y los framebuffers. El sistema de shaders de Godot maneja todo eso. Declaras un shader_type, implementas las funciones fragment() o vertex(), y Godot te pasa todo lo que necesitas. Puedes concentrarte en el efecto visual en lugar de la plomería de la API gráfica.

La trampa es que la documentación de Godot sobre shaders es completa pero no siempre amigable para principiantes. La referencia te dice qué hace cada built-in. No siempre te dice por qué lo usarías o cómo interactúan las cosas entre sí. Pasé mucho tiempo leyendo la documentación y luego leyendo el código fuente de los shaders propios de Godot para entender cómo estaban implementados los materiales built-in antes de poder extenderlos.

El Primer Shader: Resaltado de Unidad

Mi primer shader personalizado real fue un resaltado de unidad. El brillo pulsante que indica que una unidad está seleccionada en mi juego táctico. Premisa simple: tomar el sprite de la unidad, hacerlo brillar con un contorno pulsante y cambiar el color del brillo dependiendo de si la unidad es del jugador, un enemigo o un aliado.

Esperaba que esto tomara un día. Tomó cuatro. No porque el código del shader fuera complejo, el shader final tiene unas 30 líneas, sino porque no entendía los sistemas de coordenadas de Godot, no entendía cómo funcionaba el muestreo UV para contornos y seguía confundiendo el espacio de pantalla con el espacio de textura. El avance llegó cuando entendí que un efecto de contorno funciona muestreando píxeles vecinos y verificando si alguno de ellos es transparente donde el píxel actual es opaco. Eso es todo. Una vez que entendí eso, la implementación fue directa.

glsl
// Godot Shader Language, unit highlight with pulse
shader_type canvas_item;

uniform vec4 outline_color : source_color = vec4(1.0, 1.0, 0.0, 1.0);
uniform float outline_width : hint_range(0.0, 10.0) = 2.0;
uniform float pulse_speed : hint_range(0.0, 5.0) = 2.0;
uniform float pulse_min : hint_range(0.0, 1.0) = 0.4;

void fragment() {
    vec2 size = outline_width * TEXTURE_PIXEL_SIZE;
    vec4 color = texture(TEXTURE, UV);

    float alpha = 0.0;
    alpha = max(alpha, texture(TEXTURE, UV + vec2(size.x, 0.0)).a);
    alpha = max(alpha, texture(TEXTURE, UV - vec2(size.x, 0.0)).a);
    alpha = max(alpha, texture(TEXTURE, UV + vec2(0.0, size.y)).a);
    alpha = max(alpha, texture(TEXTURE, UV - vec2(0.0, size.y)).a);

    float pulse = pulse_min + (1.0 - pulse_min) *
        (0.5 + 0.5 * sin(TIME * pulse_speed));

    vec4 outline = outline_color * vec4(1.0, 1.0, 1.0, pulse);

    COLOR = mix(outline * (1.0 - color.a), color, color.a);
    COLOR.a = max(color.a, alpha * outline_color.a * pulse);
}

Shaders Climáticos: Lluvia y Nieve

Los efectos climáticos fueron los primeros shaders donde me sentí genuinamente orgulloso del resultado. Mi shader de lluvia se ejecuta como un efecto de CanvasLayer a pantalla completa y crea la ilusión de lluvia superponiendo múltiples texturas de ruido con desplazamiento a diferentes velocidades y opacidades, luego aplicando un pase de refracción que distorsiona ligeramente la escena debajo.

La lluvia en sí no fue la parte difícil. Desplazar una textura es una de las primeras cosas que enseña cualquier tutorial de shaders. La parte difícil fue la refracción, hacer que los trazos de lluvia distorsionen visiblemente lo que hay detrás, lo cual requiere leer de la textura de pantalla en una posición UV desplazada. Godot 4 la expone mediante un uniform sampler2D con hint_screen_texture (el viejo built-in SCREEN_TEXTURE ya no existe), pero la interacción entre el z-ordering del CanvasLayer y cuándo se captura la textura de pantalla es sutil y me hizo pasar una noche entera con renderizado incorrecto antes de entender el orden de captura.

La nieve fue en realidad más fácil que la lluvia porque no necesitaba refracción. El shader de nieve usa un enfoque de partículas dentro del shader. Genero posiciones de copos de nieve proceduralmente usando ondas sinusoidales y ruido fraccionario, lo que da una distribución natural sin necesitar un sistema de partículas real. El shader expone la fuerza y dirección del viento como uniforms, así que puedo animar el viento a través de GDScript durante los eventos climáticos.

La acumulación de nieve, el segundo shader de nieve, fue más interesante. Lee una máscara de textura personalizada que define dónde puede acumularse la nieve (superficies planas, partes superiores de muros) y progresivamente mezcla entre la textura base y una versión cubierta de nieve usando un uniform controlado por tiempo. Combinado con el shader de nieve cayendo, la escena transiciona de despejada a nevada de una manera que afecta tanto la atmósfera como el suelo.

El Ciclo Día/Noche: 6 Shaders Trabajando Juntos

Mi ciclo día/noche no es un solo shader. Son seis shaders que se coordinan entre sí a través de uniforms compartidos controlados por un nodo central DayCycleManager. Cada shader maneja un aspecto del cambio visual: temperatura de color general, intensidad de sombras, color de luz ambiental, gradiente del cielo, visibilidad de estrellas y posición de luna/sol.

Diseñar este sistema me enseñó la lección más importante de arquitectura de shaders que conozco: los shaders deben ser controlados por datos, no por tiempo directamente. Mi intento inicial hacía que cada shader leyera TIME independientemente y calculara la fase actual del día por su cuenta. El resultado fue una inconsistencia sutil. Cada shader llevaba su propia copia del cálculo de fase, así que pequeñas diferencias los iban separando, y ninguno seguía el reloj del juego: el TIME de Godot sigue corriendo mientras el juego está en pausa y da la vuelta cada 3.600 segundos por defecto.

El enfoque correcto: el DayCycleManager calcula un único valor normalizado day_progress de 0.0 a 1.0 en cada tick de _process() y lo pasa como uniform a los seis shaders. Cada shader usa el mismo valor en el mismo frame. La salida visual está perfectamente sincronizada.

DayCycleManager (C#)Envía el uniform day_progress (0.0 a 1.0) a todos los shaders en cada f… 1 Shader de gradiente del cieloInterpola entre las paletas de amanecer, día, atardecer y noche 2 Shader de temperatura de colorDesplaza toda la escena del amanecer cálido a la noche fría 3 Shader de luz ambientalAjusta la intensidad y el color de la luz ambiental 4 Shader de sombras de personajesCambia la dirección y suavidad de la sombra según el ángulo del sol 5 Shader de visibilidad de estrellasHace aparecer el campo de estrellas y el brillo lunar en valores noctur… 6 Shader de posición de sol / lunaUbica el disco del sol o la luna según el day_progress actual 7

Shaders de Entorno Destructible

Los shaders técnicamente más complejos del proyecto son los efectos de entorno destructible. Cuando un muro recibe daño, quiero que la superficie visible se agriete, se chamusque y eventualmente colapse. El shader para esto lee un uniform de valor de daño y lo usa para mezclar entre tres capas de textura: superficie intacta, superficie agrietada y subsuperficie chamuscada.

El efecto de agrietamiento usa un patrón de ruido Voronoi para generar la geometría de las grietas proceduralmente. A medida que el daño aumenta, el umbral de grietas disminuye y más bordes de las celdas Voronoi se hacen visibles, dando una apariencia de fractura natural. Las áreas chamuscadas usan un pase separado de detección de bordes que resalta los bordes de las grietas en rojo-naranja antes de que expongan completamente la textura de subsuperficie.

Lograr que esto funcionara correctamente requirió entender cómo mezclar entre capas de textura usando una máscara, cómo generar ruido Voronoi en un shader (que es más complejo que el ruido Perlin) y cómo mapear correctamente el valor de daño de la lógica del juego a los umbrales de mezcla visual. El shader final tiene unas 80 líneas y tomó casi dos semanas de iteración en tiempo libre para llegar a un resultado con el que estuviera satisfecho.

Lo Que Fue Sorprendentemente Fácil

El intercambio de paletas. Esperaba que esto fuera complicado. Reemplazar un conjunto de colores con otro para variaciones de color de unidades. No lo es. Creas una textura de paleta pequeña, muestreas el color del sprite, buscas la posición de ese color en la paleta origen y luego devuelves el color correspondiente de la paleta destino. Si tu sprite está diseñado con una paleta limitada (que el mío lo está, por diseño), todo el intercambio ocurre en unas 15 líneas de código de shader.

Los efectos de disolución también fueron más fáciles de lo esperado. Muestrear una textura de ruido, comparar el valor de ruido con un uniform de umbral de disolución y descartar el píxel si está por debajo del umbral. La disolución consume el sprite desde un patrón de ruido en lugar de un barrido simple. Añade un color de borde en el límite del umbral y obtienes un efecto de borde ardiente gratis. Uso esto para las animaciones de muerte de unidades.

  • Corrección de color y aplicación de LUT. Búsqueda directa en textura 3D
  • Efectos de contorno simples. Muestreo de píxeles vecinos como se describió arriba
  • Líneas de escaneo en espacio de pantalla. Patrón repetitivo en UV.y, nada más
  • Parpadeo de sprite (para feedback de daño). Mezcla entre color del sprite y blanco usando un uniform
  • Revelado de niebla de guerra. SDF circular sobre la capa de niebla, caída suave en el borde

Lo Que Fue Genuinamente Difícil

El mapeo normal para sprites de personajes. Godot soporta mapas normales en sprites, pero hacer que se vean correctos para personajes 2D parados en un mundo 3D, bajo una sola dirección de luz consistente, tomó mucho tiempo de ajuste. El problema es que los mapas normales asumen un modelo de iluminación específico, y el look artístico que quería no era ese modelo. Terminé escribiendo un shader de iluminación de sprites personalizado que interpreta el mapa normal de manera diferente para producir un resultado más estilizado.

El ordenamiento de transparencia. Cuando múltiples shaders transparentes se apilan uno sobre otro, lluvia sobre un resaltado de unidad transparente sobre una niebla semitransparente, el renderizador puede producir una mezcla incorrecta dependiendo del orden de dibujo. Entender el orden de dibujo, el z-index de CanvasItem para la capa de sprites y cuándo ocurre correctamente la composición de transparencia tomó un tiempo significativo de depuración.

La optimización de rendimiento. Los shaders se ejecutan en la GPU, así que los shaders individuales son rápidos. Pero 28 shaders ejecutándose simultáneamente en una escena con muchos objetos visibles empezó a mostrar impacto en el rendimiento en hardware de gama baja. Tuve que añadir niveles de calidad. Versiones simplificadas de los shaders climáticos que desactivan operaciones costosas como la refracción en configuraciones de baja calidad. El perfilado de uso de GPU en Godot es menos maduro que el perfilado de CPU, lo que hizo esto más difícil de lo que debería haber sido.

La Transferencia de Habilidades al Trabajo de UI Web

Escribir shaders cambió cómo pienso sobre los efectos visuales en el trabajo de UI web. Animaciones CSS, filtros SVG, efectos de canvas. Todos operan sobre las mismas primitivas matemáticas que el código de shaders. Lerp, smoothstep, funciones de ruido, coordenadas UV. Cuando veo un efecto de filtro CSS ahora, automáticamente pienso en cómo sería el código de shader equivalente. Cuando implemento una animación de transición en un componente Angular, pienso en qué curva de interpolación daría la sensación correcta.

También me dio una apreciación mucho más profunda de lo que realmente hace el hardware GPU. Los desarrolladores Angular tienden a pensar en las transformaciones y animaciones CSS como magia. Saber que se compilan en operaciones de GPU con el mismo tipo de multiplicaciones de matrices y muestreo de texturas que hago en shaders me convierte en un desarrollador más informado al tomar decisiones de rendimiento.

Los shaders parecen intimidantes desde afuera porque el vocabulario es desconocido. Una vez que te das cuenta de que son solo funciones que transforman entradas en colores de píxeles, y que esas entradas son solo vectores y texturas, la intimidación se desvanece y la creatividad comienza.

Referencias

GodotShadersGame DevelopmentVisual Effects