A stylized deferred rendering pipeline built on a custom DirectX 12 engine, inspired by Minecraft’s Iris/OptiFine shader ecosystem. The project implements a complete G-Buffer pipeline with SM6.6 bindless resources, a modular ShaderBundle system with hot-swappable shader packs, and a suite of post-processing effects including volumetric clouds, volumetric lighting, screen-space reflections, and SSAO. Demonstrates expertise in modern graphics API design, GPU-driven rendering architecture, and real-time shader development.
Deferred Rendering Pipeline
The rendering pipeline is built on a strict four-layer architecture: the application layer (Game) drives scene logic, the integration layer (RendererSubsystem) exposes 60+ public APIs, the core system layer manages DirectX 12 state and PSO caching, and the resource layer handles GPU memory through RAII-based D12Resource subclasses. All textures and buffers are registered into a single global descriptor heap (1M capacity) and accessed via SM6.6 bindless indexing, eliminating traditional root signature slot juggling and enabling the shader to reference any resource through ResourceDescriptorHeap[index]. The pipeline is driven by a set of discrete RenderPass classes on the application side (Shadow, Terrain, TerrainCutout, TerrainTranslucent, SkyBasic, SkyTextured, Cloud, Deferred, Composite, and Final), each encapsulating its own shader programs, texture bindings, and render state. Render targets, depth textures, and shader programs are resolved at runtime through a data-driven ShaderBundle configuration.
Supporting this pipeline is a set of tightly scoped subsystems: a DXCCompiler that translates HLSL to DXIL with include graph resolution and comment-directive parsing, a UniformManager orchestrating 11 constant buffers via the Template Method pattern, a PSOManager that lazily creates and caches pipeline state objects keyed by shader + render state + RT format, and provider classes (ColorTextureProvider, DepthTextureProvider, SamplerProvider) that own render target allocation, ping-pong flipping, depth copy semantics, and dynamic sampler binding.
graph TB
subgraph APP["App Layer"]
RP["RenderPasses"]
end
subgraph INT["Integration Layer"]
RS["RendererSubsystem"]
FQ["FullQuadsRenderer"]
end
subgraph CORE["Core Systems"]
D3D["D3D12RenderSystem"]
PSO["PSOManager"]
UM["UniformManager"]
DXC["DXCCompiler"]
end
subgraph BUNDLE["ShaderBundle System"]
SBM["ShaderBundleManager"]
FB["FallbackChain"]
PROP["ShaderProperties"]
end
subgraph PROVIDER["Providers"]
CTP["ColorTextureProvider"]
DTP["DepthTextureProvider"]
SP["SamplerProvider"]
end
subgraph RES["Resource Layer"]
BRM["BindlessResourceManager"]
GDH["GlobalDescriptorHeap"]
BUF["D12Buffer"]
TEX["D12Texture"]
end
RP --> RS
RP --> FQ
RS --> D3D
RS --> PSO
RS --> UM
RS --> CTP
RS --> DTP
RS --> SP
SBM --> DXC
SBM --> FB
SBM --> PROP
PSO --> SBM
PSO --> D3D
CTP --> TEX
DTP --> TEX
UM --> BUF
BRM --> GDH
TEX --> BRM
BUF --> BRM
Render Target / Depth Provider
The engine manages all render targets through a unified provider architecture built on the IRenderTargetProvider interface. Four concrete providers, ColorTextureProvider (colortex0 to 15), DepthTextureProvider (depthtex0 to 2), ShadowColorProvider (shadowcolor0 to 7), and ShadowTextureProvider (shadowtex0 to 1), each own their GPU resources and expose a consistent API for creation, binding, clearing, and resizing. A RenderTargetBinder aggregates all four providers behind a single facade, using pending/current state hashing to minimize redundant OMSetRenderTargets calls.
Color and shadow-color targets use a dual-texture ping-pong scheme: each D12RenderTarget holds a main and an alternate D12Texture, with a BufferFlipState<N> bitset tracking which buffer is currently the write target per slot. Calling Flip(index) swaps read/write roles without any GPU copy. Depth textures are single-buffered. DepthTextureProvider instead snapshots depth at key pipeline stages via CopyDepth() (for example, copying depthtex0 into depthtex1 before translucent geometry). Every texture is registered into the global bindless descriptor heap at creation time. Each provider maintains an IndexUniforms constant buffer that maps slot indices to bindless SRV handles, updated per-frame via UpdateIndices() and uploaded to a dedicated cbuffer register (b3 to b6) so shaders can sample any RT by index.
graph TB
RTB["RenderTargetBinder"]
subgraph Providers
CTP["ColorTextureProvider"]
DTP["DepthTextureProvider"]
SCP["ShadowColorProvider"]
STP["ShadowTextureProvider"]
end
subgraph Resources
DRT["D12RenderTarget"]
DDT["D12DepthTexture"]
end
BFS["BufferFlipState"]
IDX["IndexUniforms"]
GDH["GlobalDescriptorHeap"]
RTB --> CTP
RTB --> DTP
RTB --> SCP
RTB --> STP
CTP --> DRT
SCP --> DRT
DTP --> DDT
STP --> DDT
CTP --> BFS
SCP --> BFS
CTP --> IDX
DTP --> IDX
SCP --> IDX
STP --> IDX
DRT --> GDH
DDT --> GDH
IDX -.-> GDH
Vertex Layout Registration
A deferred renderer needs to push very different vertex data depending on the pass. A full-screen quad only requires position, UV, and color (PCU, 24 bytes), while terrain geometry carries normals, lightmap coordinates, block entity IDs, and mid-texture coordinates for atlas animation (TerrainVertex, 56 bytes). Hardcoding any single input layout into the PSO would either waste bandwidth or lock out game-layer extensions entirely. The engine solves this with a VertexLayoutRegistry: an abstract VertexLayout base class exposes GetInputElements() / GetStride(), concrete implementations (Vertex_PCU, Vertex_PCUTBN, TerrainVertex) self-register at startup, and the registry hands out const VertexLayout* pointers by name. Because the pointer is part of the PSOKey hash, the PSOManager automatically caches a separate PSO per shader-layout combination with zero manual bookkeeping.
Each RenderPass declares its layout in BeginPass() via SetVertexLayout(). Terrain passes bind TerrainVertexLayout, everything else uses the default Vertex_PCUTBN. The renderer resets to the default at the start of every frame, so passes that don’t call SetVertexLayout() just work. Game-layer layouts like TerrainVertexLayout are registered after engine startup, keeping the dependency arrow pointing inward (Open-Closed Principle). TerrainVertexLayout also exposes a MulticastDelegate event (OnBuildVertexLayout) that lets the ShaderBundle system inject material IDs into vertices at mesh-build time, without the vertex type or chunk mesh ever knowing about shader packs.
graph LR
subgraph Registry["VertexLayoutRegistry"]
VL["VertexLayout"]
PCU["Vertex_PCU"]
PCUTBN["Vertex_PCUTBN"]
TV["TerrainVertex"]
end
VL --> PCU
VL --> PCUTBN
VL --> TV
RP["RenderPass"] -->|SetVertexLayout| RS["RendererSubsystem"]
RS -->|layout ptr in PSOKey| PSO["PSOManager"]
PSO -->|GetInputElements| VL
Per Render Target Blend Config
In a deferred pipeline, different render targets in the same pass often need different blending behavior. A water pass might alpha-blend color output on colortex0 while writing normals to colortex4 with blending disabled to avoid corrupting G-Buffer data. DirectX 12 supports this through IndependentBlendEnable, but configuring it per-pass and per-RT from shader pack authors requires a clean data-driven path. The engine provides exactly that through two configuration channels.
The first channel is shaders.properties, where ShaderBundle authors declare blend modes per program and optionally per colortex slot. A global directive like blend.gbuffers_water = SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ONE_MINUS_SRC_ALPHA sets the default blend for all RTs in that pass, while a per-buffer override like blend.gbuffers_water.colortex4 = off disables blending on a specific slot. During ShaderBundle loading, ShaderProperties parses these directives and InjectBlendDirectives() maps the semantic colortex index to the physical RT slot index through the drawBuffers array, then stores the result in ProgramDirectives. The second channel is direct code configuration, where a RenderPass calls SetBlendConfig(config) for global blend or SetBlendConfig(config, rtIndex) for per-RT override.
At draw time, RendererSubsystem packs the current blend state (global config + up to 8 per-RT overrides) into the PSOKey. The PSOManager fills all 8 RT slots with the global config first, then applies per-RT overrides only where isUndefined is false. This sentinel value distinguishes “explicitly set to opaque” from “not configured”, ensuring that unspecified slots inherit the global default cleanly. Each RenderPass resets blend to Opaque() after drawing to prevent state leaking into the next program.
graph TB
subgraph Config["Configuration Sources"]
SP["shaders.properties"]
CODE["RenderPass Code"]
end
subgraph Processing
PARSE["ShaderProperties"]
INJ["InjectBlendDirectives"]
PD["ProgramDirectives"]
end
subgraph Runtime
RSUB["RendererSubsystem"]
PKEY["PSOKey"]
PSOMGR["PSOManager"]
D3D["D3D12_BLEND_DESC"]
end
SP --> PARSE --> INJ --> PD
CODE --> RSUB
PD --> RSUB
RSUB --> PKEY --> PSOMGR --> D3D
DXC Compiler
The engine compiles all HLSL shaders at runtime through a DXCCompiler wrapper around Microsoft’s DirectX Shader Compiler. Every shader targets Shader Model 6.6 with HLSL 2021 syntax and 16-bit type support enabled, producing DXIL bytecode that the PSOManager consumes directly. Because the engine uses a fully bindless architecture, traditional shader reflection (ID3D12ShaderReflection) is stripped entirely. Resource indices arrive through root constants rather than reflected binding slots, which cuts compilation time significantly and removes a whole class of binding mismatch bugs.
Before source code reaches DXC, the engine’s own include and directive systems have already processed it. The IncludeGraph builds a dependency tree, the IncludeProcessor flattens it into a single translation unit, and the CommentDirectiveParser extracts render state from structured comments. DXC receives a fully self-contained source string with no remaining #include directives.
Include Graph
ShaderBundle HLSL files use #include to share common libraries (lighting math, noise functions, uniform declarations). Rather than relying on DXC’s built-in file-system include handler, the engine resolves includes ahead of time through a two-phase process. First, IncludeGraph performs a BFS traversal starting from the entry file, discovering every transitive dependency and building a DAG of FileNode objects. Each node records its ShaderPath (a normalized Unix-style virtual path) and its list of child includes. Second, IncludeProcessor walks this graph in DFS order and concatenates the file contents into a single expanded source string, skipping duplicates so that shared headers are included exactly once. The result is a flat, self-contained HLSL source that DXC compiles without ever touching the file system.
This design also enables fast incremental checks. Because the graph is cached per ShaderBundle load, the engine can detect when a shared header changes and invalidate only the programs that depend on it.
Comment Directive Parsing
Iris-style shaders encode render state directly in HLSL comments rather than in external configuration files. The engine’s CommentDirectiveParser is a stateless scanner that extracts these directives from the expanded pixel shader source and populates a ProgramDirectives object. Supported directives include:
/* RENDERTARGETS: 0,3,4 */or/* DRAWBUFFERS:034 */to declare which color attachments this program writes to, determiningNumRenderTargetsandRTVFormats[]in the PSO/* DEPTH_TEST: LEQUAL */and/* DEPTH_WRITE: true */to configure depth-stencil state/* CULLFACE: BACK */to set rasterizer cull mode/* BLEND: ADD */for blend operation hints
The PSOManager reads these directives when building a PSOKey, so each unique combination of draw buffers, depth mode, cull face, and blend config produces its own cached PSO. This keeps shader authors in full control of GPU state without touching any C++ code.
graph LR
subgraph Include["Include Resolution"]
IG["IncludeGraph BFS"]
IP["IncludeProcessor DFS"]
end
subgraph Directives["Directive Extraction"]
CDP["CommentDirectiveParser"]
PD["ProgramDirectives"]
end
HLSL["HLSL Source Files"] --> IG --> IP --> FLAT["Expanded Source"]
FLAT --> DXC["DXCCompiler SM6.6"]
FLAT --> CDP --> PD
DXC --> DXIL["DXIL Bytecode"]
PD --> PKEY["PSOKey"]
DXIL --> PSOMGR["PSOManager"]
PKEY --> PSOMGR
PSOMGR --> PSO["ID3D12PipelineState"]
Built-in Engine Shader Library
The engine ships with a self-contained shader library that provides every built-in program, uniform declaration, and utility function a ShaderBundle needs to render a complete frame. Any ShaderBundle shader can #include "../include/core.hlsl" to pull in all uniform cbuffers, vertex structures (VSInput/VSOutput), the standard vertex transform, and math constants in a single line.
Uniform buffers are split across two register spaces. Space 0 holds engine-managed cbuffers that the renderer populates automatically every frame or every draw call: transform matrices (b7), camera parameters (b9), viewport dimensions (b10), per-object data (b1), and all bindless index tables for color textures (b3), depth textures (b4), shadow colors (b5), shadow textures (b6), samplers (b8), and custom images (b2). Space 1 is reserved for game-side and ShaderBundle-authored uniforms that map to Iris-compatible variables: world time (b1), fog parameters (b2), world info like cloud height and ambient light (b3), common rendering state such as rain strength and sky color (b8), and celestial data including sun angle and shadow light position (b9). This separation means the engine’s root signature owns space 0 through direct root CBVs while space 1 binds through a descriptor table that the game layer fills, so ShaderBundle authors can add custom cbuffers in space 1 without touching engine code.
All texture access is fully bindless. Rather than declaring Texture2D : register(t*), each include file stores bindless SRV indices in uint4 arrays inside cbuffers, and shaders sample via ResourceDescriptorHeap[index]. The uint4 packing avoids the HLSL cbuffer alignment trap where scalar arrays pad each element to 16 bytes. Several include files also provide helper functions beyond raw data: LinearizeDepth() and LinearToNDCDepth() in camera uniforms, CalculateFogFactor() and ApplyFog() in fog uniforms, and named sampler aliases (linearSampler, pointSampler, shadowSampler, wrapSampler) in sampler uniforms.
engine/shaders/
core/
core.hlsl main entry, includes all uniforms
gbuffers_basic.vs/ps.hlsl
gbuffers_textured.vs/ps.hlsl
include/
camera_uniforms.hlsl b9 space0, LinearizeDepth()
matrices_uniforms.hlsl b7 space0, all MVP/shadow matrices
viewport_uniforms.hlsl b10 space0, resolution and aspect
color_texture_uniforms.hlsl b3 space0, colortex0-15 indices
depth_texture_uniforms.hlsl b4 space0, depthtex0-2 indices
shadow_color_uniforms.hlsl b5 space0, shadowcolor0-7 indices
shadow_texture_uniforms.hlsl b6 space0, shadowtex0-1 indices
sampler_uniforms.hlsl b8 space0, sampler aliases
perobject_uniforms.hlsl b1 space0, model matrix/color
custom_image_uniforms.hlsl b2 space0, custom texture indices
common_uniforms.hlsl b8 space1, renderStage/rain/sky
worldtime_uniforms.hlsl b1 space1, worldTime/moonPhase
fog_uniforms.hlsl b2 space1, fog color/density
worldinfo_uniforms.hlsl b3 space1, cloud height/ambient
celestial_uniforms.hlsl b9 space1, sun/moon/shadow pos
developer_uniforms.hlsl debug-only uniforms
lib/
fog.hlsl fog calculation utilities
spaceConversion.hlsl coordinate space transforms
program/
gbuffers_terrain.vs/ps.hlsl terrain rendering
gbuffers_water.vs/ps.hlsl water surface
shadow.vs/ps.hlsl shadow map generation
composite.vs/ps.hlsl post-process compositing
final.vs/ps.hlsl final output to swapchain
... sky, cloud, debug programs
Application-Side Render Pipeline
The engine deliberately does not own the rendering order. RendererSubsystem provides stateless APIs (bind shader, set blend, draw geometry, upload uniforms) but never decides which pass runs when. That responsibility belongs to the application layer, where Game::RenderWorld() calls each RenderPass in an explicit, linear sequence. This separation means the engine can serve any rendering strategy (forward, deferred, hybrid) without modification, while the application defines the exact pipeline topology for its use case.
Each RenderPass inherits from SceneRenderPass, an abstract base class that defines three hooks: Execute() (public entry point), BeginPass() (set up render state, bind programs and targets), and EndPass() (restore state). The base class also handles ShaderBundle hot-reload automatically by subscribing to OnBundleLoaded / OnBundleUnloaded events in its constructor and unsubscribing in its destructor, so concrete passes only need to override the callbacks to refresh their cached ShaderProgram pointers. A static helper RenderPassHelper translates drawBuffers index lists into typed render target references that UseProgram() consumes.
Game owns all passes as unique_ptr<SceneRenderPass> and calls them in a fixed Iris-compatible order inside RenderWorld(). The pipeline follows a strict sequence: shadow generation, sky rendering into the G-Buffer, opaque terrain geometry, deferred lighting (which must complete before translucents so they blend onto a fully lit scene), translucent geometry (water and clouds), multi-stage compositing (SSR, volumetric light, tonemapping), and final output to the swapchain.
graph TB
subgraph Shadow["1. Shadow"]
S1["ShadowRenderPass"]
S2["ShadowCompositeRenderPass"]
end
subgraph Sky["2. Sky"]
SK1["SkyBasicRenderPass"]
SK2["SkyTexturedRenderPass"]
end
subgraph GBuffer["3. Opaque G-Buffer"]
T1["TerrainRenderPass"]
T2["TerrainCutoutRenderPass"]
end
DEF["4. DeferredRenderPass"]
subgraph Trans["5. Translucent"]
TT["TerrainTranslucentRenderPass"]
CL["CloudRenderPass"]
end
COMP["6. CompositeRenderPass"]
FIN["7. FinalRenderPass"]
DBG["DebugRenderPass"]
S1 --> S2 --> SK1 --> SK2 --> T1 --> T2 --> DEF --> TT --> CL --> COMP --> FIN
FIN -.-> DBG
Code/Game/Framework/RenderPass/
SceneRenderPass.hpp/cpp abstract base class
RenderPassHelper.hpp/cpp static utilities
WorldRenderingPhase.hpp phase enum
ConstantBuffer/ POD uniform structs
RenderShadow/ shadow map generation
RenderShadowComposite/ shadow post-process
RenderSkyBasic/ sky dome and void
RenderSkyTextured/ sun, moon, stars
RenderTerrain/ opaque terrain
RenderTerrainCutout/ alpha-tested foliage
RenderTerrainTranslucent/ water and ice
RenderCloud/ cloud geometry
RenderDeferred/ deferred lighting
RenderComposite/ SSR, VL, tonemap
RenderFinal/ output to swapchain
RenderDebug/ debug overlays
The ShaderBundle system is this engine’s equivalent of Minecraft’s Iris/OptiFine ShaderPack. A ShaderBundle is a self-contained directory of HLSL programs, property files, fallback rules, and custom textures that completely defines how the world is rendered. Shader authors can write their own bundles to achieve any visual style (toon shading, photorealistic PBR, stylized painterly) without modifying a single line of engine C++ code. The engine discovers available bundles by scanning the .enigma/shaderbundles/ directory at startup and supports hot-swapping between them at runtime through an ImGui selector or API call.
The architecture uses a dual-bundle design. An engine bundle ships with the renderer and is always loaded as the final fallback. When a user bundle is active, every GetProgram() call walks a three-level fallback chain: first the current user-defined sub-bundle (for per-profile shader variants), then the bundle’s own program/ folder following fallback_rule.json chains (for example gbuffers_clouds falls back to gbuffers_textured then to gbuffers_basic), and finally the engine bundle which guarantees that every pass always has a valid shader. Bundle switching is deferred to the frame boundary to avoid D3D12 errors from deleting render targets mid-frame, and the previous bundle’s resources are released automatically through shared_ptr ownership.
Shader Bundle System
A ShaderBundle is organized as a directory under .enigma/shaderbundles/ with a fixed layout that the engine scans at load time. The program/ folder holds the primary shader programs (one .vs.hlsl and one .ps.hlsl per pass). The bundle/ folder contains named sub-bundles that can override any program for profile or dimension variants. lib/ stores reusable HLSL libraries (lighting, noise, shadow math, tonemapping) that programs pull in via #include. include/ holds shared declarations specific to this bundle. shaders.properties configures render targets, blend modes, and buffer formats per program. block.properties maps block IDs to material categories for the MaterialIdMapper. Custom textures live in textures/ with optional .enigmeta sidecar files that define sampling and format metadata.
On the engine side, the Bundle module is split into focused subsystems. ShaderBundle and UserDefinedBundle manage program ownership and the three-level fallback chain. ShaderProperties and PackRenderTargetDirectives parse the properties files into structured data. MaterialIdMapper bridges block IDs to shader material categories. BundleTextureLoader and EnigmetaParser handle custom texture loading. ShaderBundleSubsystem ties everything together as the engine integration point, handling discovery, lifecycle, and configuration persistence through shaderbundle.yml.
graph TB
subgraph Integration
SBS["ShaderBundleSubsystem"]
CFG["Configuration"]
end
subgraph Core
SB["ShaderBundle"]
UDB["UserDefinedBundle"]
PFC["ProgramFallbackChain"]
end
subgraph Config["Configuration Parsing"]
SP["ShaderProperties"]
RTD["PackRenderTargetDirectives"]
PSD["PackShadowDirectives"]
MIM["MaterialIdMapper"]
end
subgraph Assets["Asset Loading"]
BTL["BundleTextureLoader"]
EMP["EnigmetaParser"]
end
subgraph Helpers
JH["JsonHelper"]
FH["FileHelper"]
SH["ScanHelper"]
end
SBS --> SB
SBS --> CFG
SB --> UDB
SB --> PFC
SB --> SP
SB --> RTD
SB --> PSD
SB --> MIM
SB --> BTL
BTL --> EMP
SB --> Helpers
EnigmaDefault/shaders/
shaders.properties RT formats, blend modes, buffer config
block.properties block ID to material mapping
bundle.json bundle metadata
program/
gbuffers_terrain.vs/ps terrain geometry
gbuffers_water.vs/ps water surface
shadow.vs/ps shadow map generation
deferred1.vs/ps deferred lighting
composite.vs/ps post-process pass 0
composite1.vs/ps post-process pass 1
composite5.vs/ps tonemapping
final.vs/ps output to swapchain
... sky, cloud, debug programs
bundle/
mycustom_bundle_0/ sub-bundle variant (overrides program/)
gbuffers_terrain.vs/ps
composite.vs/ps
...
lib/
atmosphere.hlsl sky and atmosphere math
fog.hlsl fog calculations
lighting.hlsl diffuse and specular models
noise.hlsl procedural noise functions
shadow.hlsl shadow sampling and bias
tonemap.hlsl HDR to LDR conversion
volumetric_light.hlsl volumetric ray marching
water.hlsl water surface utilities
include/
settings.hlsl user-facing quality toggles
... bundle-specific declarations
textures/
cloud-water.png custom texture asset
cloud-water.png.enigmeta sampling and format metadata
Material ID Mapper
In a voxel renderer, all terrain geometry shares the same vertex shader and pixel shader per pass. Water, grass, stone, and leaves all flow through gbuffers_terrain as identical quads with no built-in way for the shader to tell them apart. Without a material identification mechanism, effects like water reflections, translucent tinting, or emissive glow would require separate render passes per block type, which defeats the purpose of batched chunk rendering.
The MaterialIdMapper solves this by bridging a data file (block.properties) to the vertex stream. ShaderBundle authors define mappings in a simple properties format where each line assigns a numeric material ID to one or more namespaced block names (for example block.32000=simpleminer:water). At bundle load time, MaterialIdMapper parses these entries into an unordered_map<string, uint16_t> lookup table. The mapper then subscribes to TerrainVertexLayout::OnBuildVertexLayout, a MulticastDelegate event that fires every time the voxel mesher builds a quad. When the event fires with a block name that has a mapping, OnBuildVertex() stamps the material ID into the m_entityId field of all four quad vertices. The shader reads this value from TEXCOORD2 (matching Iris’s mc_Entity semantic) and branches on it to apply material-specific logic.
This design keeps the voxel mesher, vertex layout, and shader completely decoupled. The mesher knows nothing about materials. The vertex layout only knows it has a uint16 entity ID slot. The shader only reads an integer. All the knowledge of “water is 32000” lives in block.properties, which ShaderBundle authors can edit without recompiling anything.
graph LR
BP["block.properties"] --> MIM["MaterialIdMapper"]
MIM --> EVT["OnBuildVertexLayout"]
MESH["Voxel Mesher"] --> EVT
EVT --> TV["TerrainVertex m_entityId"]
TV --> PS["Pixel Shader TEXCOORD2"]
Life Hook Event
The ShaderBundle system uses two event mechanisms to keep subsystems decoupled from the bundle lifecycle. The first is a string-based EventSystem that fires named events through the engine’s global event bus. The second is a typed MulticastDelegate system that provides compile-time safe, direct callback registration. Both allow any subsystem to react to bundle changes without the bundle module holding references to its consumers.
| Event | Mechanism | Trigger | Typical Subscriber |
|---|---|---|---|
OnShaderBundleLoaded | EventSystem | After a user bundle finishes loading | RenderPasses (rebuild PSO cache) |
OnShaderBundleUnloaded | EventSystem | Before switching back to engine bundle | RenderPasses (release user programs) |
OnShaderBundlePropertiesModified | EventSystem | When shaders.properties is edited at runtime | PSOManager (invalidate cached state) |
OnShaderBundlePropertiesReset | EventSystem | When properties are reset to defaults | PSOManager (restore original config) |
OnShaderBundleReload | EventSystem | When a hot-reload is requested | ShaderBundleSubsystem (recompile all) |
OnBundleLoaded | MulticastDelegate | After bundle load completes | MaterialIdMapper subscription setup |
OnBundleUnloaded | MulticastDelegate | After bundle unload | MaterialIdMapper subscription teardown |
OnBuildVertexLayout | MulticastDelegate | Per quad during chunk meshing | MaterialIdMapper (inject entity ID) |
The MulticastDelegate events are particularly important for the MaterialIdMapper workflow. When a bundle loads, ShaderBundleSubsystem subscribes the mapper’s OnBuildVertex callback to TerrainVertexLayout::OnBuildVertexLayout and stores the returned DelegateHandle. When the bundle unloads, it removes the subscription using that handle. This ensures material ID injection is only active while a bundle with block.properties is loaded, and no dangling callbacks survive a bundle swap.
Update RTs Configuration
A deferred renderer packs different data into each render target: HDR color in colortex0 might need R16G16B16A16_FLOAT, while normals in colortex2 fit in R8G8B8A8_SNORM, and a material mask in colortex3 only needs R8G8B8A8_UNORM. The engine lets ShaderBundle authors declare these formats, clear behavior, and clear colors directly in HLSL through a dedicated include file (rt_formats.hlsl). This keeps all RT configuration colocated with the shaders that write to them, rather than scattered across C++ code or external config files.
The configuration uses two directive styles parsed by PackRenderTargetDirectives. Format directives live inside /* */ comment blocks because DXGI format names like R16G16B16A16_FLOAT are not valid HLSL identifiers. The ConstDirectiveParser extracts them by scanning raw source lines before compilation. Clear and clear-color directives are valid HLSL const declarations (const bool colortex0Clear = true, const float4 colortex0ClearColor = float4(0,0,0,1)) that the same parser picks up through AST-level const evaluation. Both directive types support all four RT categories: colortex (0 to 15), depthtex (0 to 2), shadowcolor (0 to 7), and shadowtex (0 to 1).
At bundle load time, the engine collects these directives from all shader sources, merges them with YAML-defined defaults, and produces a RenderTargetConfig per slot. The providers then use these configs to create GPU resources with the correct DXGI format, set the appropriate clear action (Load, Clear, or DontCare), and apply the specified clear color at the start of each frame.
// rt_formats.hlsl example (EnigmaDefault)
// Format directives (inside comments, not valid HLSL)
/*
const int colortex0Format = R16G16B16A16_FLOAT;
const int colortex1Format = R8G8B8A8_UNORM;
const int colortex2Format = R8G8B8A8_SNORM;
*/
// Clear control (valid HLSL const declarations)
const bool colortex0Clear = true;
const bool colortex6Clear = false;
// Clear color
const float4 colortex1ClearColor = float4(0.0, 0.0, 1.0, 1.0);
// Shadow RT configuration
const bool shadowcolor0Clear = true;
const float4 shadowcolor0ClearColor = float4(1.0, 1.0, 1.0, 1.0);
graph TB
subgraph HLSL["rt_formats.hlsl"]
FMT["Format Directives"]
CLR["Clear / ClearColor"]
end
subgraph Parsing
CDP["ConstDirectiveParser"]
PRTD["PackRenderTargetDirectives"]
end
subgraph Output["RenderTargetConfig per slot"]
CF["DXGI Format"]
CA["Clear Action"]
CC["Clear Color"]
end
FMT --> CDP
CLR --> CDP
CDP --> PRTD
YAML["YAML Defaults"] --> PRTD
PRTD --> CF
PRTD --> CA
PRTD --> CC
CF --> PROV["Providers"]
CA --> PROV
CC --> PROV
Shader Properties
Each ShaderBundle includes a shaders.properties file that acts as the central configuration surface for shader authors. This file controls pipeline behavior without requiring any C++ changes, giving bundle creators full authority over how their shaders interact with the rendering engine.
The properties file supports several directive categories. Quality profiles define named presets (POTATO through ULTRA) that map to sets of macro definitions and numeric parameters like shadow distance, reflection quality, and SSAO settings. Custom texture bindings assign image assets to numbered slots per program (for example texture.deferred.3=textures/cloud-water.png), making them available in HLSL through the bindless GetCustomImage() accessor. Blend directives configure per-program and per-RT blending as described in the Per Render Target Blend Config section. The ShaderProperties parser loads this file at bundle load time and distributes the parsed data to the relevant subsystems: profiles feed into macro definitions for DXC compilation, texture bindings go to BundleTextureLoader, and blend directives are injected into ProgramDirectives for PSO creation.
Stylized Shader Bundle
EnigmaDefault is the project’s flagship ShaderBundle, targeting high-quality stylized rendering inspired by the Complementary Reimagined shader pack for Minecraft. The visual direction aims for rich atmospheric depth, soft natural lighting, and painterly color grading while preserving the blocky charm of voxel geometry. Rather than chasing photorealism, the bundle emphasizes mood and readability through carefully tuned volumetric clouds, warm atmospheric scattering, smooth water reflections, and subtle ambient occlusion. Every effect is implemented in pure HLSL within the bundle’s lib/ and program/ directories, running entirely through the engine’s data-driven deferred pipeline with no hardcoded rendering logic on the C++ side.
Shadow Mapping and Bias
The shadow system uses a single shadow map with nonlinear XY distortion rather than cascaded shadow maps. The distortion warps clip-space coordinates so that the area near the camera receives higher texel density while distant regions are compressed, achieving a similar near-field precision benefit as CSM with a single depth pass. The Z axis is additionally compressed to 20% of its original range, expanding effective depth precision across the entire shadow frustum. The distortion factor is driven by SHADOW_DISTANCE (configurable per quality profile, defaulting to 128 blocks).
Bias uses a two-layer approach. The primary technique is normal offset bias applied in world space: the shadow sampling point is pushed along the surface normal by an amount that adapts to both distance (farther surfaces get larger offsets to compensate for reduced texel density) and angle (grazing angles where NdotL approaches zero receive up to 2x the offset). This eliminates shadow acne without the light-bleeding artifacts that constant depth bias introduces on thin geometry. The secondary layer is a soft depth comparison that uses a narrow transition window (factor of 256) normalized against the Z compression, producing near-hard edges that the PCF filter then softens.
Shadow sampling supports six quality tiers from hard single-tap to 16-tap circular PCF. The PCF kernel distributes samples in a circular pattern using Interleaved Gradient Noise (IGN) for screen-space dithering, breaking up banding artifacts without temporal filtering. The kernel radius scales dynamically with three factors: distance from camera (far shadows get wider kernels to match the lower texel density), rain strength (overcast weather softens shadows up to 3x), and an edge fade band that smoothly transitions shadows to full brightness over the last 8 blocks before the shadow distance cutoff.
The shadow pass also writes two additional render targets beyond the depth buffer. shadowcolor0 stores a water caustic pattern sampled from a custom noise texture with dual-frequency blending, and shadowcolor1 stores underwater volumetric light color with exponential distance attenuation. Both are consumed later in the lighting and composite stages for water rendering.
graph TB
subgraph Shadow["ShadowRenderPass"]
SVS["shadow.vs.hlsl"]
SPS["shadow.ps.hlsl"]
end
subgraph Output["Shadow Output"]
ST1["shadowtex1 depth"]
SC0["shadowcolor0 caustics"]
SC1["shadowcolor1 underwater VL"]
end
subgraph Deferred["DeferredRenderPass"]
D1["deferred1.ps.hlsl"]
LIT["lighting.hlsl"]
SHAD["shadow.hlsl"]
end
SVS --> ST1
SPS --> SC0
SPS --> SC1
ST1 --> SHAD
SHAD --> LIT
SC0 --> LIT
SC1 --> LIT
LIT --> D1
D1 --> CT0["colortex0 lit scene"]
Volumetric Cloud
The cloud system replaces Minecraft’s vanilla geometric clouds entirely (the gbuffers_clouds pass discards all geometry) and renders volumetric clouds through screen-space ray marching in the deferred lighting pass (deferred1.ps.hlsl). Cloud shape is driven by a custom 256x256 noise texture (cloud-water.png, bound as customImage3) rather than runtime FBM noise. The .b channel encodes a density distribution pattern that is sampled at two different smoothstep widths: a tight roundness = 0.125 for shape definition and a softer roundness = 0.35 for self-shadow lookups. The density function applies an 8th-power threshold (x^8) to produce the sharp, well-defined cloud edges characteristic of the Complementary Reimagined style, where low-density regions collapse to near-zero and high-density regions snap to solid.
Ray marching begins by intersecting the view ray with a horizontal slab defined by cloudAltitude (default 192) plus or minus CLOUD_STRETCH (4.2 blocks of vertical thickness). The march uses uniform step sizes with IGN dithering to break banding, and supports three quality tiers: 16, 32, or 48 samples. Several optimizations keep the cost manageable: early exit when the ray exceeds render distance, skipping samples occluded by terrain geometry, and a first-hit termination strategy that stops marching as soon as a cloud sample is found rather than accumulating transmittance across the full slab.
Cloud lighting uses a height gradient (bottom-dark, top-bright with a 2.5 power curve) combined with up to two self-shadow samples along the light direction. Rather than physically-based Beer-Lambert transmittance or Henyey-Greenstein phase functions, the bundle follows the Complementary Reimagined approach of a half-Lambert view-sun dot product mixed with the height gradient, producing a stylized but convincing approximation at lower computational cost. Cloud color blends between sky-tinted ambient and warm direct light, with smooth transitions for sunrise/sunset, nighttime, and rain conditions. Wind animation is tied to worldTime (game-synchronized) rather than real-time frame counters, so clouds move consistently with time acceleration.
The final cloud color and alpha are composited into colortex0 via alpha blending in the deferred pass. A nonlinear cloud depth (square-root encoded for near-field precision) is written to colortex5.a and passed to composite1, where it limits the volumetric light ray march range so that light shafts do not penetrate through cloud layers.
graph LR
TEX["cloud-water.png .b"] --> DENSITY["GetCloudNoise"]
DENSITY --> MARCH["Ray March 16-48 steps"]
MARCH --> LIGHT["Cloud Lighting"]
LIGHT --> DEF["deferred1.ps.hlsl"]
DEF --> CT0["colortex0 composited"]
DEF --> CT5["colortex5.a cloud depth"]
CT5 --> COMP1["composite1 VL clamp"]