Skip to main content
2022 C++17 OpenGL 3.2 GLSL 330 GLFW GLM

3D Rendering Engine with Shadow Mapping

Custom OpenGL engine comparing four shadow mapping algorithms with runtime switching

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