Vulkan Real-Time Renderer

A custom real-time renderer built from scratch in C++ using Vulkan. Features scene loading, PBR materials, environment lighting, dynamic lights, shadow mapping, and mirror reflections.

VulkanC++GLSL / SPIR-VPBRIBL
Vulkan Real-Time Renderer - PBR scene

Table of Contents

S72 Scene Loading & GPU Mesh Packing

The base of the renderer is a custom S72 scene-loading path. I parse the scene graph, walk the root nodes, and turn mesh data into one packed GPU vertex buffer. Instead of keeping each mesh as a separate upload, I store every renderable mesh range in a single packed buffer and keep a lookup from S72 mesh pointers to vertex ranges.

This made the rendering loop much simpler: each scene node becomes an object instance with a transform, a material reference, and a range into the shared vertex buffer. It also helped when I added multiple material pipelines later, because the mesh storage stayed the same while the draw path could branch between Lambertian, PBR, mirror, and other material types.

Full S72 scene rendered from the default scene camera

Full S72 scene rendered from the default scene camera

Scene Cameras, Animation, and Debug Controls

For the renderer to feel like a real scene viewer rather than a single hardcoded camera, the application supports scene cameras from the S72 file, a user orbit camera, and a debug camera. I can cycle camera modes, switch scene cameras, pause/restart animation, step animation frame-by-frame, and toggle culling/debug visualization controls.

This was useful for development because I could inspect the same scene from the authored camera and from a free camera without changing the scene file. The debug camera was especially helpful for checking culling and shadow-map alignment.

Debug camera view showing frustum culling

Debug camera for inspecting culling and scene structure

Environment Background & Tone Mapping

The environment background pass reconstructs a world-space view direction from screen-space coordinates, samples an environment cubemap, applies exposure in stops, and then applies a selectable tone mapper.

This gives the renderer a proper sky/environment instead of a flat clear color. It also became the foundation for image-based lighting: the same authored environment asset can drive both the visible background and the lighting response on materials.

Lambertian Materials, Textures, and Normal Mapping

The standard object pipeline supports textured Lambertian shading with normal maps. The vertex shader outputs world-space position, normal, UV, and tangent data. In the fragment shader, I build a TBN matrix, sample the normal map, flip the normal-map Y channel for the Vulkan texture convention I am using, and transform the tangent-space normal into world space.

This was one of the first places where the renderer started to look less like a basic prototype and more like an actual material system. Normal mapping made a big difference because even simple meshes could react to light and environment detail without extra geometry.

Physically Based Rendering Pipeline

The PBR pipeline is separate from the simpler Lambertian pipeline. It handles albedo, normal, roughness, and metalness textures, builds a world-space TBN basis, and evaluates a GGX-style microfacet BRDF.

The shader combines:

  • Diffuse and specular direct lighting
  • Metallic/roughness material response
  • Fresnel-Schlick terms
  • GGX normal distribution
  • Smith geometry masking
  • Environment diffuse irradiance
  • Prefiltered specular environment lighting
  • BRDF integration LUT

This made rough materials, metallic materials, and glossy materials behave differently under the same lighting setup instead of just changing texture color.

Image-Based Lighting Resources

For environment lighting, I load multiple derived cubemap resources from the active environment:

  • A Lambertian irradiance cubemap for diffuse IBL
  • A GGX prefiltered cubemap with mip levels for specular IBL
  • A BRDF LUT for split-sum specular lighting

The renderer also has fallback resources so the descriptor sets remain valid even if a scene does not provide the full environment setup. This was important in Vulkan because descriptor bindings need to be valid and predictable; I could not rely on the shader simply ignoring missing textures.

Runtime Light System

The runtime light extraction path walks the S72 scene graph node hierarchy, composes world transforms, and converts authored S72 lights into a GPU-friendly light buffer.

The light system supports:

Sun lights Sphere lights Spot lights Light color/tint Radius or angular size Power/strength Distance limits Spotlight field of view Spotlight blend/penumbra controls

This let the same render scene contain both global environment lighting and authored local lights.

Shadow Mapping for Spot Lights

The shadow system focuses on spot lights that request shadows. For each shadow-casting spot light, I create a depth image, image view, framebuffer, and sampler. I render the scene from the light's point of view into the shadow map, then sample that map during the lighting pass.

In the shader, shadowing is a light-space depth comparison. I transform the current world-space fragment into the spotlight clip space, convert that into shadow UVs, apply depth bias, and compare against the stored depth map.

I also experimented with softer shadows using a PCSS-style approach:

  • Blocker search
  • Penumbra estimate
  • Variable-radius PCF filtering

This made spot shadows softer and more plausible than a single hard depth comparison.

Mirror Materials

I also added a mirror material path. Mirror objects use their own pipeline and reuse the environment cubemap. The fragment shader reflects the view vector around the surface normal and samples the environment cubemap in that reflected direction.

This is not full planar reflection; it is an environment reflection material. But it was a useful stepping stone because it tested the environment map, per-material pipeline selection, camera position push constants, and tone mapping in a focused way.

Mirror object reflecting the cubemap environment

Mirror material reflecting the environment cubemap

Vulkan Pipeline / Descriptor Organization

A big part of the project was learning how to organize Vulkan state cleanly. The renderer has separate pipeline objects for background, lines/debug drawing, Lambertian objects, PBR objects, mirrors, and shadows.

The important descriptor sets are organized around:

  • World/environment data
  • Per-object transform storage buffers
  • Material textures
  • IBL resources
  • Runtime light buffers
  • Shadow maps

The hardest part was keeping the CPU-side structs, GLSL layouts, descriptor set layouts, and pipeline layouts in agreement. I used explicit structs and size checks where possible because Vulkan will not hide layout mistakes for you.

What I Learned

This project started as a scene viewer and grew into a small rendering engine. The most valuable part was connecting every stage myself:

Scene parsingGPU buffer packingCamera transformsEnvironment assetsMaterial classificationDescriptor allocationPBR shadingDynamic lightsShadow maps

Compared to working inside a higher-level engine, Vulkan made every dependency explicit: render passes, image layouts, descriptor sets, shader inputs, GPU buffers, synchronization, and pipeline state all had to line up correctly.

A more detailed technical walkthrough is planned for GitHub in the future.

Gallery

PBR materials with varying roughness

PBR Materials

Mirror reflections with environment mapping

Mirror Reflections

Frustum culling visualization

Frustum Culling

Instanced rendering wave visualization

Instanced Wave

Massive instanced grid rendering

Instanced Grid

Math animated grid ripple

Math Animated Grid

Tech Stack

C++

Core language

Vulkan API

Graphics API

GLSL / SPIR-V

Shaders

PBR

Material model

Credits

  • S72 scene format and course framework provided by the real-time rendering class.
  • Environment map assets and generated IBL resources are used as renderer test content.
  • Renderer implementation, material pipelines, light extraction, shadow pass, and shader work by me.