Skip to content

Real-Time Global Illumination — Screen Space, Voxel Cone and Light Probes

DodaTech Updated 2026-06-21 7 min read

In this tutorial, you'll learn about Real. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Real-time global illumination approximates how light bounces between surfaces in a scene, adding realistic ambient lighting, color bleeding, and indirect reflections without the cost of Path Tracing.

What You'll Learn & Why It Matters

In this tutorial, you will learn real-time GI techniques — screen space ambient occlusion (SSAO), screen space reflections (SSR), voxel cone tracing for diffuse GI, and light probes for dynamic scenes. These techniques power the indirect lighting in modern games.

Real-world use: Unreal Engine 5's Lumen, Frostbite's GI system, and Unity's GPU Progressive Lightmapper all use real-time GI techniques. DodaTech's Rendering Pipeline uses SSAO for enhanced depth perception in visualization tools.

Prerequisites

  • Lighting Models (previous)
  • Post-Processing (previous)
  • Voxel Rendering concepts

Learning Path

flowchart LR
  A[Lighting Models] --> B[Real-Time GI]
  B --> C[Path Tracing]
  B --> D[BRDF and Materials]
  B --> E[Post-Processing]
  C --> F[Voxel Rendering]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Screen Space Ambient Occlusion (SSAO)

SSAO approximates indirect lighting by sampling the depth buffer around each pixel and checking for occlusion:

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D noiseTexture;
uniform vec3 samples[64];

void main()
{
    vec3 fragPos = texture(gPosition, TexCoords).rgb;
    vec3 normal = normalize(texture(gNormal, TexCoords).rgb);
    vec3 randomVec = texture(noiseTexture, TexCoords * noiseScale).xyz;

    vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 TBN = mat3(tangent, bitangent, normal);

    float occlusion = 0.0;
    float radius = 0.5;
    int kernelSize = 64;

    for (int i = 0; i < kernelSize; i++)
    {
        vec3 samplePos = TBN * samples[i];
        samplePos = fragPos + samplePos * radius;

        vec4 offset = projection * vec4(samplePos, 1.0);
        offset.xyz /= offset.w;
        offset.xyz = offset.xyz * 0.5 + 0.5;

        float sampleDepth = texture(gPosition, offset.xy).z;
        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
        occlusion += (sampleDepth >= samplePos.z + 0.025 ? 1.0 : 0.0) * rangeCheck;
    }

    float ao = 1.0 - (occlusion / kernelSize);
    FragColor = vec4(vec3(ao), 1.0);
}
flowchart TD
  A[Depth Buffer] --> B[Sample Hemisphere Around Pixel]
  B --> C[Compare Sample Depth to Scene Depth]
  C --> D[Count Occluded Samples]
  D --> E[Ambient Occlusion Factor]
  E --> F[Multiply Scene Lighting by AO]

Screen Space Reflections (SSR)

SSR traces Reflection rays in screen space using the depth and normal buffers:

uniform sampler2D gColor;
uniform sampler2D gNormal;
uniform sampler2D gDepth;

vec3 screen_space_reflect(vec3 viewPos, vec3 viewDir, vec3 normal)
{
    vec3 reflectDir = normalize(reflect(viewDir, normal));
    vec3 position = viewPos;
    vec3 step = reflectDir * 0.1;

    for (int i = 0; i < 32; i++)
    {
        position += step;
        vec4 projPos = projection * vec4(position, 1.0);
        vec2 uv = projPos.xy / projPos.w * 0.5 + 0.5;
        if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1)
            break;

        float depth = texture(gDepth, uv).r;
        float viewDepth = -position.z;
        if (abs(viewDepth - depth) < 0.05)
        {
            return texture(gColor, uv).rgb;
        }
    }
    return vec3(0.0);
}

Voxel Cone Tracing

Voxel cone tracing GI voxelizes the scene into a 3D texture, then traces cones through the voxel grid to gather indirect light:

// Voxel texture (3D texture storing color + normal)
uniform sampler3D voxelTexture;
uniform vec3 voxelGridCenter;
uniform float voxelGridExtent;

vec3 cone_trace(vec3 position, vec3 direction, float coneAngle, int steps)
{
    vec3 color = vec3(0.0);
    float diameter = 0.0;
    vec3 pos = position;

    for (int i = 0; i < steps; i++)
    {
        float stepSize = diameter * 2.0;
        pos += direction * stepSize;
        diameter += stepSize * tan(coneAngle);

        vec3 uv = (pos - voxelGridCenter) / voxelGridExtent + 0.5;
        if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1 || uv.z < 0 || uv.z > 1)
            break;

        // Sample mip level based on cone diameter
        float mipLevel = log2(diameter * voxelGridExtent);
        vec3 voxelColor = textureLod(voxelTexture, uv, mipLevel).rgb;
        color += voxelColor * (1.0 - length(color));  // Accumulate
    }
    return color;
}
flowchart TD
  A[Scene Geometry] --> B[Voxelization Pass]
  B --> C[3D Voxel Texture]
  D[Surface Point] --> E[Trace Cone into Voxel Grid]
  E --> F[Sample Mip Levels]
  F --> G[Accumulate Indirect Light]
  G --> H[Add to Direct Lighting]

Light Probes

Light probes store pre-computed indirect lighting at specific positions:

struct LightProbe
{
    glm::vec3 position;
    float shCoefficients[9][3];  // Spherical harmonics coefficients
    float influenceRadius;
};

std::vector<LightProbe> probes;

// Place probes in scene
for (const auto& pos : probePositions)
{
    LightProbe probe;
    probe.position = pos;
    probe.influenceRadius = 5.0f;
    // Bake: render cubemap, project to SH
    bake_probe(probe, scene);
    probes.push_back(probe);
}
// Sample nearest light probes for dynamic objects
uniform LightProbe probes[64];
uniform int probeCount;

vec3 sample_probes(vec3 position, vec3 normal, vec3 viewDir)
{
    vec3 indirect = vec3(0.0);
    float totalWeight = 0.0;

    for (int i = 0; i < probeCount; i++)
    {
        vec3 delta = position - probes[i].position;
        float dist = length(delta);
        if (dist < probes[i].influenceRadius)
        {
            float weight = 1.0 - (dist / probes[i].influenceRadius);
            weight = max(weight, 0.0) * max(weight, 0.0);
            indirect += evaluate_sh(probes[i].shCoefficients, normal) * weight;
            totalWeight += weight;
        }
    }

    return totalWeight > 0.0 ? indirect / totalWeight : vec3(0.02);
}

Combined GI Integration

void main()
{
    // Direct lighting
    vec3 direct = calculate_direct_lighting(N, L, V, material);

    // Indirect diffuse (SSAO + light probes)
    float ao = texture(aoTexture, TexCoords).r;
    vec3 indirectDiffuse = sample_probes(fragPos, N, V) * ao;

    // Indirect specular (SSR)
    vec3 indirectSpecular = screen_space_reflect(viewPos, viewDir, N);

    vec3 finalColor = direct + indirectDiffuse + indirectSpecular;
    FragColor = vec4(finalColor, 1.0);
}

Common Errors & Mistakes

1. SSAO Banding

Mistake: Visible banding patterns in the AO result because the hemisphere samples are not randomized per pixel.

Fix: Rotate the hemisphere samples using a noise texture. Use a 4x4 noise pattern and apply it via TBN matrix rotation.

2. SSR Edge Clamping

Mistake: SSR reflections abruptly disappear at screen edges or show hard cutoffs.

Fix: Fade SSR intensity near screen edges using a smooth falloff. Use rough reflections (blur) for off-screen contributions.

3. Voxel Leaking

Mistake: Voxel GI samples bleeding through thin walls because the voxel resolution is too low.

Fix: Increase voxel grid resolution for thin geometry, use conservative voxelization, or dilate the voxel grid to fill gaps.

4. Light Probe Leaking

Mistake: Light probes sampling light through walls when placed near geometry boundaries.

Fix: Place probes with visibility checks. Use probe volumes that respect scene geometry. Interpolate only between probes with line-of-sight to the shading point.

Practice Questions

Question 1

What is the difference between SSAO and full GI?

Show answer SSAO only approximates occlusion of ambient light near contact points (crevices, corners). Full GI also handles color bleeding, indirect reflections, and light bouncing between surfaces of different colors.

Question 2

Why are light probes baked rather than computed in real time?

Show answer Baking light probes pre-computes indirect lighting from many directions via Monte Carlo Path Tracing. This is too expensive to compute per frame, so results are stored and reused until the scene changes.

Question 3

What is the main limitation of screen space reflections?

Show answer SSR only reflects geometry that is visible on screen. Objects behind the camera, occluded by other objects, or outside the view frustum have no screen space data and cannot be reflected.

Question 4

How does voxel cone tracing handle different levels of detail?

Show answer The voxel grid stores mipmap levels. Wider cones (for distant indirect light) sample higher mip levels (lower resolution), while narrow cones sample fine detail. This naturally handles LOD.

Challenge

Implement a real-time GI system combining SSAO, SSR, and voxel cone tracing. Compare the quality and performance of each technique individually and combined on a test scene with both indoor and outdoor areas.

FAQ

What is the difference between Lumen and Voxel Cone Tracing?

Lumen (Unreal Engine 5) uses a combination of signed distance field tracing and mesh distance fields for GI. Voxel cone tracing uses a voxelized representation. Lumen handles dynamic geometry better but is more expensive.

Can real-time GI work on mobile?

Mobile GPUs can handle SSAO and simplified light probes. Voxel cone tracing and SSR are typically limited to desktop consoles due to memory and compute requirements.

What is radiance Caching?

Radiance Caching stores indirect lighting at sparse points and interpolates between them. It is similar to light probes but adaptive — more cachepoints are placed in areas with high lighting variation.

Is real-time GI replacing baked lighting?

Real-time GI is becoming more common, but baked lighting remains the standard for static scenes due to higher quality and lower runtime cost. Many games use a hybrid: baked GI for static geometry and real-time probes for dynamic objects.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Author: DodaTech | Last updated: June 21, 2026

DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro