DEV Community

Cover image for Colored Shadow Penumbra: sombras con color en Unreal Engine 5.7
lu1tr0n
lu1tr0n

Posted on • Originally published at elsolitario.org

Colored Shadow Penumbra: sombras con color en Unreal Engine 5.7

El colored shadow penumbra es uno de esos efectos que pasan inadvertidos hasta que alguien los apaga: ver una escena sin él hace que las sombras parezcan, de repente, demasiado limpias, demasiado neutras, demasiado 3D. La técnica reproduce un fenómeno óptico real (la transición coloreada entre la zona iluminada y la sombra dura) y, hasta hace poco, implementarla en Unreal Engine 5 implicaba pelearse con post-procesos frágiles que rompían con Lumen o con ciclos día/noche. La nueva implementación que el artista técnico Romain Durand puso de moda en redes y que Chosker liberó este mes resuelve el problema editando directamente los shaders del motor, con un costo de rendimiento prácticamente cero.

En este artículo desarmamos cómo funciona, copiamos línea por línea el código HLSL que se inyecta en los shaders de iluminación diferida, explicamos por qué necesita penumbras anchas para verse y discutimos cuándo conviene activarlo en producción. Si trabajás con UE5 desde LATAM, ya sea en un estudio indie o haciendo arch-viz, el colored shadow penumbra es una de esas mejoras de pulido que separan un render técnicamente correcto de uno que se siente vivo.

Qué es exactamente colored shadow penumbra

La luz, cuando atraviesa el borde de un objeto, no proyecta una sombra binaria entre iluminado y oscuro. Hay una región de transición llamada penumbra, donde la fuente de luz queda parcialmente ocluida y solo aporta una fracción de su intensidad. En esa franja, los rebotes indirectos (la luz que viene de paredes, suelos y el ambiente general) tienen relativamente más peso que la directa, y como la luz indirecta arrastra el color de las superficies que rebota, la penumbra termina teñida de los colores de la escena.

El fenómeno se conoce también como shadow terminator coloring en literatura de raytracing offline. Renderizadores no-real-time como Arnold, V-Ray o Cycles lo computan de forma natural porque trazan rayos secundarios. En tiempo real, Unreal Engine 5 simplifica el cálculo: usa un único término de sombra (SurfaceShadow) que va de 0 (en sombra completa) a 1 (totalmente iluminado) y modula la luminancia diffuse por ese factor. Esa simplificación es rápida pero borra la información cromática de la transición.

La técnica de Chosker reintroduce el efecto sin pagar el costo de un trazado real. La idea es ingeniosa: en la región donde SurfaceShadow es intermedio (la penumbra), saturar artificialmente el color base de la superficie. Con un colored shadow penumbra bien calibrado, una pared roja en sombra muestra rojos más vivos en su transición, una camisa azul deja un halo azulado al borde de su sombra y los materiales metálicos ganan profundidad sin lookdev adicional.
El efecto se nota más en bordes suaves de luces grandes y materiales saturados.

Por qué editar el shader del motor (y no un post-process)

Existen al menos tres caminos para introducir colored shadows en una escena de UE5: un Material Function aplicado por objeto, un Post Process Material global, o editar los shaders del engine directamente. Chosker descartó las primeras dos por una razón concreta: con Lumen activado o con ciclos día/noche, los valores que llegan al post-process ya están mezclados (luz directa, GI, especulares) y reconstruir cuál fragmento corresponde a sombra y cuál a iluminación se vuelve un ejercicio de adivinanza. Editar el shader de iluminación diferida resuelve el problema en su origen: ahí todavía existe la separación clara entre DiffuseLuminance y SurfaceShadow.

La ventaja adicional es que funciona con todos los tipos de luz (direccionales, puntuales, focales, rectangulares) sin código extra y se ejecuta una sola vez por píxel, en el mismo pase donde ya se está mezclando la sombra. El costo en GPU es marginal: un dot product y dos lerp, instrucciones que cualquier hardware moderno absorbe sin pestañear.

📌 Nota: Editar los shaders del motor no requiere clonar el repositorio completo de Epic ni compilar Unreal desde código fuente. Basta con la versión Launcher: los shaders del engine son archivos .usf/.ush en la carpeta Engine\Shaders\Private y se recompilan en frío con Ctrl + Shift + . dentro del editor.

Implementación con Substrate (UE 5.7)

Substrate es el nuevo sistema de materiales de Unreal Engine introducido como experimental en 5.2 y promovido a beta en 5.7. Si tu proyecto lo tiene activado (lo que es cada vez más común en producciones nuevas), el shader que necesitás editar es Engine\Shaders\Private\Substrate\SubstrateDeferredLighting.ush. Alrededor de la línea 190 vas a encontrar la línea que calcula la luminancia especular:

float3 SpecularLuminance = BSDFEvaluate.IntegratedSpecularValue * LightData.SpecularScale;
Enter fullscreen mode Exit fullscreen mode

Inmediatamente después, agregás el bloque de colored shadow penumbra:

// Colored shadow penumbra - Start
const float PenumbraSaturation = 4.0f;

float3 LuminanceFactors = float3(0.3f, 0.59f, 0.11f);
float3 PenumbraColor = dot(DiffuseLuminance, LuminanceFactors);
PenumbraColor = lerp(PenumbraColor, DiffuseLuminance, PenumbraSaturation);
DiffuseLuminance = lerp(DiffuseLuminance, PenumbraColor, 1.0f - BSDFShadowTerms.SurfaceShadow);
// Colored shadow penumbra - End
Enter fullscreen mode Exit fullscreen mode

El valor PenumbraSaturation = 4.0f es un punto de partida agresivo, pensado para que el efecto se vea en capturas comparativas. En producción, valores entre 1.5 y 2.5 dan un resultado más natural. Con 1.0 el efecto desaparece completamente (el lerp de saturación se cancela).

Implementación clásica sin Substrate

Si tu proyecto sigue usando el pipeline tradicional de iluminación diferida, el archivo objetivo es Engine\Shaders\Private\DeferredLightPixelShaders.usf. Alrededor de la línea 397 buscás:

OutColor += Radiance;
Enter fullscreen mode Exit fullscreen mode

Y agregás justo después:

// Colored shadow penumbra - Start
const float PenumbraSaturation = 4.0f;

float3 LuminanceFactors = float3(0.3f, 0.59f, 0.11f);
float3 PenumbraColor = dot(OutColor.xyz, LuminanceFactors);
PenumbraColor = lerp(PenumbraColor, OutColor.xyz, PenumbraSaturation);
OutColor.xyz = lerp(OutColor.xyz, PenumbraColor, 1.0f - SurfaceShadow);
// Colored shadow penumbra - End
Enter fullscreen mode Exit fullscreen mode

La lógica es idéntica; solo cambian los nombres de las variables porque el flujo de Substrate usa una estructura distinta para encapsular el resultado de la BSDF.

Cómo funciona el código línea por línea

Vale la pena desarmar las cinco líneas porque condensan tres conceptos importantes de teoría de color y composición:

  • Vector de luminancia (0.3, 0.59, 0.11) — son los pesos perceptuales del estándar Rec. 601 para convertir RGB a una intensidad gris equivalente. El verde pesa más porque el ojo humano es más sensible a esa banda. Hacer un dot entre el color y este vector colapsa cualquier color a su luminancia.- Saturación inversa — el primer lerp mueve el color desde su versión gris (PenumbraColor) hacia el color original (DiffuseLuminance) usando un factor mayor a 1. Cuando el factor pasa de 1, el lerp extrapola y produce una versión más saturada que la original. Es el mismo truco que usan los nodos de saturación de Photoshop o After Effects.- Mezcla por shadow term — el segundo lerp combina el color saturado con el color original usando 1.0 - SurfaceShadow como peso. En zonas totalmente iluminadas (SurfaceShadow = 1) el peso es 0 y no pasa nada. En zonas totalmente en sombra (SurfaceShadow = 0) el peso es 1 pero el color a mezclar ya es muy oscuro, así que tampoco se nota. Solo en la franja de transición se ve la versión saturada.

Es elegante porque no necesita detectar dónde está la penumbra: la propia función 1.0 - SurfaceShadow tiene su pico justo en esa región (cerca de 0.5) y cae hacia los extremos. La saturación se modula sola.

Visualización del flujo

graph LR
  A["Luz directa"] --> B["BSDF + Shadow Term"]
  B --> C["DiffuseLuminance RGB"]
  C --> D["Luminancia gris (Rec. 601)"]
  D --> E["Saturación inversa (lerp > 1)"]
  E --> F["Mezcla por (1 - SurfaceShadow)"]
  F --> G["Color final con penumbra coloreada"]
Enter fullscreen mode Exit fullscreen mode

Ajustar PenumbraSaturation: el parámetro que importa

El único hiperparámetro de la técnica es PenumbraSaturation. Vale la pena dedicar tiempo a calibrarlo, porque el rango útil es estrecho:

  • 1.0 — desactiva el efecto. Útil como sentinela durante debugging.- 1.2 a 1.8 — efecto sutil, perceptible solo en escenas con materiales saturados o luces grandes. Buen rango para arch-viz fotorrealista.- 2.0 a 3.0 — sweet spot para juegos estilizados, escenas con paleta artística o cinemáticas. El efecto se ve sin gritar.- 4.0 o más — exagerado a propósito, ideal para capturas de marketing pero rompe el realismo.

💡 Tip: Como el valor está hardcodeado en el shader, cada cambio implica recompilar shaders (varios minutos la primera vez, segundos después). Si vas a iterar mucho, conviene exponerlo como una console variable usando FAutoConsoleVariableRef en C++ o, más rápido todavía, parametrizarlo desde un material function global.

Limitaciones y casos donde no se ve

El colored shadow penumbra tiene tres limitaciones honestas que conviene comunicar al equipo de arte antes de venderlo como solución mágica:

  • Solo luces dinámicas — la sombra baked se calcula offline antes de que el shader corra, así que no hay penumbra que colorear. Si tu proyecto depende de iluminación pre-baked (común en mobile o VR de bajos requerimientos), esta técnica no aporta nada.- Necesita penumbras anchas — luces puntuales pequeñas con shadow maps duros producen penumbras de pocos píxeles, donde el efecto pasa desapercibido. Funciona mejor con luces rectangulares grandes, sky lights con soft shadows o virtual shadow maps con kernel amplio.- No se ve en grises ni saturados — un material totalmente gris no tiene color que saturar, y un material ya saturado al máximo tampoco gana. El efecto luce mejor en colores medios y materiales con reflectividad media.

Por qué importa para estudios indie en LATAM

El argumento más fuerte para adoptar la técnica no es estético sino económico. Un estudio indie en LATAM compite contra producciones AAA con presupuestos de iluminación cien veces mayores: lookdev artists dedicados, lighters senior, herramientas propietarias para comp y grading. El colored shadow penumbra es una de esas técnicas asimétricas que cierran parte de esa brecha sin contratar a nadie nuevo: cinco líneas de HLSL aplicadas una vez al engine fork del proyecto y todas las escenas heredan el mejor pulido visual.

Es también una excusa perfecta para subir el nivel del equipo en programación de shaders. Modificar SubstrateDeferredLighting.ush obliga a entender qué hace cada paso del pipeline diferido, qué información sobrevive entre el G-Buffer y el shading pass, y cómo Unreal estructura su evaluación de BSDFs. Es el tipo de conocimiento que en estudios grandes se aprende osmóticamente con el código fuente del engine, y que en LATAM se logra raras veces porque el acceso al source code de Epic sigue siendo un trámite que pocos completan.
El efecto suma profundidad cromática sin afectar la performance en runtime.

Qué sigue para colored shadows en tiempo real

El siguiente paso natural es exponer PenumbraSaturation a nivel de cada luz, no global del proyecto. La forma elegante sería agregar un campo FloatProperty a FLightData, llenarlo desde C++ y leerlo en el shader. Eso permitiría que un foco rojizo de una habitación cálida tenga saturación distinta a una luz fría de exterior, lo cual es más fiel a cómo se trabaja la luz en cinematografía.

Otra dirección interesante es separar el factor para diffuse y para specular. La implementación actual modifica solo el componente diffuse (que es donde el efecto tiene sentido físico) pero hay artistas que quisieran un control independiente para empujar el look hacia algo más estilizado. Y a más largo plazo, integrar la técnica con Lumen para que la saturación respete la información de GI ya calculada parece la evolución natural: ahí mismo está el color real que aporta la radiosidad, sin necesidad de aproximarlo con un truco.

📖 Resumen en Telegram: Ver resumen

Preguntas frecuentes

¿Funciona en proyectos comerciales sin pagar licencia adicional a Epic?

Sí. Modificar los shaders del motor en la versión Launcher cae dentro del EULA estándar de Unreal Engine. No es lo mismo que distribuir un fork modificado del engine: solo estás cambiando archivos de shader que ya viven en tu proyecto. La licencia royalty del 5% sobre el millón de dólares se aplica por el uso del engine, no por las modificaciones.

¿Funciona con Lumen activado?

Sí, y es justamente uno de los argumentos a favor de editar el shader directamente en lugar de un post-process. Lumen aporta la GI; el colored shadow penumbra opera sobre la luz directa antes de que se sume al resultado final. No interfieren entre sí.

¿Cuál es el costo en GPU?

Despreciable en cualquier hardware compatible con UE5. Son 5 líneas de aritmética sin lecturas adicionales de texturas ni branching. En profilers como RenderDoc o el GPU Visualizer interno, el incremento del pase de iluminación es del orden del 0.1% o menos. La penalidad real viene una sola vez: la recompilación de shaders cuando el archivo cambia.

¿Por qué mi escena se ve igual después de aplicar el código?¿Cómo lo desactivo si causa problemas en producción?

Comentás las cinco líneas con /* */ y guardás el archivo. Unreal recompila los shaders en el siguiente arranque y la escena vuelve al comportamiento original. Por eso conviene siempre dejar los marcadores // Colored shadow penumbra - Start y // Colored shadow penumbra - End en el código: facilitan localizar el bloque para revertirlo en cualquier momento.

¿Puedo combinarlo con otros mods de shaders del engine?

Sí, mientras los bloques no toquen las mismas variables. La regla práctica es mantener cada mod en su propio archivo o en una sección bien delimitada del archivo del engine, documentar el cambio en un README del proyecto y versionar las modificaciones del engine en el mismo repo del juego para que otros desarrolladores no las pierdan al actualizar Unreal.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Top comments (0)