back to projects
renderer.cppC++ · OpenGL · GLSL

OpenGL Graphics Engine

A forward renderer built directly on the OpenGL API, without an engine underneath it. Handles its own shader compilation, buffer management, framebuffer setup, and scene loading.

Blinn-PhongFresnelCubemapFBO / RTTAssimpImGui
[01]

my role

Solo project. I wrote the renderer, the shader pipeline, and the editor tooling: roughly 2,300 lines of C++ plus three GLSL shaders. Assimp, GLM, SDL3, GLEW and Dear ImGui are third-party libraries; everything built on top of them is mine.

[02]

approach

  • 01Blinn-Phong shading with separate ambient, diffuse and specular terms, computed per-fragment in GLSL.
  • 02Fresnel-weighted environment reflections: reflection strength scales with viewing angle via a power curve, so surfaces get more reflective at grazing angles rather than reflecting uniformly.
  • 03Cubemap sampling for the reflected environment, using the reflection vector about the surface normal.
  • 04Framebuffer objects with attached renderbuffers for render-to-texture, enabling off-screen passes.
  • 05Model loading through Assimp, with vertex data uploaded into VAO/VBO pairs and mipmapped textures.
  • 06An ImGui panel wired to live uniforms, so material, light and reflection parameters can be tuned while the renderer runs instead of requiring a recompile.
[03]

trade-offs

  • 01Every instance issues its own draw call. There is no batching or instanced rendering, so submission cost grows linearly with object count rather than with unique geometry. Measured at roughly 0.42 microseconds per draw call, that stays cheap into the thousands, but it is the ceiling this design would hit first.
  • 02Forward rendering keeps the pipeline simple and makes transparency straightforward, but lighting cost scales with lights multiplied by fragments. The shader is capped at 8 lights for that reason.
  • 03Reflections sample a cubemap rather than tracing the real scene, so they are convincing for environment reflection and wrong for reflecting nearby dynamic objects.
[04]

measured results

22 draws · 413K tris0.14 msGPU
372 draws · 17.5M tris1.54 msGPU
1,072 draws · 51.8M tris3.03 msGPU
2,122 draws · 103M tris8.06 msGPU
4,222 draws · 206M tris14.13 msGPU

Release build at native 2560x1440 on an RTX 4080 SUPER, measured with double-buffered GL_TIME_ELAPSED queries so the CPU never stalls waiting on the GPU. Scene complexity was scaled by spawning additional mesh instances from the Lua scene definition. Two linear relationships hold across the range: CPU draw submission costs roughly 0.42 microseconds per draw call, and the GPU sustains roughly 14 million triangles per millisecond. The renderer is CPU-submission-bound at low object counts and GPU-bound from about 50 instances upward. Frame time is not quoted here because the swap interval is set to 1 and the display runs at 239 Hz, so wall-clock frame time reports the vsync wait rather than the cost of the work.