Parte: 21 — Arquitectura avanzada de motores y rendering · Fuente: Akenine-Möller et al., «Real-Time Rendering» (global illumination) · Pharr, Jakob & Humphreys, «Physically Based Rendering» ⏱️ Duración estimada: 125 min · Nivel: Avanzado
Entender cómo se calcula la luz indirecta: la que rebota. Una habitación iluminada por una ventana no está iluminada solo por el rectángulo de sol en el suelo — está iluminada por toda la luz que ese rectángulo rebota hacia el techo, las paredes y de vuelta. Sin luz indirecta, las sombras son negras y la escena parece un decorado con focos.
Vas a recorrer el abanico completo de técnicas, desde lo horneado hasta el path tracing, entendiendo el compromiso de cada una: qué calidad da, qué cuesta, qué restricciones impone (¿se puede mover la luz? ¿se puede mover la geometría?) y qué hardware necesita. Y verás la clave de por qué el ray tracing en tiempo real es viable hoy: no se trazan suficientes rayos, se trazan poquísimos y se acumulan y filtran a lo largo del tiempo — exactamente lo de la clase 345.
Al finalizar, el alumno podrá:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Luz directa e indirecta | La segunda es la que hace que una escena parezca real. |
| 2 | La ecuación del rendering | El problema que todas estas técnicas aproximan. |
| 3 | Lightmaps | Máxima calidad, cero dinamismo. |
| 4 | Probes de irradiancia | El compromiso más usado. |
| 5 | GI en espacio de pantalla | Barata y con una limitación evidente. |
| 6 | Voxel GI | Dinámica, con coste de memoria y fugas. |
| 7 | Ray tracing por hardware | Qué acelera exactamente. |
| 8 | Denoising | Sin él, el ray tracing en tiempo real es ruido. |
| 9 | Rendering híbrido | Lo que hacen los juegos de verdad. |
| 10 | Elegir con criterio | Depende del proyecto, no de la moda. |
Godot 4.x con sus tres sistemas de GI: LightmapGI (horneado), VoxelGI (dinámica, interiores) y SDFGI (dinámica, exteriores grandes), más ReflectionProbe y SSAO/SSIL. Trabajaremos en res://gi/ con una escena de comparación. Necesitas GPU para la parte visual; los cálculos de armónicos esféricos y el análisis de coste se pueden verificar sin ella. Documentación: iluminación global en Godot.
Habitación con una ventana, suelo blanco, techo blanco:
SOLO DIRECTA:
- Rectángulo de sol en el suelo, brillante.
- Todo lo demás: NEGRO. El techo, negro. Las paredes en sombra, negras.
- Los objetos fuera del rectángulo, invisibles.
→ Parece un decorado con un foco.
CON INDIRECTA (1 rebote):
- El rectángulo de sol ilumina el techo desde abajo.
- Las paredes reciben luz rebotada del suelo.
- Los objetos en sombra son visibles, con luz suave.
→ Parece una habitación.
CON INDIRECTA (varios rebotes):
- La luz se reparte por toda la habitación.
- Aparece el color bleeding: un suelo rojo tiñe de rojo el techo.
→ Parece una fotografía.
Y el dato que conviene tener presente: en un interior, la luz indirecta suele aportar más del 60 % de la iluminación total. No es un detalle de acabado.
| Técnica | Calidad | Coste runtime | Luces dinámicas | Geometría dinámica | Memoria | Hardware |
|---|---|---|---|---|---|---|
| Solo directa | ✗ | Mínimo | ✅ | ✅ | — | Cualquiera |
| Ambiente constante | Muy baja | Cero | ✅ | ✅ | — | Cualquiera |
| Lightmaps | Máxima | Casi cero | ❌ | ❌ (solo recibe) | Texturas | Cualquiera |
| Probes de irradiancia | Buena | Muy bajo | ❌ (horneados) | ✅ | Baja | Cualquiera |
| SSAO / SSIL | Baja (contacto) | Bajo | ✅ | ✅ | Baja | GPU media |
| SSGI | Media | Medio | ✅ | ✅ | Media | GPU media |
| Voxel GI | Buena | Medio-alto | ✅ | ⚠️ parcial | Alta | GPU media-alta |
| SDFGI | Buena | Medio-alto | ✅ | ⚠️ parcial | Media | GPU media-alta |
| RT (1 rebote) | Muy buena | Alto | ✅ | ✅ | Media | GPU con RT |
| Path tracing | Referencia | Muy alto | ✅ | ✅ | Media | GPU con RT potente |
Las tres decisiones que fija esta tabla: ¿se mueven las luces?, ¿se mueve la geometría? y ¿qué hardware mínimo soportas?. Con esas tres respuestas, la elección casi se hace sola.
Proceso de horneado:
1. Cada superficie estática recibe coordenadas UV únicas (UV2, sin solaparse).
2. Se trazan miles de rayos desde cada texel hacia el hemisferio.
3. Se acumula la luz que llega, con sus rebotes.
4. Se guarda en una textura por objeto o en un atlas.
Runtime: una lectura de textura. Es literalmente gratis.
Restricciones:
- Ni la geometría ni las luces se pueden mover: si se mueven, el lightmap
deja de corresponder con la escena y se ve mal (una sombra donde no hay nada).
- Los objetos dinámicos NO reciben lightmap: necesitan probes.
- Horneado de minutos u horas → la iteración de iluminación es lenta.
- Ocupa memoria de textura, y en mundos grandes mucha.
# Verificable sin GPU: un lightmap sin UV2 correcto no sirve de nada, y es el
# error más común. Este script lo detecta antes de hornear tres horas.
func validar_uv2(malla: ArrayMesh) -> Array[String]:
var errores: Array[String] = []
for s in malla.get_surface_count():
var arrays := malla.surface_get_arrays(s)
var uv2 = arrays[Mesh.ARRAY_TEX_UV2]
if uv2 == null:
errores.append("superficie %d: sin UV2 (no se puede hornear)" % s)
continue
for uv in uv2:
if uv.x < 0.0 or uv.x > 1.0 or uv.y < 0.0 or uv.y > 1.0:
errores.append("superficie %d: UV2 fuera de [0,1]" % s)
break
return errores
class_name ArmonicosEsfericos
extends RefCounted
# 9 coeficientes por canal (orden 2) bastan para representar la irradiancia
# DIFUSA con muy poco error: la función es suave por naturaleza. Para reflejos
# especulares no basta, y por eso hacen falta reflection probes aparte.
var coef := [] # 9 × Color
func _init() -> void:
coef.resize(9)
coef.fill(Color.BLACK)
static func base(d: Vector3) -> PackedFloat32Array:
# Base de armónicos esféricos hasta orden 2.
return PackedFloat32Array([
0.282095, # Y00
0.488603 * d.y, 0.488603 * d.z, 0.488603 * d.x, # Y1-1, Y10, Y11
1.092548 * d.x * d.y, # Y2-2
1.092548 * d.y * d.z, # Y2-1
0.315392 * (3.0 * d.z * d.z - 1.0), # Y20
1.092548 * d.x * d.z, # Y21
0.546274 * (d.x * d.x - d.y * d.y), # Y22
])
func acumular(direccion: Vector3, radiancia: Color, peso: float) -> void:
var b := base(direccion)
for i in 9:
coef[i] += radiancia * b[i] * peso
func evaluar(normal: Vector3) -> Color:
var b := base(normal)
# Coeficientes de convolución con el coseno: convierten radiancia en
# irradiancia difusa. Son constantes conocidas, no hay que calcularlas.
const A := [3.141593, 2.094395, 2.094395, 2.094395,
0.785398, 0.785398, 0.785398, 0.785398, 0.785398]
var r := Color.BLACK
for i in 9:
r += coef[i] * b[i] * A[i]
return Color(maxf(r.r, 0.0), maxf(r.g, 0.0), maxf(r.b, 0.0))
La fuga de luz es su artefacto característico: si un probe está dentro de una pared y un objeto cercano interpola desde él, recibe la luz del otro lado. Se mitiga colocando los probes con criterio, con oclusión por probe, o usando probes con información de visibilidad.
// SSAO/SSGI: se muestrea el búfer de profundidad alrededor del píxel para
// estimar cuánta luz le llega. Muy barato, y con un límite claro.
float oclusion_ambiental(vec2 uv, vec3 normal, float radio) {
float oclusion = 0.0;
float prof = texture(profundidad, uv).r;
for (int i = 0; i < N_MUESTRAS; i++) {
vec2 offset = _muestra_hemisferio(i, normal) * radio;
float prof_muestra = texture(profundidad, uv + offset).r;
// Si lo que hay en esa dirección está MÁS CERCA de la cámara, tapa.
if (prof_muestra < prof - 0.01) {
oclusion += 1.0;
}
}
return 1.0 - oclusion / float(N_MUESTRAS);
}
LA limitación, y no tiene solución dentro de la técnica:
Solo se ve lo que está EN PANTALLA. Por tanto:
- Un objeto fuera de cámara no proyecta oclusión ni aporta rebote.
- Un objeto tapado por otro no contribuye.
- Al girar la cámara, la iluminación indirecta CAMBIA. Es incorrecto y
se nota, sobre todo en los bordes de la pantalla.
Por eso el espacio de pantalla se usa como COMPLEMENTO (detalle de contacto)
y no como fuente principal de GI.
Lo que hace una unidad de RT por hardware:
1. Recorrer el BVH (la estructura de la clase 342) muy rápido.
2. Calcular intersecciones rayo-triángulo en paralelo.
Lo que NO hace:
- Sombrear el punto de impacto (eso lo hacen los shaders normales).
- Construir el BVH (lo construye el driver, y cuesta).
- Eliminar el ruido (eso es el denoiser).
Estructura de aceleración en dos niveles:
TLAS (top level) → instancias del mundo, con su transformada
└─ BLAS (bottom) → geometría de cada malla, en su espacio local
Mover un objeto = actualizar su entrada en el TLAS (barato).
Deformar una malla (animación) = reconstruir su BLAS (caro).
← Por eso los personajes animados son lo más costoso del RT.
Rayos por píxel necesarios para una imagen limpia por fuerza bruta: ~1.000
Rayos por píxel viables en tiempo real: 0,5 - 2
Con 1 rayo/píxel, la imagen es PURO RUIDO. Inutilizable.
El pipeline real:
1. Trazar 1 rayo por píxel → imagen extremadamente ruidosa
2. Filtrado espacial guiado por normal, → reduce el ruido usando vecinos
profundidad y material de la MISMA superficie
3. Acumulación temporal con motion → suma muestras de frames pasados
vectors (clase 345) → equivale a decenas de rayos
4. Filtrado final y nitidez → resultado utilizable
Y de ahí sale la conclusión: el ray tracing en tiempo real HEREDA todos los
artefactos del rendering temporal. El ghosting en reflejos y las estelas en
las sombras RT son exactamente el problema de la clase 345.
Ningún juego traza todo con rayos. Se combinan técnicas por efecto:
Geometría y luz directa → rasterización (rápida y madura)
Sombras → shadow maps, o RT si el presupuesto llega
Reflejos → SSR primero; RT donde SSR falla (fuera de pantalla)
Oclusión ambiental → SSAO, o RTAO
GI difusa → probes/SDFGI, o RT con 1 rebote
Refracción y cáusticas → aproximaciones; RT completo casi nunca
Y con niveles de calidad por hardware:
Bajo: directa + probes horneados + SSAO
Medio: + SSGI + SSR
Alto: + SDFGI o Voxel GI
Ultra: + reflejos RT + sombras RT
extends SceneTree # gi/comparar.gd
const CONFIGS := [
{"nombre": "solo_directa", "gi": "ninguna", "ssao": false},
{"nombre": "ambiente", "gi": "ambiente", "ssao": false},
{"nombre": "ssao", "gi": "ambiente", "ssao": true},
{"nombre": "lightmap", "gi": "lightmap", "ssao": true},
{"nombre": "voxelgi", "gi": "voxel", "ssao": true},
{"nombre": "sdfgi", "gi": "sdfgi", "ssao": true},
]
func _init() -> void:
print("== Coste y calidad de las técnicas de GI ==")
print(" técnica ms/frame VRAM MB dinámica PSNR vs ref")
var referencia := _renderizar_referencia() # path traced offline
for c in CONFIGS:
var r := _medir(c)
print(" %-14s %8.2f %9.1f %-9s %8.2f"
% [c["nombre"], r["ms"], r["vram_mb"],
"sí" if r["dinamica"] else "no", _psnr(referencia, r["imagen"])])
quit()
¿Las luces y la geometría son ESTÁTICAS?
├─ SÍ → Lightmaps + probes para lo dinámico.
│ Es la mejor calidad por el menor coste runtime. Sigue siendo la
│ elección correcta para muchísimos juegos, y no es "anticuada".
└─ NO → ¿Qué se mueve?
├─ Solo objetos (luces fijas) → Lightmaps + probes de irradiancia
├─ Ciclo día/noche → SDFGI o Voxel GI
├─ Todo (destrucción, construcción) → SDFGI, o RT si el público lo permite
└─ ¿Y el hardware mínimo?
├─ Móvil / gama baja → Probes horneados + SSAO. Nada más.
├─ Consola actual → SDFGI o Voxel GI, RT opcional
└─ PC de gama alta → RT como opción de calidad, nunca como requisito
Construye una escena de comparación de técnicas de iluminación global con al menos cinco configuraciones, medición de coste (ms/frame y VRAM), comparación objetiva de calidad frente a una referencia, validación de UV2 para lightmaps, implementación verificable de armónicos esféricos y documentación del árbol de decisión aplicado a tu proyecto.
Criterio de aceptación: (a) una prueba headless verifica que la base de armónicos esféricos es correcta: una radiancia uniforme en todas las direcciones produce irradiancia uniforme al evaluarla en cualquier normal, con error menor del 1 %; (b) el validador de UV2 detecta mallas sin UV2 y con UV2 fuera de [0,1]; (c) el banco de comparación mide ms/frame y VRAM de las cinco configuraciones e imprime la tabla; (d) la calidad se compara objetivamente contra una imagen de referencia con PSNR; (e) se documenta, para cada técnica, si soporta luces dinámicas y geometría dinámica, comprobado moviendo ambas en la escena; (f) el proyecto define al menos tres niveles de calidad gráfica con su combinación de técnicas y su hardware objetivo; (g) el README documenta el árbol de decisión aplicado y por qué se eligió la técnica principal.
| Síntoma / mensaje | Causa y cómo arreglar |
|---|---|
| Las sombras son completamente negras | No hay iluminación indirecta. Añade probes, GI o al menos ambiente. |
| Un objeto dinámico se ve plano en una escena horneada | Los lightmaps no le afectan. Necesita probes de irradiancia. |
| Luz que atraviesa una pared | Fuga de luz de probes o voxels. Recoloca, ajusta resolución o usa oclusión. |
| La iluminación cambia al girar la cámara | Es SSGI: solo ve lo que está en pantalla. Complementa con GI no dependiente de vista. |
| El horneado tarda horas y hay que iterar | Hornea a baja calidad para iterar y a alta solo para la build. |
| El lightmap se ve con costuras o manchas | UV2 solapadas o mal generadas. Valídalas antes de hornear. |
| Los reflejos RT tienen ghosting | Es el denoising temporal. Los mismos artefactos de la clase 345. |
| RT activo mata el rendimiento con personajes | Reconstruir el BLAS de mallas animadas es caro. Limita cuáles participan. |
| VoxelGI consume toda la VRAM | Resolución de voxels demasiado alta. Bájala o usa SDFGI. |
❓ ¿Los lightmaps están anticuados? En absoluto. Siguen dando la mejor calidad por el menor coste en runtime, y para un juego con iluminación estática son la elección correcta. Lo que ha cambiado es que ahora hay alternativas dinámicas viables — no que las horneadas hayan dejado de ser buenas. Muchos juegos con excelente iluminación de los últimos años usan lightmaps.
❓ ¿Merece la pena el ray tracing? Como opción de calidad en PC de gama alta, sí: mejora reflejos, sombras y GI de forma notable. Como requisito, casi nunca: dejas fuera a la mayoría de tu público. La regla práctica es que el juego debe verse bien sin RT y mejor con él.
❓ ¿Por qué el RT es tan caro si el hardware lo acelera? Porque el hardware acelera el trazado, no el sombreado ni el denoising. Con 1 rayo por píxel a 4K son 8 millones de rayos por frame, cada uno con su sombreado, más el filtrado espacial y la acumulación temporal. La aceleración es enorme comparada con hacerlo en software, y aun así el presupuesto sigue siendo justo.
❓ ¿Qué elijo para un proyecto pequeño? Lightmaps para lo estático, probes para lo dinámico, SSAO para el detalle de contacto. Es la combinación con mejor relación calidad/esfuerzo, funciona en cualquier hardware y su coste en runtime es mínimo. Solo cambia si necesitas luces dinámicas de verdad.
❓ ¿Cómo pruebo esto en CI sin GPU? No puedes probar la imagen, y conviene decirlo claro. Lo que sí se verifica sin GPU es lo que se comprueba en el reto: la corrección de los armónicos esféricos, la validez de las UV2, la configuración de los niveles de calidad y que las escenas cargan e importan limpias. La comprobación visual se documenta y se hace en una máquina con GPU, antes de cada release.
LightmapGI y horneado: https://docs.godotengine.org/en/4.3/tutorials/3d/global_illumination/using_lightmap_gi.html · uso: lectura de respaldo del objetivo; no se usa en el procedimientoClase 347 - HDR y gestión moderna de color