This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
The module reaches up into its consumer for exactly two things, both declared by the module
and defined by the application. The linker enforces both, so nothing can be forgotten silently.
(This replaced an earlier arrangement in which module internals directly #included an
AppConstants.h that every consumer had to provide.)
1. AppVulkanConfig() — Setup/VulkanConfig.h. Fixed configuration, handed over once:
app name/version, company/project (which resolve the per-user storage path), window title and
default size, clear color, stereo flag, log file name, exe path. Every field has a default, so
a definition can be as short as setting appName and windowTitle.
2. AppStoredSettings() — Setup/iAppSettings.h. The app's persistent settings, as a
live object the module calls back into: window geometry is read at window creation and written
back on every move/resize/fullscreen change, then committed via Save(). An existing settings
class simply inherits iAppSettings. Returning nullptr is legitimate and means "this app
persists nothing" — windows then open at the configured default size and saving is skipped.
Both are functions, deliberately, not globals or values passed to a constructor: they are
called during static initialization (an app whose global settings object resolves its file path
in its constructor asks before main() runs), and a function with a function-local static
resolves correctly whenever first called. Two consequences worth knowing:
VulkanConfig::exePathis late-bound —argvisn't known at static-init time, so an app that snapshots its fields once must refresh that one field per call.AppStoredSettings()should returnnullptruntil the settings object is genuinely constructed. Both bundled examples gate on a zero-initializedboolset at the end of the constructor; without it, a virtual call through a not-yet-constructed global would crash.
See TestHarness/src/VulkanTester.cpp and TestHarness/src/AppSettings.cpp for the reference
implementations.
- CMake 3.16+
- Vulkan SDK
- SDL2 and SDL2_image libraries
- GLM (OpenGL Mathematics library)
macOS:
# Install via Homebrew
brew install vulkan-headers vulkan-loader molten-vk sdl2 sdl2_image glmWindows:
- Install Vulkan SDK from https://vulkan.lunarg.com/
- Use vcpkg for dependencies:
vcpkg install sdl2 sdl2-image glm vulkanLinux/Ubuntu:
sudo apt-get update
sudo apt-get install vulkan-tools libvulkan-dev vulkan-validationlayers-dev
sudo apt-get install libsdl2-dev libsdl2-image-dev libglm-dev
sudo apt-get install build-essential cmake pkg-configRaspberry Pi 5:
# Same as Linux, plus ARM64-specific packages
sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnuAll Platforms:
cd TestHarness
mkdir -p build && cd build
cmake ..
cmake --build . -j$(nproc)Windows (Visual Studio):
cd TestHarness
mkdir build && cd build
cmake .. -G "Visual Studio 16 2019" -A x64
cmake --build . --config ReleaseThe built executable will be located at:
- macOS/Linux:
TestHarness/build/VulkanTester - Windows:
TestHarness/build/Release/VulkanTester.exe
TestHarness uses organized directory structure:
TestHarness/src/- Source files (.cpp)TestHarness/include/- Header files (.h)TestHarness/src/shaders/- SPIR-V compiled shaders (.spv)
- macOS: Uses MoltenVK for Vulkan support, requires portability extensions
- Windows: Direct Vulkan support, uses Win32 platform extensions
- Linux: Direct Vulkan support, uses XCB platform extensions
- Raspberry Pi 5: ARM64 optimizations enabled automatically
The build system automatically copies pre-compiled SPIR-V shaders from src/shaders/*.spv to the appropriate runtime directory during the build process.
Note: This module is typically used as a submodule by projects like HelloVulkanSDL which provide additional shader compilation infrastructure.
VulkanModule is a reusable foundation for Vulkan graphics projects, providing object-oriented encapsulation of Vulkan's initialization and setup requirements.
Coordinate System:
- The module defaults to standard Vulkan: right-handed,
+Zout of the screen, CCW front faces - Optional left-handed mode (
+Zinto the screen, forward) is opted into per application:target_compile_definitions(MyApp PRIVATE INVERT_Z_SETTING=true)
Assist/VulkanMath.hreads that asconstexpr bool INVERT_Z, defaulting tofalse- Scope:
INVERT_Zhas exactly one effect — it reversesGraphicsPipeline's defaultVkFrontFace(GraphicsPipeline.cpp), because left-handed geometry winds opposite. It does not alter projection or view math; the module never callsglm::perspectiveLH_ZO()orglm::lookAtLH(). Applications build their own MVP matrices and choose handedness there. - Consequently an app that sets
INVERT_Z_SETTING=truemust also supply left-handed projection/view matrices itself — the setting only keeps culling consistent with them. - Consumers read
INVERT_Ztoo. Applications commonly branch on this same symbol as their coordinate-system contract — LevelEdit/TuneTrip keys projection (glm::perspectiveLH_ZO), OBJ winding order, Z-negation at model load, and camera math off it. So while its effect within this module is the singleVkFrontFaceflip, changing the effective value is engine-wide for such an app. Treat it as a coordinate-system contract, not a local toggle.
Winding Order Convention:
- Counter-clockwise (CCW) is the standard for front-facing triangles
- Pipeline configured with
VK_FRONT_FACE_COUNTER_CLOCKWISEby default - All geometry (manually-defined or procedurally-generated) must use CCW winding
- OBJ files follow CCW standard (Wavefront specification)
- See the
DEV NOTEat the end ofObjects/GraphicsPipeline.cppfor detailed rationale
Override for Direct3D/Unity Models:
- Use
FRONT_CLOCKWISEorMODELED_FOR_DIRECT3DCustomizer flags for CW-wound models - Under
INVERT_Zthe two flips cancel, back to counter-clockwise — a left-handed app and a D3D-wound model already agree. (⚠️ Fixed Aug 2026: a bitwise^on mismatched operands meant they never cancelled, so this override was inert in exactly the apps that need it.) - See "Rendering Customizations" section below for details
Objects/ - Encapsulates Vulkan subsystems in RAII fashion:
VulkanInstance- Manages VkInstanceGraphicsDevice- Manages VkDevice/VkPhysicalDevice selection and queuesSwapchain- Handles swapchain creation and recreation on resizeRenderPass,GraphicsPipeline,Framebuffers- Core rendering pipelineWindowSurface- Platform surface abstractionSyncObjects- Semaphores and fences for GPU/CPU synchronization
Setup/ - Orchestrates initialization:
VulkanSetup- Main initialization class that creates all Vulkan objects in dependency orderVulkanConfigure- Configuration managementShader- Shader module loading
Adjunct/ - Higher-level abstractions:
Renderables/- Unified renderable system with pipeline batching optimizationRenderable- Single unified class for dynamic geometry (defaults toUPON_EACH_FRAMErecording)SecondaryRenderable- Optimized for static geometry using secondary command buffers recorded once at initializationRenderBatchManager- Pipeline batching system to minimize state changesiRenderableBase- Base for self-managed renderables (ImGui)iRenderable- Extends base with full pipeline/descriptor managementShaderCache- Shared shader management with reference counting to eliminate redundant shader loadingiRenderable::UpdateUniformBuffers()- Public method for uploading UBO data to GPU
VertexTypes/- Various vertex format definitionsTextureImage,UniformBuffer- Resource managementDynamicUniformBuffer- Efficient per-object uniform data using dynamic offsets for rendering thousands of objectsShadowing/- Complete shadow mapping infrastructureShadowMap- Shadow map image, render pass, framebuffer, and samplerShadowPass- Shadow pass command buffer recording and management
Platform/ - Platform abstraction layer:
OSAbstraction/PlatformSDL- SDL2 window/input handling (primary platform)OSAbstraction/MacOSFullScreen.mm- macOS native fullscreen detection/control (Obj-C++)FileSystem/- File I/O with conventions for shader/texture/model pathsImageHandling/- Image loading via SDL_image or STB
- RAII Resource Management: Objects manage their Vulkan resources through constructors/destructors
- Recreate Pattern: Objects support
Recreate()for handling window resize/minimize events - Dependency Order: VulkanSetup instantiates objects in strict dependency order
- Platform Abstraction: iPlatform interface allows different windowing systems (SDL2, GLFW, XCB)
- Resource Sharing: ShaderCache enables sharing ShaderModules across renderables to eliminate redundant loads
- Dynamic Uniform Buffers: Support for VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC with per-object offsets for efficient multi-object rendering
- Pipeline Batching: RenderBatchManager groups renderables by pass and pipeline, reducing state changes from O(N) to O(M)
- Pass-Based Rendering: Explicit render order (opaque → transparent → lines → self-managed) ensures correct depth sorting
- Vulkan SDK (headers and loader)
- SDL2 and SDL2_image
- GLM (math library)
- Optional: Dear ImGui for debug UI (stubbed if not used)
The TestHarness directory contains a minimal test application demonstrating module usage. The main class VulkanTester shows the typical initialization pattern:
- Create platform (SDL)
- Instantiate VulkanSetup with platform and configuration
- Store references to key objects for rendering loop
Dynamic Uniform Buffers enable efficient rendering of large numbers of objects by using a single shared buffer with dynamic offsets instead of creating individual UniformBuffer objects per renderable.
Usage:
// 1. Create DynamicUniformBuffer (typically in application initialization)
const uint32_t MAX_OBJECTS = 1000;
const uint32_t FRAMES_IN_FLIGHT = swapchain.getNumImages();
DynamicUniformBuffer* dynamicUBO = new DynamicUniformBuffer(MAX_OBJECTS, FRAMES_IN_FLIGHT, device);
// 2. Configure drawable to use dynamic UBO
drawable->pUBOs = {
camera.uboMVP, // Binding 0: Static camera matrices
UBO(dynamicUBO) // Binding 1: Dynamic per-object transforms
};
// 3. Set dynamic offset on renderable
Renderable renderable(*drawable, vulkan, platform);
renderable.hasDynamicOffset = true;
renderable.dynamicOffset = dynamicUBO->getDynamicOffset(objectIndex);
// 4. Update per-object transforms each frame
dynamicUBO->updateObjectTransform(frameIndex, objectIndex, modelMatrix);Benefits:
- Renders thousands of objects with minimal overhead
- Single buffer allocation instead of hundreds/thousands of individual buffers
- Reduced descriptor set updates
- Lower memory fragmentation
- Works seamlessly with pipeline batching optimization
Implementation Details:
- Automatically adds
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMICto descriptor pool RenderablecheckshasDynamicOffsetand passes offset array tovkCmdBindDescriptorSets()- Update transforms before recording command buffers each frame
ShaderCache eliminates redundant shader loading when multiple renderables use the same shaders, crucial for scenes with thousands of objects.
Usage:
// 1. Create ShaderCache (typically in application initialization)
ShaderCache* shaderCache = new ShaderCache(vulkan.device);
// 2. Get or create shared shaders for a drawable
ShaderModules* pSharedShaders = shaderCache->getOrCreate(drawable->shaders);
drawable->pSharedShaderModules = pSharedShaders;
shaderCache->addRef(pSharedShaders);
// 3. Create renderable (will use shared shaders)
Renderable renderable(*drawable, vulkan, platform);
// 4. ShaderCache handles cleanup automatically via reference counting
// When last renderable using a shader set is destroyed, shaders are freedBenefits:
- Shaders loaded once and shared across all renderables using the same shader set
- Automatic reference counting prevents premature deletion
- Reduced I/O and memory usage
- Faster scene initialization
Implementation Details:
- Shaders are cached by a key generated from shader file names and types
iRenderablecheckspSharedShaderModulesand uses it if provided, otherwise creates its ownownsShaderModulesflag prevents double-deletion- Reference counting ensures shaders persist until last user is destroyed
VulkanModule supports secondary command buffers for optimal rendering of static geometry that never changes. This provides significant performance benefits by recording draw commands once at initialization time instead of every frame.
Core Component:
SecondaryRenderable(Adjunct/Renderables/SecondaryRenderable.{h,cpp}) - Renderable using secondary command buffers
When to Use:
Use SecondaryRenderable for:
- Static geometry that never changes (skyboxes, static environment elements)
- Geometry recorded once and reused every frame
- Objects where command buffer recording CPU cost is significant
Use Renderable for:
- Dynamic geometry that changes frequently
- Objects requiring per-frame updates
- Most scene objects (default choice)
Usage Example:
// Define static geometry (e.g., skybox)
DrawableSpecifier* drawable = new DrawableSpecifier(
skyboxMesh,
"Skybox",
"skybox" // Render pass name
);
drawable->shaders = {
{ VERTEX, "skybox-vert.spv" },
{ FRAGMENT, "skybox-frag.spv" }
};
// Create SecondaryRenderable (records commands once at init)
SecondaryRenderable* skybox = new SecondaryRenderable(*drawable, vulkan, platform);
// No per-frame recording needed - command buffer reused automaticallyBenefits:
- Zero CPU overhead per frame - command buffers recorded once, reused forever
- Optimal for static geometry - skyboxes, static backgrounds, UI elements
- Automatic reuse - no manual management required
- Full Recreate() support - handles window resize/display changes automatically
Implementation Details:
- Inherits from
iRenderablefor compatibility with existing infrastructure - Records secondary command buffers at initialization time
- Command buffers marked as
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT - Automatically re-recorded during
Recreate()for window resize events - Works seamlessly with pipeline batching and render pass ordering
Performance Characteristics:
- First frame: Slight overhead for secondary command buffer recording
- Subsequent frames: Near-zero CPU cost (only
vkCmdExecuteCommands()call) - Best for: Geometry with 100+ draw calls that never changes
- Trade-off: Slightly higher initial memory usage vs. primary command buffers
Assist/GameClock.h provides frame timing utilities for time-dependent rendering and game logic.
Features:
- Delta time calculation: Precise per-frame time for smooth animations
- Elapsed time tracking: Total time since application start
- FPS counter: Automatic calculation and tracking, updated every second
Usage:
GameClock gameClock;
// In main loop
gameClock.BeginNewFrame(); // Call once per frame
float deltaTime = gameClock.deltaSeconds(); // Time since last frame
float elapsed = gameClock.secondsElapsed(); // Total elapsed time
int fps = gameClock.getFPS(); // Current FPS (0 until first second)
bool fpsUpdated = gameClock.wasFPSUpdated(); // True if FPS recalculated this frameFPS Counter Details:
- Calculates FPS every second based on actual frame count
- Returns 0 until first full second has elapsed
wasFPSUpdated()allows checking when to update UI/logs- Useful for performance diagnostics and monitoring
PlatformSDL Extensions:
SetWindowTitle()- Update window title (useful for FPS display)- Display information logging at startup:
- Monitor index and name
- Resolution and refresh rate
- Performance hints for built-in vs external displays
IsEventQUIT()- Application quit detection- Shift+ESC - Quit application (requires modifier to prevent accidental quits)
- SDL_QUIT - Window manager quit event
- Window close button - User closes window
- Keyboard handling:
- ESC (unshifted) - Exits fullscreen back to windowed mode
- Shift+ESC - Quits application
macOS-Specific:
- MoltenVK configuration for maximum performance
- vsync disabled at Metal layer when possible
- Async queue submits enabled
- Note: macOS compositor may still enforce vsync on external displays
PlatformSDL automatically saves and restores window position, size, and fullscreen state across sessions. The app's AppSettings class provides the persistence backend.
How it works:
PlatformSDL::recordWindowGeometry()is called automatically on window move/resize events- Saves to
AppConstants.Settings(the app'sAppSettingsinstance) and callsSettings.Save() - On startup,
PlatformSDL::createVulkanCompatibleWindow()reads saved values fromAppSettingsto position/size the window, and validates against all connected displays - Window position is validated across all monitors; falls back to centered on primary if the saved position is off-screen
iPlatform public member:
bool isFullScreen- Tracks current fullscreen state (updated automatically)
What gets saved (fields required in the app's AppSettings class):
int startingWindowWidth; // Window width (windowed mode)
int startingWindowHeight; // Window height (windowed mode)
int startingWindowX; // Window X position (windowed mode)
int startingWindowY; // Window Y position (windowed mode)
bool isFullScreen = false; // Whether to restore fullscreen on next launchWhen fullscreen is active, only isFullScreen is updated -- the windowed dimensions are preserved so the correct window size is restored when leaving fullscreen.
PlatformSDL provides borderless fullscreen (SDL_WINDOW_FULLSCREEN_DESKTOP) with automatic save/restore and macOS native fullscreen interception.
Behavior:
| Action | Result |
|---|---|
| Green button (macOS) | Native fullscreen detected, automatically switched to borderless fullscreen |
| ESC (unshifted) | Exits fullscreen back to windowed mode |
| Shift+ESC | Quits application (from any state) |
| Quit while fullscreen | Saves isFullScreen: true + last windowed geometry |
| Launch with saved fullscreen | Borderless fullscreen applied on first idle poll |
macOS native fullscreen detection (Platform/OSAbstraction/MacOSFullScreen.mm):
SDL2 does not track macOS native fullscreen (green title-bar button) with its own flags. On notched MacBooks, the window size is identical in windowed-maximized and native fullscreen, making size-based detection impossible. The solution queries NSWindow styleMask & NSWindowStyleMaskFullScreen directly via SDL_GetWindowWMInfo.
When native fullscreen is detected, PlatformSDL:
- Exits native fullscreen via
[NSWindow toggleFullScreen:] - Applies borderless fullscreen (
SDL_WINDOW_FULLSCREEN_DESKTOP) on the next idle poll
This provides consistent behavior: ESC always exits fullscreen, no menu bar appears on mouse hover, and the state is properly saved/restored.
Deferred fullscreen restore:
SDL_SetWindowFullscreen() requires the macOS Cocoa run loop to be active, so it cannot be called during window creation. PlatformSDL sets a pendingFullScreen flag during createVulkanCompatibleWindow() and applies fullscreen on the first idle PollEvent() call (when the event queue is empty and the run loop is pumping).
CMake requirements for consuming projects:
# Enable Obj-C++ compilation (required for MacOSFullScreen.mm)
project(MyProject CXX OBJCXX)
# Add the .mm source file (Apple only)
if(APPLE)
list(APPEND Platform_OSAbstraction
"Vulkan/Platform/OSAbstraction/MacOSFullScreen.mm"
)
endif()
# Link the Cocoa framework (Apple only)
if(APPLE)
find_library(COCOA_FRAMEWORK Cocoa)
list(APPEND ADDITIONAL_LIBRARY_DEPENDENCIES ${COCOA_FRAMEWORK})
endif()
# Compile Obj-C++ with ARC — see "Objective-C memory management" below.
if(APPLE)
set_source_files_properties(
"Vulkan/Platform/OSAbstraction/MacOSFullScreen.mm"
PROPERTIES COMPILE_OPTIONS "-fobjc-arc"
)
endif()Build with ARC, and keep the source ARC-neutral anyway. Both halves matter:
1. Both build systems must agree. An Xcode target defaults to CLANG_ENABLE_OBJC_ARC = YES, while
CMake compiles Obj-C++ without ARC unless told otherwise. The same .mm compiled one way in Xcode and
the other in CMake broke the build once (Aug 2026, macOS_SetSustainedPerformance). Hence the
set_source_files_properties(... "-fobjc-arc") above — consuming projects should mirror it.
The non-ARC direction is the dangerous one, because it fails silently: a method returning an
autoreleased object (beginActivityWithOptions:reason:, most +arrayWith…/+stringWith…
constructors) stored in a static or long-lived variable dies at the next pool drain, leaving a dangling
pointer and a quietly inert feature — no compile error, no crash until much later. ARC cannot get this
wrong; hand-written retain/release can.
2. Write ARC-neutral source regardless. VulkanModule is consumed by multiple projects whose build settings we don't control, so memory management here should compile correctly either way:
static id<NSObject> s_token = nil; // __strong under ARC; hand-retained otherwise.
s_token = [[NSProcessInfo processInfo] beginActivityWithOptions: opts reason: why];
#if !__has_feature(objc_arc)
[s_token retain]; // ARC does this implicitly via the strong static.
#endif
...
#if !__has_feature(objc_arc)
[s_token release];
#endif
s_token = nil; // Under ARC, this assignment is what releases it.Runtime behavior is identical in both modes, so there is no divergence to reason about — only the spelling differs. ARC governs Objective-C object pointers only; C++ code is entirely unaffected, so enabling it costs a mixed C++/Obj-C codebase nothing.
AppSettings requirements:
Geometry persistence rides on the iAppSettings interface described under "What a Consuming
Application Must Supply" — the module reads a WindowGeometry at startup and writes one back on
every move/resize, then calls Save(). The app decides how (and whether) that is stored; if
AppStoredSettings() returns nullptr, the module simply skips saving.
To restore fullscreen across sessions, the app must actually persist WindowGeometry::isFullScreen
alongside the position and size. It's easy to miss: an app can carry an isFullScreen member,
have the module set it correctly, and still never write it to storage — in which case windows
always reopen windowed, with no error to indicate why.
VulkanModule provides complete shadow mapping infrastructure for realistic shadow rendering. The system supports both orthographic (directional/sun) and perspective (point light) projections, with flexible camera orientation modes and automatic resolution scaling.
Core Components:
ShadowSystem (Adjunct/Shadowing/ShadowSystem.h) - Primary Interface
- Unified management of shadow mapping resources and rendering
- Zero VRAM allocation when
SHADOW_TECHNIQUE_NONEis specified - Encapsulates multiple
ShadowMapinstances (one per swapchain image) andShadowPass - Single, clean API for initialization and per-frame recording
- Automatic cross-frame synchronization support
- Methods:
recordFrame()- Returns true if shadows were recorded, false if disabledisEnabled()- Check if shadow system is activegetPerFrameDescriptorInfo()- Get shadow map descriptors for all framesgetRenderPass(),getExtent(),getCommandBuffer()- Resource accessors
ShadowMap (Adjunct/Shadowing/ShadowMap.h)
- Encapsulates all shadow map resources:
- Depth-only image with configurable resolution (default 2048x2048)
- Image view, render pass, and framebuffer
- Depth sampler with PCF (Percentage Closer Filtering)
- Supports
Recreate()for window resize handling RecreateWithNewResolution()for dynamic resolution changes- RAII resource management
ShadowPass (Adjunct/Shadowing/ShadowPass.h)
- Manages shadow pass command buffer recording
- Depth-only rendering from light's perspective
- Proper layout transitions and synchronization
- Records all shadow-casting renderables
- Critical: Uses actual shadow map dimensions via
shadowMap.getWidth()/getHeight()(not hardcoded constants)
ShadowProjection (Adjunct/Shadowing/ShadowProjection.h)
- Reusable utility for calculating light-space projection matrices
- Projection modes:
SHADOW_ORTHOGRAPHIC: Parallel light rays (sun/directional)SHADOW_PERSPECTIVE: Radial light rays (point light)
- Camera orientation modes (
ShadowCameraMode):SHADOW_CAMERA_CUSTOM_DIRECTION: Uses custom direction vector (e.g. directional/sun light, spotlights). Default direction is -Y (straight down).SHADOW_CAMERA_LOOK_AT_TARGET: Looks from light position toward a specified target point. Defaults to origin for backward compatibility with Scenes.
- Dynamic resolution calculation:
calculateRecommendedResolution()scales shadow map based on FOV and camera mode - Handles gimbal lock avoidance automatically
- Configurable ortho size, FOV, near/far planes
Usage Example:
#include "Shadowing/ShadowMap.h"
#include "Shadowing/ShadowPass.h"
#include "Shadowing/ShadowProjection.h"
// 1. Calculate optimal shadow map resolution based on FOV and camera mode
float shadowFOV = glm::radians(170.0f); // Wide FOV for maximum coverage
ShadowCameraMode cameraMode = SHADOW_CAMERA_CUSTOM_DIRECTION;
uint32_t optimalResolution = ShadowProjection::calculateRecommendedResolution(
shadowFOV, cameraMode, SHADOW_PERSPECTIVE);
// Returns 4096x4096 for 170° FOV with CUSTOM_DIRECTION mode
// 2. Create shadow map with dynamic resolution
ShadowMap* shadowMap = new ShadowMap(vulkan.device, commandPool,
optimalResolution, optimalResolution);
ShadowPass* shadowPass = new ShadowPass(vulkan, *shadowMap);
// 3. Each frame, calculate light-space matrix with camera mode
vec3 lightPos = light.getPosition();
vec3 target = vec3(0.0f, 0.0f, 0.0f); // Scene focus point (origin, or camera position for Trips)
float fov = glm::radians(170.0f);
float farPlane = 60.0f;
shadowUBO.lightSpaceMatrix = ShadowProjection::calculateLightSpaceMatrix(
lightPos, target, SHADOW_PERSPECTIVE,
SHADOW_CAMERA_LOOK_AT_TARGET, // Look toward target from light position
glm::vec3(0.0f, -1.0f, 0.0f), // customDirection (for CUSTOM_DIRECTION mode)
15.0f, // orthoSize (for ORTHOGRAPHIC)
fov, // Field of view
0.1f, // nearPlane
farPlane); // farPlane
// 4. Record shadow pass
shadowPass->recordShadowPass(shadowRenderables, frameIndex);
// 5. Shadow map sampler automatically bound at descriptor binding 4
// Main pass shaders sample shadow map for shadow calculationsConfiguration (VulkanConfigure.h):
// Shadow projection mode
enum ShadowProjectionMode {
SHADOW_ORTHOGRAPHIC, // Directional/sun light (parallel rays)
SHADOW_PERSPECTIVE // Point light source (radial rays)
};
// Shadow camera orientation mode
enum ShadowCameraMode {
SHADOW_CAMERA_CUSTOM_DIRECTION, // Uses custom direction vector (default -Y)
SHADOW_CAMERA_LOOK_AT_TARGET // Looks from light toward target point (default origin)
};
// Quality/performance tunables (defaults)
const uint32_t SHADOW_MAP_WIDTH = 2048; // Default resolution (scales with FOV)
const uint32_t SHADOW_MAP_HEIGHT = 2048;
const int PCF_KERNEL_RADIUS = 1; // Shadow softness (1=3x3, 2=5x5, 3=7x7)
const float SHADOW_BIAS = 0.0015f; // Prevents shadow acneShadow Projection Modes:
-
SHADOW_ORTHOGRAPHIC:
- Parallel light rays across entire scene
- Uniform shadow quality regardless of distance
- Best for outdoor scenes with sun/moon lighting
- Uses
glm::ortho()projection
-
SHADOW_PERSPECTIVE:
- Radial light rays from light position
- Matches Phong lighting for accurate shadows
- Shadow detail decreases with distance
- Best for indoor scenes with point lights
- Uses
glm::perspective()with configurable FOV (default 90°, up to 170° for wide coverage)
Shadow Camera Orientation Modes:
-
SHADOW_CAMERA_CUSTOM_DIRECTION:
- Camera uses custom direction vector (default -Y, i.e. straight down)
- Best for directional/sun lighting with orthographic projection
- Direction stays constant regardless of scene movement (parallel rays)
- Specify direction via
customDirectionparameter
-
SHADOW_CAMERA_LOOK_AT_TARGET:
- Camera looks from light position toward a specified target point
- Target defaults to origin for backward compatibility with Scenes
- For Trips, pass the scene focus point (e.g. camera XZ position) so shadows track the action
- Best for point-light shadows with perspective projection
Dynamic Resolution Scaling:
ShadowProjection::calculateRecommendedResolution() automatically selects optimal shadow map resolution based on:
- FOV: Wider FOV requires higher resolution to prevent pixelation
- Camera mode: CUSTOM_DIRECTION mode benefits from higher resolution at wide FOV
- Projection mode: Perspective typically needs higher resolution than orthographic
Resolution guidelines (for CUSTOM_DIRECTION):
- FOV 150°+: 4096x4096 (4K shadow map)
- FOV 120-150°: 3072x3072 (3K shadow map)
- FOV 90-120°: 2048x2048 (2K shadow map, default)
- FOV < 90°: 2048x2048
Quality Tuning:
Shadow Map Resolution:
- 1024x1024: Fast, softer shadows (low-end hardware)
- 2048x2048: Balanced (default for narrow FOV)
- 3072x3072: High quality (wide FOV 120-150°)
- 4096x4096: Very high quality (wide FOV 150°+, high-end hardware)
- Use
calculateRecommendedResolution()for automatic selection - Override via
ShadowMapconstructor orRecreateWithNewResolution()
PCF Kernel Radius:
- 1 (3x3): 9 samples, fast, sharper edges
- 2 (5x5): 25 samples, balanced
- 3 (7x7): 49 samples, slow, very soft shadows
Shadow Bias:
- Two-layer approach recommended: hardware depth bias on shadow pass pipeline (
DEPTH_BIAScustomizer flag) plus small fragment shader bias - Hardware bias (constant + slope factors) prevents self-shadowing at the GPU level
- Fragment shader bias catches residuals (0.0003–0.0005 typical)
- Too much total bias: "peter panning" (detached shadows)
FOV Selection:
- 90° or less: Standard, good quality with 2K shadow map
- 90-120°: Standard-wide, may need 2-3K shadow map
- 120-150°: Wide, requires 3K shadow map to prevent pixelation
- 150°+: Very wide, requires 4K shadow map for sharp shadows
UBO Bindings: Applications using shadow mapping must follow these descriptor bindings:
- Binding 0: Camera MVP matrices
- Binding 1: Dynamic UBO (per-object transforms)
- Binding 2: Lighting UBO
- Binding 3: Shadow UBO (light-space matrix)
- Binding 4: Shadow map sampler (texture)
Implementation Details:
- Uses
GLM_FORCE_DEPTH_ZERO_TO_ONEfor [0,1] depth range - Two-pass rendering: shadow pass → main pass
- PCF sampling in fragment shader for soft shadows
- Adaptive bias based on surface angle to light
- Shadows affect diffuse/specular only, not ambient
VulkanModule provides bitfield flags (Adjunct/Renderables/Customizer.h) for per-renderable customization of the graphics pipeline. Applications can extend this enum with additional flags.
Built-in Flags:
enum Customizer
{
NONE = 0,
WIREFRAME = 0b00000001, // VK_POLYGON_MODE_LINE instead of _FILL
SHOW_BACKFACES = 0b00000010, // VK_CULL_MODE_NONE instead of _BACK_BIT
FRONT_CLOCKWISE = 0b00000100, // not VK_FRONT_FACE_COUNTER_CLOCKWISE (Vulkan native)
MODELED_FOR_DIRECT3D = 0b00001000, // Model uses D3D/Unity conventions (vs OpenGL/Vulkan)
ALPHA_BLENDING = 0b00010000, // Enable alpha blending + disable depth writes
LINE_TOPOLOGY = 0b00100000 // VK_PRIMITIVE_TOPOLOGY_LINE_LIST
};How To Use:
DrawableSpecifier* drawable = object->createDrawable();
// Single flag
drawable->customize = SHOW_BACKFACES;
// Multiple flags (bitwise OR)
drawable->customize = static_cast<Customizer>(LINE_TOPOLOGY | SHOW_BACKFACES | ALPHA_BLENDING);
// Create renderable
Renderable renderable(*drawable, vulkan, platform);Flag Details:
-
WIREFRAME: Renders triangle edges as lines (debug/visualization)
- Sets
VK_POLYGON_MODE_LINEin rasterization state - Does NOT affect primitive topology (still triangle list)
- Sets
-
SHOW_BACKFACES: Disables backface culling
- Sets
VK_CULL_MODE_NONEin rasterization state - Essential for transparent geometry where both sides are visible
- Useful for line rendering to show away-facing edges
- Sets
-
FRONT_CLOCKWISE: Uses clockwise winding for front faces
- Sets
VK_FRONT_FACE_CLOCKWISE - Direct3D/Unity native (OpenGL uses counter-clockwise by default)
- Use
MODELED_FOR_DIRECT3Dfor models created with D3D or Unity conventions
- Sets
-
ALPHA_BLENDING: Enables alpha blending and disables depth writes
- Configures blend state for transparency (src_alpha, one_minus_src_alpha)
- Disables depth writes (allows see-through rendering)
- Required for billboards, particles, and transparent geometry
-
LINE_TOPOLOGY: Renders as line list instead of triangles
- Sets
VK_PRIMITIVE_TOPOLOGY_LINE_LISTin input assembly - Indices interpreted as pairs: (v0,v1), (v2,v3), etc.
- Perfect for glowing edges, wireframe overlays, debug visualization
- Typically combined with
SHOW_BACKFACESandALPHA_BLENDING
- Sets
Extending Customizer:
Applications can extend the enum with additional flags (use bits 0b01000000 and higher):
// In application-level Customizer.h override
enum Customizer
{
// ... built-in flags ...
LINE_TOPOLOGY = 0b00100000,
MY_CUSTOM_FLAG = 0b01000000, // Application-specific
ANOTHER_FLAG = 0b10000000
};Pipeline Configuration:
The GraphicsPipeline class automatically applies customizations during pipeline creation:
- Rasterization state:
polygonMode,cullMode,frontFace - Input assembly:
topology(triangle list vs line list) - Depth/stencil state:
depthWriteEnable(disabled with ALPHA_BLENDING) - Color blend state: Blend equation (configured with ALPHA_BLENDING)
Common Combinations:
// Transparent geometry (windows, glass)
drawable->customize = static_cast<Customizer>(SHOW_BACKFACES | ALPHA_BLENDING);
// Glowing line edges (TRON-style)
drawable->customize = static_cast<Customizer>(LINE_TOPOLOGY | SHOW_BACKFACES | ALPHA_BLENDING);
// Debug wireframe overlay
drawable->customize = WIREFRAME;
// Direct3D-native model
drawable->customize = static_cast<Customizer>(MODELED_FOR_DIRECT3D | FRONT_CLOCKWISE);