3D Scene Pipeline
Descriptor Set Layout
The 3D pipeline uses two descriptor sets:
Set 0 (per-frame, shared across all entities):
- Binding 0:
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER—UniformBufferObject(view/proj matrices), vertex stage - Binding 1:
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER—LightUBO(camera pos + up to 8 lights), fragment stage
Set 1 (per-material):
- Binding 0:
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER— base color texture, fragment stage
Graphics Pipeline State
Created in createGraphicsPipeline():
- Vertex input:
Vertexbinding (pos, normal, color, uv) — 4 attribute descriptions - Topology:
TRIANGLE_LIST - Rasterizer: fill mode, 1.0 line width,
BACKculling,COUNTER_CLOCKWISEfront face - Depth/stencil: depth test ON, depth write ON, compare op
LESS - Color blend: blending OFF (opaque), all RGBA write mask
- Multisampling:
SAMPLE_COUNT_1_BIT(no MSAA) - Dynamic state: viewport + scissor (set per frame in
recordCommandBuffer)
Push Constants
A 64-byte PushConstantData (one mat4 model) is pushed per entity via vkCmdPushConstants. This carries the entity's world transform, avoiding per-entity UBO updates.
Pipeline Layout
Two descriptor set layouts + one push constant range:
Vertex Shader (shader.vert)
Key points:
- Normal transform:
mat3(transpose(inverse(model)))handles non-uniform scaling correctly - World position is passed to fragment shader for per-fragment lighting
- Projection already has Y-flip applied on CPU side (
proj[1][1] *= -1)
Fragment Shader (shader.frag)
The fragment shader implements Blinn-Phong shading with up to 8 dynamic lights.
The calcLight() function handles all three light types:
- Directional: no attenuation, uses
normalize(-direction)as light direction - Point: distance-based quadratic attenuation
(1 - d^2/r^2)^2, position-relative direction - Spot: same as point + cone falloff
clamp((theta - outerCone) / (innerCone - outerCone))
Lighting equation per light:
Final color = ambient + sum(baseColor * calcLight(light[i])) where ambient = baseColor * ambientIntensity.
If numLights == 0, falls back to a hardcoded directional light at (1, 1, 1) with 0.15 ambient.
Texture sampling: texture(baseColorTex, fragUV).rgb * fragColor — the texture color is multiplied by the vertex color.
Debug Wireframe Pipeline
A second pipeline (debugPipeline_) is created at the end of createGraphicsPipeline() by modifying the rasterizer and depth-stencil state:
The wireframe pipeline shares the same shaders, pipeline layout, vertex format, and vertex/index buffers as the 3D pipeline. It requires the fillModeNonSolid device feature, which is enabled during logical device creation.
Debug entities are stored in a separate debugEntities_ vector and rendered between the main 3D entities and the UI overlay in recordCommandBuffer().
Adding a new uniform: Add the field to the UBO struct in renderer.h (with correct alignas), update the matching GLSL uniform block, and ensure the buffer size matches.
Modifying lighting: Edit calcLight() in shader.frag. The specular exponent (32.0) is hardcoded — to make it configurable, add it to GpuLight or as a material property.
Adding a vertex attribute: Add to Vertex struct in renderer.h, add a VkVertexInputAttributeDescription, increment the array size, declare in shader.vert, and pass through to shader.frag if needed.