Home Especiales Tecnogeek ¿Por qué tu juego siempre quiere Compilar Shaders?

¿Por qué tu juego siempre quiere Compilar Shaders?

3
0

Cuando un juego “calcula shaders”, en realidad está ejecutando pequeños programas en la GPU que determinan cómo se ve cada píxel o vértice en pantalla. No es un cálculo único, es algo que ocurre miles de millones de veces por segundo.

Qué es un shader, en simple

Un shader es un programa que corre en la placa de video y responde preguntas como:

  • ¿De qué color es este píxel?
  • ¿Cómo le afecta la luz?
  • ¿Tiene sombras, reflejos, transparencia?
  • ¿Es rugoso, metálico, mojado?

Hay varios tipos, pero los principales son:

  • Vertex shaders: transforman la geometría (posición de vértices en el espacio).
  • Fragment/pixel shaders: deciden el color final de cada píxel.
  • (más modernos: geometry, compute, etc.)

Por qué es tan pesado

Porque la escala es brutal:

  • Una resolución 1080p = ~2 millones de píxeles por frame
  • A 60 FPS = 120 millones de píxeles por segundo
  • Cada píxel puede ejecutar decenas o cientos de operaciones matemáticas

Y eso sin contar:

  • múltiples luces
  • sombras dinámicas
  • reflejos en tiempo real
  • efectos como niebla, bloom, motion blur

Cada uno suma más cálculos por píxel.

Compìlando shaders

Qué significa “compilar shaders”

Cuando abrís un juego y ves “compilando shaders”:

  • el juego está traduciendo esos programas a instrucciones específicas para tu GPU
  • los optimiza para tu hardware (por ejemplo NVIDIA vs AMD)
  • guarda resultados para evitar microcortes (stutter) después

Esto no existía en juegos viejos porque no había esa flexibilidad.

Por qué antes no hacía falta

Los juegos antiguos (tipo Quake o Half-Life):

  • Usaban pipeline fijo (fixed-function pipeline)
  • La GPU tenía funciones predefinidas:
    • iluminación básica
    • texturas simples
    • nada programable

Resultado:

  • Mucho más limitado visualmente
  • Mucho más predecible (no había que compilar nada)

El cambio clave

Con la llegada de GPUs programables (early 2000s):

  • APIs como DirectX y OpenGL permitieron escribir shaders personalizados
  • Los desarrolladores pasaron de “usar lo que hay” a “programar cómo se ve todo”

Esto permitió:

  • iluminación realista (PBR)
  • sombras suaves
  • reflejos dinámicos
  • materiales complejos

Pero el costo es:

  • más complejidad
  • más uso de GPU
  • necesidad de compilación previa

Ejemplo de un Shader

Este sería un fragment shader (el que decide el color de cada píxel):

#version 330 core

in vec3 Normal;
in vec3 FragPos;

out vec4 FragColor;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
  // Luz ambiente (base mínima de iluminación)
  float ambientStrength = 0.2;
  vec3 ambient = ambientStrength * lightColor;

  // Luz difusa (cómo le pega la luz según el ángulo)
  vec3 norm = normalize(Normal);
  vec3 lightDir = normalize(lightPos - FragPos);
  float diff = max(dot(norm, lightDir), 0.0);
  vec3 diffuse = diff * lightColor;

  // Luz especular (brillo tipo reflejo)
  float specularStrength = 0.5;
  vec3 viewDir = normalize(viewPos - FragPos);
  vec3 reflectDir = reflect(-lightDir, norm);
  float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
  vec3 specular = specularStrength * spec * lightColor;

  // Resultado final
  vec3 result = (ambient + diffuse + specular) * objectColor;
  FragColor = vec4(result, 1.0);
}

Qué está pasando ahí (traducido a humano)

Ese código se ejecuta para cada píxel de cada objeto visible. Ahora lo desarmamos:

1. Luz ambiente

ambient = 0.2 * lightColor;

Es una iluminación mínima constante. Evita que todo sea negro si no hay luz directa.

Es “trampa visual”, pero necesaria.

2. Luz difusa (la importante)

diff = dot(norm, lightDir);

Acá usa el producto punto (dot product):

  • si la superficie mira hacia la luz → valor alto
  • si está de costado → valor medio
  • si está en sombra → 0

Esto simula cómo la luz “pega” en una superficie.

3. Luz especular (el brillo)

spec = pow(dot(viewDir, reflectDir), 32);

Esto calcula el típico brillo de “plastificado” o “metal”.

  • depende de dónde está la cámara
  • usa una potencia (32) para hacer el brillo más concentrado

Esto es lo que hace que algo parezca mojado, metálico o brillante.

4. Resultado final

result = (ambient + diffuse + specular) * objectColor;

Suma todo y lo multiplica por el color del objeto.

Compilando más shaders

Por qué esto es caro

Ahora imaginá esto:

  • Este shader puede ejecutar ~50-100 operaciones matemáticas
  • Se corre por cada píxel
  • A 1920×1080 y 60 FPS → ~120 millones de ejecuciones por segundo

Y esto es un shader SIMPLE. Juegos modernos hacen:

  • múltiples luces
  • sombras dinámicas
  • mapas de normales
  • reflejos en tiempo real
  • PBR (physically based rendering)

Un shader es básicamente:

“Para ESTE píxel específico, con ESTA luz y ESTA cámara, ¿qué color exacto debería tener?”

Y el juego responde eso millones de veces por segundo.

¿Por qué no te descarga shaders pre calculados?

En teoría suena tentador, pero en la práctica no es viable. Y no es solo por cantidad de GPUs, hay varias razones más profundas.

1. No existe “una versión por GPU”

No alcanza con “una por modelo” como NVIDIA o AMD. El shader final depende de:

  • modelo exacto de GPU
  • arquitectura (ej: RDNA, Ada Lovelace, etc.)
  • driver instalado (¡clave!)
  • sistema operativo
  • versión de API (DirectX, Vulkan, OpenGL)
  • configuraciones gráficas del juego (calidad, efectos activados, etc.)

El número de combinaciones explota de forma absurda.

2. Los drivers cambian el resultado

Los shaders no se compilan directamente a “código máquina universal”, sino a algo intermedio que el driver de la GPU termina de optimizar.

Esto significa:

  • el mismo shader puede generar código distinto con otro driver
  • incluso el rendimiento puede cambiar bastante

Si precompilás todo antes, se rompe cuando el usuario actualiza el driver.

3. El shader no es uno solo

Un juego moderno no tiene “un shader”, tiene miles de variantes:

  • con/sin sombras
  • con/sin niebla
  • distintos tipos de materiales
  • diferentes luces activas
  • efectos combinados

Esto se llama shader permutations.

Un solo material puede generar decenas o cientos de variantes.

4. El tamaño sería inmanejable

Si intentaras precompilar TODO:

  • terminarías con gigabytes de shaders
  • la mayoría nunca se usarían en la PC del jugador

5. Lo que sí se hace (soluciones reales)

 1. Compilación inicial (lo que ves en pantalla)

El juego compila shaders al inicio para TU hardware específico.

2. Caché de shaders

  • se guardan en disco
  • la próxima vez no recompila todo

3. Precompilación parcial

Algunos engines (como Unreal Engine o Unity):

  • compilan muchas variantes por adelantado
  • pero dejan la optimización final al driver

4. Pipeline State Objects (PSO)

En APIs modernas como Vulkan o DirectX 12:

  • se pueden predefinir combinaciones de estado + shaders
  • ayuda a reducir stutter

5. Distribución de cachés (caso Steam)

Plataformas como Valve Corporation con Steam:

  • distribuyen shaders precompilados según tu GPU
  • pero igual no cubren todo, solo casos comunes

No es que “no se les ocurrió” precompilar todo.

Es que el shader final depende tanto del entorno específico del usuario que solo se puede cerrar completamente en su propia máquina.

Como dato extra, hace poco NVidia e Intel se habían coordinado para empezar a implementar nuevas formas que recortaran este proceso con el “Advanced Shader Delivery”, anunciado en el último GDC, que es justamente el envío de shaders pre compilados a los jugadores.

LEAVE A REPLY

Please enter your comment!
Please enter your name here