39 KiB
Unreal Engine 5 Lighting System Documentation
Complete Guide for Game Engine Developers
This documentation explains how UE5's lighting system works - from light types to deferred rendering and light culling.
Table of Contents
- Lighting System Overview
- Light Types
- Light Parameters
- Light Mobility
- Deferred Lighting Pipeline
- Light Culling (Clustered Deferred)
- Forward Lighting
- bgfx Implementation Guide
- Key Files Reference
Lighting System Overview
UE5 uses a hybrid lighting system that combines:
Lighting Architecture:
┌────────────────────────────────────────────┐
│ 1. Direct Lighting │
│ - Directional (Sun) │
│ - Point (Light bulbs, street lights) │
│ - Spot (Flashlights, car headlights) │
│ - Rect (Area lights, windows) │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 2. Indirect Lighting │
│ - Sky Light (Environment) │
│ - Reflection Probes │
│ - Lumen GI (Dynamic) │
│ - Lightmaps (Baked) │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 3. Rendering Path │
│ - Deferred (Most lights) │
│ - Forward (Translucent, mobile) │
│ - Clustered Deferred (Many lights) │
└────────────────────────────────────────────┘
Key File: Engine/Source/Runtime/Engine/Public/SceneTypes.h
enum ELightComponentType
{
LightType_Directional, // Parallel rays (sun, moon)
LightType_Point, // Omnidirectional (bulb)
LightType_Spot, // Cone-shaped (flashlight)
LightType_Rect, // Area light (window)
LightType_MAX
};
Light Types
1. Directional Light (Sun, Moon)
Key File: Engine/Source/Runtime/Engine/Classes/Components/DirectionalLightComponent.h
Characteristics:
- Infinite distance: All rays are parallel
- No attenuation: Same intensity everywhere
- Covers entire scene: Always evaluated for every pixel
- Typical use: Sun, moon, ambient global lighting
class UDirectionalLightComponent : public ULightComponent
{
// Angular diameter of light source (affects shadow softness)
float LightSourceAngle; // Default: 0.5357° (matches sun)
float LightSourceSoftAngle; // Additional soft shadow angle
// Shadow parameters
int32 DynamicShadowCascades; // Number of CSM cascades (1-4)
float CascadeDistributionExponent; // 0.0-1.0, controls cascade spacing
float CascadeTransitionFraction; // 0.0-1.0, smooth blending
// Advanced features
bool bAtmosphereSunLight; // Integrate with sky atmosphere
int32 AtmosphereSunLightIndex; // Which sun (0 or 1)
bool bCastCloudShadows; // Cast shadows from volumetric clouds
float CloudShadowStrength; // Cloud shadow opacity
float CloudShadowOnAtmosphereStrength;
float CloudShadowOnSurfaceStrength;
};
Shadow System:
- Uses Cascaded Shadow Maps (CSM)
- Typically 2-4 cascades
- Each cascade covers progressively larger area with lower resolution
Console Variables:
r.Shadow.CSM.MaxCascades = 4
r.Shadow.DistanceScale = 1.0
r.Shadow.CSMSplitPenumbraScale = 0.5
r.Shadow.MaxResolution = 2048
2. Point Light (Light Bulbs, Street Lights, Lamps)
Key File: Engine/Source/Runtime/Engine/Classes/Components/PointLightComponent.h
Characteristics:
- Omnidirectional: Light radiates in all directions
- Inverse-square falloff: Physically accurate attenuation
- Finite radius:
AttenuationRadiusdefines hard cutoff - Typical use: Indoor lights, street lamps, fire, explosions
class UPointLightComponent : public ULocalLightComponent
{
// Attenuation
bool bUseInverseSquaredFalloff; // Physically accurate vs artistic
float LightFalloffExponent; // When not inverse-squared (2-16)
// Soft shadows (source size)
float SourceRadius; // Physical light source size (cm)
float SoftSourceRadius; // Additional softening
float SourceLength; // For capsule-shaped lights
};
Attenuation Formula:
Inverse-Squared (Physical):
float GetDistanceAttenuation(float Distance, float AttenuationRadius)
{
float NormalizedDistance = saturate(Distance / AttenuationRadius);
float Attenuation = 1.0 / (Distance * Distance + 1.0);
// Smooth cutoff at radius
float WindowFunction = Square(1.0 - Square(NormalizedDistance));
return Attenuation * WindowFunction;
}
Exponential (Artistic):
float GetDistanceAttenuation(float Distance, float AttenuationRadius, float Exponent)
{
float NormalizedDistance = saturate(Distance / AttenuationRadius);
float BaseFalloff = saturate(1.0 - pow(NormalizedDistance, 4.0));
return pow(BaseFalloff, Exponent);
}
Shadow System:
- Cubemap (6 faces) for omnidirectional shadows
- Can use single-pass geometry shader for efficiency
- Virtual Shadow Maps (VSM) support with 6 pages
3. Spot Light (Flashlights, Car Headlights, Stage Lights)
Key File: Engine/Source/Runtime/Engine/Classes/Components/SpotLightComponent.h
Characteristics:
- Cone-shaped: Directional with angular falloff
- Inherits from Point: All point light features + cone
- Two angles: Inner cone (full brightness) and outer cone (falloff)
- Typical use: Flashlights, headlights, street lamps, spotlights
class USpotLightComponent : public UPointLightComponent
{
float InnerConeAngle; // Full brightness angle (degrees)
float OuterConeAngle; // Cutoff angle (degrees)
};
Cone Attenuation:
float GetSpotAngleAttenuation(float3 LightDirection, float3 ToPixel,
float InnerCos, float OuterCos)
{
float CosAngle = dot(normalize(ToPixel), LightDirection);
// Smooth transition from outer to inner cone
float AngleAttenuation = saturate((CosAngle - OuterCos) / (InnerCos - OuterCos));
return Square(AngleAttenuation); // Squared for smoother falloff
}
Shadow System:
- Single shadow map (perspective projection)
- Matches light cone frustum
- VSM support with single page
4. Rect Light (Area Lights, Windows, Panels)
Key File: Engine/Source/Runtime/Engine/Classes/Components/RectLightComponent.h
Characteristics:
- True area light: Rectangular emissive surface
- Accurate soft shadows: Based on actual light size
- Barn doors: Light shaping attachments
- Texture projection: Can project images
- Typical use: Softboxes, windows, LED panels, TV screens
class URectLightComponent : public ULocalLightComponent
{
float SourceWidth; // Width in cm
float SourceHeight; // Height in cm
// Barn doors (light shaping)
float BarnDoorAngle; // Barn door angle (0-90°)
float BarnDoorLength; // Barn door length (cm)
// Texture projection
UTexture* SourceTexture; // Emissive texture
};
Area Light Integration:
// Simplified rect light evaluation
float3 RectLightBRDF(float3 N, float3 V, float3 Points[4], float roughness)
{
// Representative point on rect (approximation)
float3 L = ClosestPointOnRect(Points, V);
float3 H = normalize(V + L);
// Solid angle calculation
float solidAngle = RectSolidAngle(Points, worldPos);
// Standard BRDF with area normalization
float D = D_GGX(roughness, saturate(dot(N, H)));
float Vis = Vis_SmithJointApprox(roughness, NoV, NoL);
float3 F = F_Schlick(specularColor, VoH);
return (D * Vis * F) * solidAngle;
}
Shadow System:
- Single shadow map (perspective or orthographic)
- Soft shadows from accurate area calculation
- Contact shadows for detail
5. Sky Light (Environment Lighting)
Key File: Engine/Source/Runtime/Engine/Classes/Components/SkyLightComponent.h
Characteristics:
- Hemispherical: Surrounds entire scene
- Image-based: Uses cubemap (HDRI or captured)
- Distant lighting: Treated as infinitely far
- Typical use: Sky, ambient environment, IBL
class USkyLightComponent : public ULightComponentBase
{
// Source configuration
ESkyLightSourceType SourceType; // Captured scene or specified cubemap
bool bRealTimeCapture; // Update each frame?
UTextureCube* Cubemap; // Source cubemap (if specified)
// Capture settings
int32 CubemapResolution; // Power of 2 (128-2048)
float SkyDistanceThreshold; // Capture distance cutoff (cm)
float SourceCubemapAngle; // Rotation (0-360°)
// Hemisphere control
bool bLowerHemisphereIsBlack; // Solid ground below?
FLinearColor LowerHemisphereColor;
// Advanced
float OcclusionMaxDistance; // AO distance (200-1500 cm)
float Contrast; // AO contrast (0-1)
float MinOcclusion; // Min AO value (0-1)
};
Storage:
// Spherical harmonics for diffuse
FSHVectorRGB3 IrradianceEnvironmentMap;
// Prefiltered cubemap for specular
FTextureCubeRHIRef ProcessedSkyTexture;
Sampling in Shader:
// Diffuse contribution
float3 DiffuseSkyLight(float3 normal)
{
// Sample SH for fast diffuse lookup
return SampleSH(IrradianceSH, normal);
}
// Specular contribution
float3 SpecularSkyLight(float3 reflectDir, float roughness)
{
// Sample prefiltered cubemap
float mipLevel = RoughnessToMip(roughness);
return SkyLightCubemap.SampleLevel(samplerLinear, reflectDir, mipLevel).rgb;
}
Light Parameters
Intensity and Units
Key File: Engine/Source/Runtime/Engine/Classes/Engine/Scene.h
enum class ELightUnits : uint8
{
Unitless, // Legacy, arbitrary scale
Candelas, // Luminous intensity (normalized)
Lumens, // Luminous power (normalized)
EV, // Exposure value at ISO 100 (normalized)
Nits // Luminance (non-normalized, depends on source size)
};
Conversion to Luminous Power:
// From LightComponent.cpp
float GetLuminousIntensity() const
{
switch (IntensityUnits)
{
case ELightUnits::Candelas:
return Intensity;
case ELightUnits::Lumens:
// Point light: Lumens to candelas
return Intensity / (4.0 * PI);
case ELightUnits::Unitless:
return Intensity * 16.0; // Legacy conversion
case ELightUnits::EV:
return pow(2.0, Intensity - 3.0);
case ELightUnits::Nits:
// Depends on source area
return Intensity * SourceArea * PI;
}
}
Typical Values:
Candle: 1 candela
60W Incandescent Bulb: ~850 lumens (68 candelas for point)
100W Incandescent Bulb: ~1700 lumens (135 candelas)
Flashlight: 100-1000 lumens
Car Headlight: 1000-2000 lumens
Sunlight: ~120,000 lux (EV ~15)
Color Temperature
Blackbody Radiation:
// From LightComponent.h
float Temperature; // Kelvin (1700-12000)
bool bUseTemperature; // Enable/disable
// Preset temperatures:
// - Candle flame: 1850K (orange)
// - Tungsten bulb: 2700K (warm yellow)
// - Halogen: 3200K (yellow-white)
// - Noon sunlight: 5500K (white)
// - D65 white point: 6500K (standard white)
// - Overcast sky: 7000K (cool white)
// - Blue sky: 10000K (blue)
Temperature to RGB Conversion:
FLinearColor GetColorTemperature(float Temp)
{
// Approximation of blackbody radiation
float u = (0.860117757f + 1.54118254e-4f * Temp + 1.28641212e-7f * Temp * Temp) /
(1.0f + 8.42420235e-4f * Temp + 7.08145163e-7f * Temp * Temp);
float v = (0.317398726f + 4.22806245e-5f * Temp + 4.20481691e-8f * Temp * Temp) /
(1.0f - 2.89741816e-5f * Temp + 1.61456053e-7f * Temp * Temp);
// Convert CIE uv to RGB (simplified)
return UVToLinearRGB(u, v);
}
IES Light Profiles
Real-world photometric data:
Key File: Engine/Source/Runtime/Engine/Classes/Engine/TextureLightProfile.h
class UTextureLightProfile : public UTexture2D
{
float Brightness; // Multiplier
float TextureMultiplier; // Additional scale
};
Usage in Shader:
// IES profile stored as 1D or 2D texture
Texture2D IESTexture;
SamplerState IESSampler;
float GetIESAttenuation(float3 worldPos, float3 lightPos, float3 lightDir)
{
float3 toLightDir = normalize(worldPos - lightPos);
// Horizontal angle (azimuth)
float phi = atan2(toLightDir.y, toLightDir.x);
// Vertical angle (elevation)
float theta = acos(dot(toLightDir, lightDir));
// Sample IES texture
float2 uv = float2(theta / PI, (phi + PI) / (2.0 * PI));
float iesValue = IESTexture.SampleLevel(IESSampler, uv, 0).r;
return iesValue * IESBrightnessScale;
}
Common uses:
- Street lights (specific beam patterns)
- Stage/theatrical lighting
- Automotive headlights
- Architectural lighting
Attenuation Radius and Falloff
// From LocalLightComponent.h
float AttenuationRadius; // Hard cutoff distance (cm)
float LightFalloffExponent; // Falloff curve (when not inverse-squared)
Radius Estimation:
// Auto-calculate radius from intensity (when radius is 0)
float GetDefaultLightRadius() const
{
const float MinLightAttenuation = 0.01; // 1% threshold
if (bUseInverseSquaredFalloff)
{
// Inverse-squared: I / (d^2) = threshold
return sqrt(Intensity / MinLightAttenuation);
}
else
{
// Exponential: estimate based on exponent
return Intensity * SomeArtisticScale;
}
}
Source Size (Soft Shadows)
Point/Spot Lights:
float SourceRadius; // Physical light source size (cm)
float SoftSourceRadius; // Additional artistic softening (cm)
float SourceLength; // For capsule-shaped lights (cm)
Directional Lights:
float LightSourceAngle; // Angular diameter (degrees)
float LightSourceSoftAngle; // Additional soft angle (degrees)
// Sun: 0.5357° (0.93° with atmosphere)
// Moon: 0.52°
Effect on Shadows:
- Larger source → softer shadows (wider penumbra)
- Used in ray-traced shadows for cone angle
- Used in PCSS for kernel size
- VSM uses this for shadow softness
Light Mobility
Key File: Engine/Source/Runtime/Engine/Classes/Components/SceneComponent.h
enum class EComponentMobility : uint8
{
Static, // Baked at build time
Stationary, // Hybrid (baked indirect + dynamic direct)
Movable // Fully dynamic
};
Static Lights
Characteristics:
- Completely baked into lightmaps
- Zero runtime cost (texture reads only)
- Best quality: Unlimited bounces, full GI
- Cannot change: Position, color, intensity locked
- Build required: Must rebuild lighting
Use cases:
- Architectural lighting in static buildings
- Environmental lighting (sun in static scenes)
- Any light that never changes
Lightmap Resolution:
// Per-static-mesh setting
int32 OverriddenLightMapRes; // Lightmap texels per unit
// Project-wide
r.LightMap.DefaultLightMapRes = 64
Stationary Lights
Hybrid approach - most complex:
What's Baked:
- Indirect lighting: All bounced light in lightmaps
- Static shadows: Shadows from static geometry
What's Dynamic:
- Direct lighting: Calculated per-frame (allows intensity/color changes)
- Dynamic shadows: Shadows from movable objects
- Specular: Real-time specular highlights
Key Limitation:
// Maximum 4 overlapping stationary lights per pixel
// Uses 4 shadow map channels
int32 ShadowMapChannel; // 0-3, or -1 if no static shadows
Channel Assignment:
// From LightComponent.h
uint32 bHasStaticLighting : 1;
uint32 bHasStaticShadowing : 1;
int32 PreviewShadowMapChannel; // Editor visualization
Console Variables:
r.AllowStaticLighting = 1
r.Shadow.Virtual.Cache = 1 // Cache static shadows in VSM
Use cases:
- Sun in mostly-static scenes with dynamic characters
- Indoor lights where only direct light changes
- Most common for outdoor environments
Movable Lights
Fully dynamic:
- Everything runtime: Lighting and shadows computed per-frame
- Can move/change: Position, color, intensity, radius
- Higher cost: Full shadow rendering every frame
- No baking: Instant iteration
Shadow Options:
bool bCastDynamicShadow; // Enable shadow rendering
bool bCastStaticShadow; // Ignored for movable (always false)
bool bAffectTranslucentLighting;
Use cases:
- Flashlights, muzzle flashes
- Vehicle headlights
- Destructible environments
- Any light that moves or changes
Performance:
// Rough cost estimate (depends on scene complexity):
// - Directional: 2-5 ms (CSM rendering)
// - Spot: 0.5-2 ms (single shadow map)
// - Point: 1-4 ms (cubemap)
// - Rect: 1-3 ms (area light)
Deferred Lighting Pipeline
UE5's primary rendering path for most lights.
Key Files:
Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cppEngine/Source/Runtime/Renderer/Private/LightRendering.cppEngine/Shaders/Private/DeferredLightPixelShaders.usf
Pipeline Overview
1. GBuffer Pass
↓
[GBuffer Textures: Normal, BaseColor, Roughness, Metallic, etc.]
↓
2. Lighting Accumulation Pass (for each light)
↓
[Scene Color += Light Contribution]
↓
3. Post Processing
GBuffer Layout
From: Engine/Shaders/Private/DeferredShadingCommon.ush
struct FGBufferData
{
// Geometric
float3 WorldNormal;
float3 WorldTangent;
float Depth;
// Material PBR properties
float3 BaseColor;
float3 DiffuseColor; // Derived: BaseColor * (1 - Metallic)
float3 SpecularColor; // Derived: lerp(0.04, BaseColor, Metallic)
float Metallic;
float Specular;
float Roughness;
float Anisotropy;
// Lighting modifiers
float GBufferAO;
// Shading model
uint ShadingModelID;
// Custom data (shading model specific)
float4 CustomData;
// Baked lighting
float4 PrecomputedShadowFactors; // 4 shadow channels
};
Render Targets:
RT0: SceneColor (accumulation target)
RT1: GBufferA - WorldNormal (RGB), PerObjectData (A)
RT2: GBufferB - Metallic (R), Specular (G), Roughness (B), ShadingModelID (A)
RT3: GBufferC - BaseColor (RGB), AO (A)
RT4: GBufferD - CustomData (shading model specific)
RT5: GBufferE - PrecomputedShadowFactors (RGBA)
RT6: GBufferF - WorldTangent (RGB), Anisotropy (A)
Deferred Light Shader
From: Engine/Shaders/Private/DeferredLightPixelShaders.usf
void DeferredLightPixelMain(
float4 SVPos : SV_POSITION,
out float4 OutColor : SV_Target0)
{
// 1. Calculate screen UV
float2 ScreenUV = SvPositionToScreenUV(SVPos);
// 2. Read GBuffer
FGBufferData GBuffer = GetGBufferData(ScreenUV);
// 3. Reconstruct world position from depth
float SceneDepth = CalcSceneDepth(ScreenUV);
float3 WorldPosition = ScreenToWorld(ScreenUV, SceneDepth);
// 4. Get light data (from uniform buffer)
FDeferredLightData LightData = InitDeferredLightFromUniforms();
// 5. Calculate light attenuation (distance + angle)
float LightMask = GetLocalLightAttenuation(
WorldPosition,
LightData,
LightData.Normal, // For rect lights
LightData.Direction // For spot lights
);
if (LightMask <= 0.0)
{
OutColor = 0;
return; // Early out if outside light range
}
// 6. Get shadow attenuation
float4 LightAttenuation = GetLightAttenuation(ScreenUV);
// 7. Calculate camera vector
float3 CameraVector = normalize(View.WorldCameraOrigin - WorldPosition);
// 8. Evaluate BRDF lighting
FDeferredLightingSplit Lighting = GetDynamicLighting(
WorldPosition,
CameraVector,
GBuffer,
1.0, // AO
GBuffer.ShadingModelID,
LightData,
LightAttenuation,
uint2(SVPos.xy)
);
// 9. Output (additive blend)
OutColor = float4(Lighting.SpecularLighting + Lighting.DiffuseLighting, 0);
}
BRDF Evaluation
From: Engine/Shaders/Private/DeferredLightingCommon.ush
FDeferredLightingSplit GetDynamicLighting(...)
{
float3 L = LightData.Direction; // Light direction
float3 V = CameraVector; // View direction
float3 N = GBuffer.WorldNormal;
float3 H = normalize(V + L);
float NoL = saturate(dot(N, L));
float NoV = saturate(dot(N, V));
float VoH = saturate(dot(V, H));
float NoH = saturate(dot(N, H));
// Shadow term
float Shadow = LightAttenuation.r;
// Diffuse
float3 Diffuse = Diffuse_Burley(GBuffer.DiffuseColor, GBuffer.Roughness, NoV, NoL, VoH);
// Specular (GGX)
float D = D_GGX(GBuffer.Roughness, NoH);
float Vis = Vis_SmithJointApprox(GBuffer.Roughness, NoV, NoL);
float3 F = F_Schlick(GBuffer.SpecularColor, VoH);
float3 Specular = (D * Vis) * F;
// Apply light color and shadow
Lighting.DiffuseLighting = Diffuse * LightData.Color * NoL * Shadow;
Lighting.SpecularLighting = Specular * LightData.Color * NoL * Shadow;
return Lighting;
}
Blend State (Additive)
Lights accumulate using additive blending:
// From LightRendering.cpp
TStaticBlendState<
CW_RGBA, // Write all channels
BO_Add, // RGB: Add
BF_One, // Src factor: 1
BF_One, // Dst factor: 1
BO_Add, // Alpha: Add
BF_One, // Src alpha factor: 1
BF_One // Dst alpha factor: 1
>::GetRHI()
// Result: SceneColor += LightContribution
Light Volume Rendering
For point and spot lights, render light bounds geometry:
// Point light: Render sphere
// Spot light: Render cone
// Depth test configuration:
// - If camera inside light volume: Disable depth test
// - If camera outside: Use depth test (cull pixels behind geometry)
uint64 DepthState = bCameraInsideLightGeometry ?
BGFX_STATE_DEPTH_TEST_ALWAYS : BGFX_STATE_DEPTH_TEST_GREATER;
Light Culling (Clustered Deferred)
For scenes with many lights (100+), use clustered deferred shading.
Key Files:
Engine/Source/Runtime/Renderer/Private/ClusteredDeferredShadingPass.cppEngine/Source/Runtime/Renderer/Private/LightGridInjection.cppEngine/Shaders/Private/ClusteredDeferredShadingPixelShader.usf
3D Grid Structure
// Grid parameters
int32 GridPixelSize = 64; // Tile size (64×64 pixels)
int32 GridSizeZ = 32; // Depth slices
// Total cells
int32 GridSizeX = (ScreenWidth + GridPixelSize - 1) / GridPixelSize;
int32 GridSizeY = (ScreenHeight + GridPixelSize - 1) / GridPixelSize;
int32 TotalCells = GridSizeX * GridSizeY * GridSizeZ;
Light Grid Injection
Build light grid (compute shader):
// cs_light_grid_injection.hlsl
[numthreads(4, 4, 4)] // Process 4x4x4 cells per thread group
void BuildLightGridCS(uint3 GroupId : SV_GroupID, uint3 ThreadId : SV_GroupThreadID)
{
uint3 GridCoord = GroupId * 4 + ThreadId;
if (any(GridCoord >= GridDimensions))
return;
// Calculate cell bounds in world space
float3 MinBounds, MaxBounds;
GetCellBounds(GridCoord, MinBounds, MaxBounds);
// Test each light
uint NumLightsInCell = 0;
uint LightIndices[MAX_LIGHTS_PER_CELL];
for (uint lightIndex = 0; lightIndex < NumLights; ++lightIndex)
{
FLightData light = Lights[lightIndex];
// Test light bounds vs cell bounds
if (IntersectLightWithCell(light, MinBounds, MaxBounds))
{
LightIndices[NumLightsInCell++] = lightIndex;
if (NumLightsInCell >= MAX_LIGHTS_PER_CELL)
break; // Cell full
}
}
// Write to light grid
uint cellIndex = GridCoordToIndex(GridCoord);
LightGrid[cellIndex].NumLights = NumLightsInCell;
LightGrid[cellIndex].LightIndexStart = AtomicAdd(GlobalLightIndexCounter, NumLightsInCell);
// Write light indices
for (uint i = 0; i < NumLightsInCell; ++i)
{
uint writeIndex = LightGrid[cellIndex].LightIndexStart + i;
GlobalLightIndexList[writeIndex] = LightIndices[i];
}
}
Clustered Shading Pass
Single fullscreen pass evaluates all lights:
// fs_clustered_deferred.hlsl
void ClusteredDeferredPS(
float4 SvPosition : SV_Position,
out float4 OutColor : SV_Target0)
{
float2 ScreenUV = SvPositionToScreenUV(SvPosition);
// Read GBuffer
FGBufferData GBuffer = GetGBufferData(ScreenUV);
float3 WorldPos = ReconstructWorldPosition(ScreenUV, GBuffer.Depth);
float3 V = normalize(CameraPos - WorldPos);
// Find grid cell
uint3 GridCoord = WorldToGridCoord(WorldPos, SvPosition.xy);
uint CellIndex = GridCoordToIndex(GridCoord);
// Get lights in this cell
uint NumLights = LightGrid[CellIndex].NumLights;
uint LightIndexStart = LightGrid[CellIndex].LightIndexStart;
// Accumulate lighting
float3 AccumulatedLighting = 0;
for (uint i = 0; i < NumLights; ++i)
{
uint lightIndex = GlobalLightIndexList[LightIndexStart + i];
FLightData light = Lights[lightIndex];
// Evaluate light
float3 L = normalize(light.Position - WorldPos);
float attenuation = CalculateAttenuation(WorldPos, light);
if (attenuation > 0.0)
{
// BRDF evaluation
float3 lighting = EvaluateBRDF(GBuffer, V, L) * attenuation * light.Color;
AccumulatedLighting += lighting;
}
}
OutColor = float4(AccumulatedLighting, 1.0);
}
Performance Benefits
Traditional deferred:
- O(Lights × Pixels) - Each light = separate pass
- Example: 100 lights × 1920×1080 = ~200M pixel shades
Clustered deferred:
- O(Pixels + Lights × Cells) - Single pass
- Example: 1920×1080 + 100 lights × (30×17×32) = ~2M + 1.6M = 3.6M
Savings: ~98% reduction in overdraw
Console Variables:
r.LightCulling.Quality = 1 // 0=off, 1=on
r.LightGridPixelSize = 64 // Tile size
r.LightGridSizeZ = 32 // Z slices
r.LightGridMaxCulledLights = 256 // Max lights per cell
Forward Lighting
Alternative rendering path, used for translucency and mobile.
Key File: Engine/Shaders/Private/ForwardLightingCommon.ush
When Forward is Used
- Translucent materials (always forward)
- Mobile rendering (primary path)
- VR forward renderer (optional)
- Hair/fur (forward+ with visibility buffer)
Forward Lighting Evaluation
// From ForwardLightingCommon.ush
FDeferredLightingSplit GetForwardDirectLighting(
uint GridIndex,
float3 WorldPosition,
FGBufferData GBuffer,
...)
{
FDeferredLightingSplit DirectLighting = (FDeferredLightingSplit)0;
// 1. Directional light (sun)
const FDirectionalLightData DirectionalLight = GetDirectionalLightData(0);
{
float3 L = DirectionalLight.Direction;
float Shadow = GetDirectionalLightShadow(ScreenUV);
DirectLighting += EvaluateLight(GBuffer, V, L, DirectionalLight.Color, Shadow);
}
// 2. Local lights (from light grid)
const FCulledLightsGridData GridData = GetCulledLightsGrid(GridIndex);
for (uint LocalLightIndex = 0; LocalLightIndex < GridData.NumLocalLights; ++LocalLightIndex)
{
uint LightIndex = ForwardLightData.CulledLightDataGrid[GridData.DataStartIndex + LocalLightIndex];
FLocalLightData LocalLight = GetLocalLightData(LightIndex);
float3 L = normalize(LocalLight.Position - WorldPosition);
float Attenuation = CalculateAttenuation(WorldPosition, LocalLight);
float Shadow = GetLocalLightShadow(ScreenUV, LightIndex);
DirectLighting += EvaluateLight(GBuffer, V, L, LocalLight.Color * Attenuation, Shadow);
}
return DirectLighting;
}
Forward vs Deferred Comparison
| Feature | Deferred | Forward |
|---|---|---|
| MSAA Support | No (GBuffer incompatible) | Yes |
| Translucency | Separate pass | Native |
| Memory | High (GBuffer) | Lower |
| Light Count | Excellent (many lights) | Good (moderate lights) |
| Material Variations | Single shader | Many variants |
| Bandwidth | High (GBuffer reads/writes) | Lower |
bgfx Implementation Guide
Basic Deferred Lighting
class DeferredLightRenderer
{
// Light uniform buffer
struct LightUniform
{
vec4 position; // xyz = position, w = radius
vec4 color; // rgb = color, a = intensity
vec4 direction; // xyz = direction (spot/directional)
vec4 params; // x = inner cone, y = outer cone, z = falloff exp
};
std::vector<LightUniform> lights;
bgfx::UniformHandle u_lightData;
bgfx::ProgramHandle directionalLightShader;
bgfx::ProgramHandle pointLightShader;
bgfx::ProgramHandle spotLightShader;
void RenderLights(const GBuffer& gbuffer)
{
// Bind GBuffer textures
bgfx::setTexture(0, s_gbufferNormal, gbuffer.normalTexture);
bgfx::setTexture(1, s_gbufferBaseColor, gbuffer.baseColorTexture);
bgfx::setTexture(2, s_gbufferMaterial, gbuffer.materialTexture);
bgfx::setTexture(3, s_gbufferDepth, gbuffer.depthTexture);
// Additive blend state
uint64_t state = BGFX_STATE_WRITE_RGB
| BGFX_STATE_WRITE_A
| BGFX_STATE_BLEND_ADD
| BGFX_STATE_DEPTH_TEST_EQUAL; // Don't write depth
bgfx::setState(state);
// Render each light
for (const auto& light : lights)
{
bgfx::setUniform(u_lightData, &light);
if (light.type == LIGHT_DIRECTIONAL)
{
// Fullscreen quad
DrawFullscreenQuad();
bgfx::submit(VIEW_LIGHTING, directionalLightShader);
}
else if (light.type == LIGHT_POINT)
{
// Render light sphere volume
DrawLightSphere(light.position, light.radius);
bgfx::submit(VIEW_LIGHTING, pointLightShader);
}
else if (light.type == LIGHT_SPOT)
{
// Render light cone volume
DrawLightCone(light);
bgfx::submit(VIEW_LIGHTING, spotLightShader);
}
}
}
};
Deferred Light Shader (GLSL)
// fs_deferred_point_light.sc
$input v_texcoord0
#include <bgfx_shader.sh>
SAMPLER2D(s_gbufferNormal, 0);
SAMPLER2D(s_gbufferBaseColor, 1);
SAMPLER2D(s_gbufferMaterial, 2); // r=roughness, g=metallic, b=specular
SAMPLER2D(s_gbufferDepth, 3);
uniform vec4 u_lightPosRadius; // xyz = position, w = radius
uniform vec4 u_lightColor; // rgb = color, a = intensity
uniform vec4 u_lightParams; // x = falloff exponent
vec3 ReconstructWorldPosition(vec2 uv, float depth)
{
// Reconstruct from depth
vec4 clipPos = vec4(uv * 2.0 - 1.0, depth, 1.0);
vec4 viewPos = mul(u_invProj, clipPos);
viewPos /= viewPos.w;
vec4 worldPos = mul(u_invView, viewPos);
return worldPos.xyz;
}
float GetAttenuation(float distance, float radius, float exponent)
{
float normalizedDist = saturate(distance / radius);
float baseFalloff = saturate(1.0 - pow(normalizedDist, 4.0));
return pow(baseFalloff, exponent);
}
void main()
{
vec2 uv = v_texcoord0;
// Read GBuffer
vec3 normal = texture2D(s_gbufferNormal, uv).rgb * 2.0 - 1.0;
vec4 baseColorAO = texture2D(s_gbufferBaseColor, uv);
vec3 baseColor = baseColorAO.rgb;
float ao = baseColorAO.a;
vec3 material = texture2D(s_gbufferMaterial, uv).rgb;
float roughness = material.r;
float metallic = material.g;
float specular = material.b;
float depth = texture2D(s_gbufferDepth, uv).r;
// Reconstruct position
vec3 worldPos = ReconstructWorldPosition(uv, depth);
// Light calculation
vec3 lightPos = u_lightPosRadius.xyz;
float lightRadius = u_lightPosRadius.w;
vec3 L = lightPos - worldPos;
float distance = length(L);
L /= distance; // Normalize
// Attenuation
float attenuation = GetAttenuation(distance, lightRadius, u_lightParams.x);
if (attenuation <= 0.0)
{
discard; // Outside light range
}
// View vector
vec3 V = normalize(u_cameraPos - worldPos);
// BRDF (simplified)
vec3 H = normalize(V + L);
float NoL = max(dot(normal, L), 0.0);
float NoV = max(dot(normal, V), 0.0);
float NoH = max(dot(normal, H), 0.0);
float VoH = max(dot(V, H), 0.0);
// Derived colors
vec3 diffuseColor = baseColor * (1.0 - metallic);
vec3 specularColor = mix(vec3_splat(0.04), baseColor, metallic);
// Diffuse (Burley)
float FD90 = 0.5 + 2.0 * VoH * VoH * roughness;
float FdV = 1.0 + (FD90 - 1.0) * pow(1.0 - NoV, 5.0);
float FdL = 1.0 + (FD90 - 1.0) * pow(1.0 - NoL, 5.0);
vec3 diffuse = diffuseColor * (1.0 / 3.14159) * FdV * FdL;
// Specular (GGX)
float a = roughness * roughness;
float a2 = a * a;
// D (GGX)
float denom = (NoH * a2 - NoH) * NoH + 1.0;
float D = a2 / (3.14159 * denom * denom);
// Vis (Smith)
float k = a * 0.5;
float vis = 0.5 / ((NoL * (NoV * (1.0 - k) + k) + NoV * (NoL * (1.0 - k) + k)));
// F (Schlick)
float Fc = pow(1.0 - VoH, 5.0);
vec3 F = specularColor + (vec3_splat(1.0) - specularColor) * Fc;
vec3 spec = D * vis * F;
// Combine
vec3 lighting = (diffuse + spec) * u_lightColor.rgb * u_lightColor.a * NoL * attenuation * ao;
gl_FragColor = vec4(lighting, 1.0);
}
Clustered Lighting (Simplified)
// Build light grid
class ClusteredLightCuller
{
static const int GRID_SIZE_X = 30; // 1920 / 64
static const int GRID_SIZE_Y = 17; // 1080 / 64
static const int GRID_SIZE_Z = 32;
struct GridCell
{
uint32_t lightIndexStart;
uint32_t numLights;
};
GridCell cells[GRID_SIZE_X * GRID_SIZE_Y * GRID_SIZE_Z];
std::vector<uint32_t> lightIndices;
bgfx::DynamicIndexBufferHandle lightIndexBuffer;
bgfx::DynamicVertexBufferHandle cellDataBuffer;
void BuildLightGrid(const std::vector<Light>& lights)
{
lightIndices.clear();
for (int z = 0; z < GRID_SIZE_Z; ++z)
{
for (int y = 0; y < GRID_SIZE_Y; ++y)
{
for (int x = 0; x < GRID_SIZE_X; ++x)
{
int cellIdx = x + y * GRID_SIZE_X + z * GRID_SIZE_X * GRID_SIZE_Y;
// Calculate cell bounds
AABB cellBounds = GetCellBounds(x, y, z);
// Test each light
uint32_t startIdx = lightIndices.size();
for (uint32_t i = 0; i < lights.size(); ++i)
{
if (LightIntersectsCell(lights[i], cellBounds))
{
lightIndices.push_back(i);
}
}
cells[cellIdx].lightIndexStart = startIdx;
cells[cellIdx].numLights = lightIndices.size() - startIdx;
}
}
}
// Upload to GPU
bgfx::update(lightIndexBuffer, 0, bgfx::copy(lightIndices.data(),
lightIndices.size() * sizeof(uint32_t)));
bgfx::update(cellDataBuffer, 0, bgfx::copy(cells, sizeof(cells)));
}
};
Key Files Reference
C++ Source Files
Light Components:
Engine/Source/Runtime/Engine/Classes/Components/LightComponent.hEngine/Source/Runtime/Engine/Classes/Components/DirectionalLightComponent.hEngine/Source/Runtime/Engine/Classes/Components/PointLightComponent.hEngine/Source/Runtime/Engine/Classes/Components/SpotLightComponent.hEngine/Source/Runtime/Engine/Classes/Components/RectLightComponent.hEngine/Source/Runtime/Engine/Classes/Components/SkyLightComponent.h
Rendering:
Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp- Main render loop
Engine/Source/Runtime/Renderer/Private/LightRendering.cpp- Deferred light rendering
Engine/Source/Runtime/Renderer/Private/ClusteredDeferredShadingPass.cpp- Clustered deferred shading
Engine/Source/Runtime/Renderer/Private/LightGridInjection.cpp- Light culling and grid building
Data Structures:
Engine/Source/Runtime/Engine/Public/SceneTypes.h- Light type enums
Engine/Source/Runtime/Engine/Classes/Engine/Scene.h- Light units enum
Shader Files
Deferred Lighting:
Engine/Shaders/Private/DeferredLightingCommon.ush- BRDF evaluation
Engine/Shaders/Private/DeferredLightPixelShaders.usf- Per-light pixel shaders
Engine/Shaders/Private/DeferredShadingCommon.ush- GBuffer utilities
Forward Lighting:
Engine/Shaders/Private/ForwardLightingCommon.ush- Forward lighting evaluation
Clustered:
Engine/Shaders/Private/ClusteredDeferredShadingPixelShader.usf- Clustered shading pass
Engine/Shaders/Private/LightGridInjection.usf- Light grid building compute shader
Summary
For Your Custom Engine:
-
Start with basic deferred lighting
- Directional, point, spot lights
- Simple attenuation and BRDF
- Additive accumulation
-
Add light parameters
- Intensity units (lumens/candelas)
- Color temperature
- Attenuation curves
-
Implement light culling
- Tiled deferred (8×8 tiles)
- Or clustered deferred (3D grid)
- Dramatically improves multi-light performance
-
Optimize
- Light volumes (sphere/cone rendering)
- Depth bounds test
- Stencil masking
Reference Values:
- Indoor bulb: 800-1700 lumens
- Car headlight: 1000-2000 lumens
- Sunlight: EV 15, ~6500K
- Attenuation radius: Auto-calculate from intensity
UE5's lighting system is battle-tested across hundreds of shipped titles. Following this architecture gives you physically-based, scalable lighting that looks great and performs well.