← Back to CV

Graphics & Simulation

3D Rendering Engine with Shadow Mapping

Custom OpenGL engine comparing four shadow mapping algorithms with runtime switching

2022 C++17 OpenGL 3.2 GLSL 330 GLFW GLM Assimp FreeType DevIL
3D Rendering Engine with Shadow Mapping screenshot

Overview

A custom 3D rendering engine built from scratch in C++17 and OpenGL, designed as a testbed for a seminar paper comparing four shadow mapping algorithms. The engine implements a full 3-pass rendering pipeline (shadow, scene, post-processing), a scene graph with cameras, lights, rigid bodies, sprites, and skyboxes, an in-engine console REPL for live parameter tuning, and a post-processing pipeline with six screen-space effects.

Architecture

3D Rendering Engine with Shadow Mapping architecture

The renderer executes three passes per frame: a shadow pass rendering from the light's perspective into depth textures (format varies by algorithm), a scene pass rendering into an off-screen FBO with dual color attachments for bloom extraction, and a post-processing pass applying toggleable effects via a full-screen quad. The scene graph uses gSceneNode as an abstract tree with recursive render/update propagation across rigid bodies, lights, sprites, skyboxes, and cameras.

Key concepts

Shadow Algorithm Discriminated Union

Shadow Algorithm Discriminated Union

Four shadow algorithms modeled as a C tagged union with an enum discriminant. Each variant owns different GPU resources (depth textures, blur targets, cascade arrays) with type-safe destructor dispatch. Enables runtime algorithm switching via the console without restarting.

Variance Shadow Mapping (VSM)

Stores depth and depth-squared in RG32F textures, using screen-space partial derivatives (dFdx/dFdy) for accurate second moments. Shadow testing uses Chebyshev's inequality with a linstep for light bleeding reduction, plus a two-pass separable Gaussian blur for soft edges.

Cascaded Shadow Mapping (CSM)

Computes per-cascade orthographic projections by extracting view frustum corners for each slice, transforming to light space, and computing tight axis-aligned bounding boxes. Each cascade gets independent resolution (8192 down to 512), concentrating GPU texture memory on near-camera shadows.

Code highlights

Discriminated union for shadow algorithms
struct ShadowAlgorithm {
    E_SHADOW_ALGORITHM type;
    union {
        DefaultShadowMapping      def;
        PercentageCloserFiltering pcf;
        VarianceShadowMapping     vsm;
        CascadeShadowMapping      csm;
    };
};

struct gShadowInfo {
    ~gShadowInfo() {
        switch (algorithm.type) {
            case ESA_NONE: delete algorithm.def.texture; break;
            case ESA_PCF:  delete algorithm.pcf.texture; break;
            case ESA_VSM:  delete algorithm.vsm.texture; break;
            case ESA_CSM:  /* cascade cleanup */ break;
        }
    };
};
VSM second moment via screen-space partial derivatives
case ESA_VSM:
    float depth = gl_FragCoord.z;
    float dx = dFdx(depth);
    float dy = dFdy(depth);
    float moment2 = depth * depth + 0.25 * (dx * dx + dy * dy);
    FragColor = vec4(depth, moment2, 0, 1);
Separable Gaussian blur for VSM
void gSceneManager::applyGaussianBlur(gTexture *depthMap, float amount) {
    m_pFilterShader->useProgram();
    m_pFilterShader->setVector3f("BlurScale",
        vec3(1.0f / (depthMap->width * amount), 0.0f, 0.0f));
    applyFilter(m_pFilterShader, depthMap, shadowMapTempTarget);
    m_pFilterShader->setVector3f("BlurScale",
        vec3(0.0f, 1.0f / (depthMap->width * amount), 0.0f));
    applyFilter(m_pFilterShader, shadowMapTempTarget, depthMap);
}

Performance

3D Rendering Engine with Shadow Mapping performance chart

Shader uniform location caching avoids repeated GL queries per frame. Front-face culling during shadow pass eliminates shadow acne. Per-cascade resolution (8192 to 512) concentrates GPU memory on near-camera shadows. Separable Gaussian blur reduces O(n^2) texture samples to O(2n). Render-to-texture architecture with bindAsRenderTarget() for zero-copy depth rendering. Pre-reserved scene containers avoid reallocation during setup.

Highlights

  • Implemented and compared four shadow mapping algorithms (default, PCF, VSM with Gaussian blur, CSM with per-cascade resolution) in a single engine with runtime switching
  • Built a complete 3D engine from scratch (~7,600 lines C++/GLSL) with a 3-pass rendering architecture, scene graph, model loading, text rendering, cubemap skyboxes, and material presets
  • Designed an in-engine console REPL for live parameter tuning with command history, animated overlay, and a templated command executor pattern
  • Post-processing pipeline with six screen-space effects including SSAO (multi-ring spiral sampling), bloom (MRT bright pixel extraction), edge detection, and blur
  • Cross-platform C++17 codebase with CMake targeting macOS (arm64/x86_64), Linux, and Windows