3D Rendering Engine with Shadow Mapping
Custom OpenGL engine comparing four shadow mapping algorithms with runtime switching
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
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
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
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;
}
};
};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);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
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
Related Projects
Monte Carlo Path Tracer + Spline Engine
Physically-based renderer with importance sampling and a functorial spline animation system
Physically-based Monte Carlo path tracer implementing the rendering equation with importance sampling, Russian roulette termination, explicit direct light sampling via solid-angle cone sampling, and three BxDF models (Lambertian, specular with Fresnel, Oren-Nayar rough diffuse)
Tossing Balls
High school graduation project — 2D physics puzzle game with Box2D integration and data-driven level loading
High school graduation project (age 17) — built a complete 2D physics game engine from scratch in C++ with a custom entity system, polymorphic animation framework, Box2D physics integration, and data-driven level loading from XML
Pac-Man
Arcade-faithful clone with per-ghost personality AI and A* pathfinding
Implemented authentic Pac-Man ghost AI with per-ghost personality targeting algorithms (direct pursuit, 4-tile ambush, vector-doubling flanking, distance-threshold retreat) faithfully matching the original arcade's documented behavior