OpenGL Graphics Engine
A forward renderer written directly on the OpenGL API, without an engine underneath. It compiles its own shaders, manages its own buffers and framebuffers, and loads scenes from Lua scripts.
My role
Solo project. I wrote the renderer, the shaders, and the editor tools: about 2,300 lines of C++ and three GLSL shaders. Assimp, GLM, SDL3, GLEW, Lua and Dear ImGui are third-party libraries. Everything built on top of them is mine.
How it works
- Blinn-Phong lighting in the fragment shader, with separate ambient, diffuse and specular terms, for up to 8 lights.
- A chrome reflection sphere. Every frame the engine renders the scene six times from the sphere's position, once per cube face, into a 256x256 cubemap. The sphere samples that cubemap, so it reflects the scene as it is right now, moving objects included.
- Fresnel on the reflections: the sphere gets more reflective at glancing angles, the way real metal does.
- Model loading through Assimp, with mesh data uploaded into vertex buffers.
- Scenes are defined in Lua, so I can change what loads without rebuilding the engine.
- An ImGui panel hooked up to the shader values, so I can tune lights, materials and reflections while it runs instead of recompiling.
Trade-offs
- Every object gets its own draw call. There's no batching or instancing yet, so CPU cost goes up with the number of objects. It was still cheap into the thousands of objects in my tests, but it's the first thing I'd fix.
- Forward rendering kept the pipeline simple, but every light costs work on every pixel, so I capped the shader at 8 lights.
- The reflection sphere draws the whole scene six extra times per frame. That's expensive, so the cubemap is only 256x256, which is why the reflections look a bit soft. It also captures from the sphere's center, so reflections of objects right next to it are slightly off.
What I measured
Release build at 2560x1440 on an RTX 4080 SUPER. I measured GPU time with OpenGL timer queries (GL_TIME_ELAPSED), reading each result one frame later so the CPU never has to sit and wait for it. To make the scene heavier I spawned more copies of the models from the Lua script. The triangle count is everything the renderer drew that frame, so with the reflection sphere on it includes the sphere's six cubemap passes. GPU time went up roughly in step with the triangle count. I don't quote a frame rate because vsync was on with a 239 Hz monitor, so frame time mostly measured waiting for the display.