← back to portfolio
yanghyunw00.github.io / portfolio.txt / academic / post-processing.txt
Depth Buffer & Post-Processing Pipeline
DigiPen CS200 Computer Graphics I
course DigiPen CS200
stack C++ · OpenGL · ImGui
# Overview
This assignment is a from-scratch project (no starter code) implementing depth buffer rendering and a multi-stage post-processing pipeline over a custom 2D diorama scene. Opaque and transparent objects are rendered with different depth-test rules, the scene is rendered into an MSAA offscreen framebuffer, and a chain of full-screen shader passes applies gamma correction, chromatic aberration, sharpening, and a distance-based dilation (bloom-like) effect.
# Tasks Completed
- Implemented a draw-order system for opaque objects in DrawOpaqueObjects() (DemoScene.cpp) that collects water/terrain/seaweed/fish quads into a vector and reorders them across three selectable modes — Front-to-Back (ascending), Back-to-Front (descending), and Random (std::mt19937 shuffle) — toggled live via ImGui radio buttons for side-by-side comparison.
- Implemented depth-tested opaque rendering by enabling GL_DEPTH_TEST / glDepthFunc(GL_LESS) and glDepthMask(GL_TRUE), passing each quad's depth value through an aDepth vertex attribute so instanced.vert writes it directly to gl_Position.z (instanced-renderer based).
- Implemented painter's-algorithm transparent rendering in DrawTransparentObjects() — every frame, bubble objects (alpha ≈ 0.7, 0xFFFFFFB2) are sorted back-to-front by depth with std::sort, then drawn with GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA blending under glDepthMask(GL_FALSE) + glDepthFunc(GL_LESS).
- Implemented an MSAA offscreen render target (OffscreenFramebuffer) as two FBOs — a multisampled renderbuffer FBO (glRenderbufferStorageMultisample, configurable up to GL_MAX_SAMPLES in 2×/4×/8×... steps) and a resolve FBO with an RGBA8 color texture plus a DEPTH_COMPONENT24 texture — resolving MSAA into a regular texture every frame via glBlitFramebuffer.
- Implemented a modular post-processing pipeline (PostProcessingPipeline) where each PostProcessingEffect owns its own framebuffer and draws a 3-vertex full-screen triangle, chaining one effect's output texture into the next effect's input in a ping-pong structure.
- Implemented four post-processing shaders:
- Gamma Correction — pow(color.rgb, vec3(uGamma)) (default 2.2), placed last in the chain.
- Chromatic Aberration — offsets the UV per R/G/B channel (0.009 / 0.006 / −0.006) along the direction vector from a focus point, with focus X/Y adjustable via ImGui sliders (0–1).
- Sharpen — a 5-tap convolution kernel (center weight amount×4+1, neighbor weight −amount, amount=0.8) implementing unsharp masking.
- Dilation (bloom-like spread) — scans a circular neighborhood (distance ≤ size) for the sample with the highest luminance (dot(rgb, vec3(0.3, 0.59, 0.11))), then blends it with the original via smoothstep(0.2, 0.5, maxLuma), with radius/step controlled by ImGui sliders (0–10).
- Implemented the ImGui control panel — draw-order radio buttons, an FPS counter (util::FPS), a draw-call counter, an MSAA on/off checkbox with a sample-count combo box (a power-of-two list built from GL_MAX_SAMPLES), per-effect enable checkboxes, and per-effect parameter sliders (gamma 0.5–4.0, chromatic aberration focus 0–1, dilation parameters 0–10).
- Designed a 2D underwater diorama scene (DemoScene) — a 100×30 tile sand/background grid, 100 seaweed decorations, fish sprites scalable from 500 up to 50,000 (randomly placed with depth in [-1, 1]), and up to 10,000 swaying bubble particles — sized so the depth-sort performance difference is noticeable at 5,000+ objects with VSync off.
# Reflection
Previous assignments always came with starter code, but this one required designing framebuffer management, depth/blend state transitions, and the post-processing chaining structure entirely from scratch, so the harder question was less "what to build" and more "how to keep the state from breaking." In particular, it took time to understand why the opaque pass writes depth with glDepthMask(GL_TRUE), while the transparent pass turns depth writes off with glDepthMask(GL_FALSE) but keeps depth testing on — transparent objects need to be occluded by already-drawn opaque objects, but must not corrupt the depth buffer against each other. Resolving an MSAA framebuffer into a regular texture (glBlitFramebuffer), and chaining one post-processing effect's output into the next effect's input through a ping-pong framebuffer structure, were both concepts I was handling for the first time and made debugging tricky — but isolating each stage and displaying its texture directly on screen to verify it made the whole pipeline's flow clear.
# Demo
Live WebAssembly build — interact with it directly in the frame above.