A voxel world built in C++ and OpenGL. The terrain streams 16×256×16 voxel chunks asynchronously across worker threads, with multi-biome procedural generation, a day/night cycle, animated water with reflection and refraction framebuffers, PCF shadow mapping, and exponential distance fog.
Most of my work was on systems and performance: the chunk VBO pipeline that culls invisible faces before upload, the multithreaded terrain loader with lock-free streaming, and the atomic flag that prevents race conditions during GPU upload. The visual features I built are PCF soft shadows and distance fog. TeammatesJackie Guan and Avi Serebrenik built procedural terrain, texturing, water, cave systems, player physics, and the day/night sky.
Each frame runs three pre-passes before any geometry reaches the screen: a shadow depth pass rendering the scene from the sun's orthographic perspective into a 4096×4096 depth-only FBO, and two water FBOs (reflection and refraction) that the water surface samples in the main pass. The main pass draws opaque chunks, then transparent chunks, then the water surface, instanced grass, and the sky. A post-process overlay applies any underwater or lava tint and draws the crosshair.
Generating terrain on the main thread means every new chunk blocks the frame. The fix is a custom ThreadPool class using C++11 std::packaged_task with perfect forwarding to dispatch terrain generation across worker threads. GPU uploads (sendOpaqueDataToGpu()) are serialized back on the main thread to avoid OpenGL context contention, since OpenGL contexts are bound per thread.
One race condition I ran into involved chunks whose CPU buffer was already being written when the draw loop tried to upload it. Wrapping the flag in a std::atomic<bool> (cpuBufferNeedsUpdate) solved this and also lets the main thread skip the O(65k) hull rebuild for chunks that haven't changed each frame.
Naively uploading every face of every block in a chunk would push millions of triangles to the GPU for geometry the player can never see. Each chunk's VBO only includes faces adjacent to transparent or empty blocks. Neighbor chunks are linked via unordered_map<Direction, Chunk*> for O(1) cross-chunk lookups during face culling, so a block at the boundary of one chunk can check the block on the other side in constant time.
Opaque and transparent geometry are packed into separate VBOs and drawn in separate passes. Drawing opaque first, then transparent, gives correct alpha blending for water and lava without a sort of the geometry.
Voxel scenes with hard-edged cube faces are unforgiving for shadow mapping: the same depth-precision problem that causes “shadow acne” (false self-shadowing where a lit face incorrectly shadows itself) shows up as dark bands across any steep face. A flat bias large enough to fix steep faces would push flat-top shadows too far, causing “peter panning.”
The solution is a slope-based bias that scales with face steepness:max(0.001 × (1 − dot(N, L)), 0), where N is the surface normal and L is the light direction. Steep faces get more offset; flat tops stay sharp. The depth pass renders the scene from the sun's orthographic perspective into a depth-only FBO (framebuffer object, an off-screen render target), and the main pass compares each fragment's sun-space depth against it to determine shadowing. A 5×5 PCF (percentage closer filtering) kernel with 25 samples softens shadow edges, and smoothstep attenuation combined with dot(sunDir, up) horizon fading prevent hard cutoffs at the shadow map boundary and at day/night transitions.
Without fog, chunks streaming in at the edge of the view radius pop in abruptly. The first approach I tried was pure exponential fog based on Inigo Quilez's colored fog model, which thickens smoothly with distance. That alone wasn't enough: terrain at 300 units was still visible through thin fog, so pop-in was still apparent.
Adding a secondary alpha attenuation pass over the [300, 325] unit range fades geometry fully transparent at the streaming boundary. An artifact appeared where steep block faces turned visibly darker at the fade zone boundary, so I tightened the attenuation slope to minimize the blending distance. The result hides chunk pop-in without LOD and keeps the horizon clean as new chunks stream in.