Loading
Thesis Research
Stylized Deferred Rendering

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, determining NumRenderTargets and RTVFormats[] 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.

EventMechanismTriggerTypical Subscriber
OnShaderBundleLoadedEventSystemAfter a user bundle finishes loadingRenderPasses (rebuild PSO cache)
OnShaderBundleUnloadedEventSystemBefore switching back to engine bundleRenderPasses (release user programs)
OnShaderBundlePropertiesModifiedEventSystemWhen shaders.properties is edited at runtimePSOManager (invalidate cached state)
OnShaderBundlePropertiesResetEventSystemWhen properties are reset to defaultsPSOManager (restore original config)
OnShaderBundleReloadEventSystemWhen a hot-reload is requestedShaderBundleSubsystem (recompile all)
OnBundleLoadedMulticastDelegateAfter bundle load completesMaterialIdMapper subscription setup
OnBundleUnloadedMulticastDelegateAfter bundle unloadMaterialIdMapper subscription teardown
OnBuildVertexLayoutMulticastDelegatePer quad during chunk meshingMaterialIdMapper (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"]

Volumetric Light

Screen Space Reflection

SSAO

Design Philosophy