How to Configure and Use Deferred Rendering with PBR in EquilibriumEngine

To enable deferred rendering with PBR in EquilibriumEngine, attach the DeferredRenderer component to the main engine entity; the system automatically creates a G-Buffer, runs geometry and light passes, and composites transparent objects using physically-based shading.

EquilibriumEngine supports two rendering paths: a forward renderer (the default) and a deferred renderer that integrates tightly with the physically-based rendering (PBR) system. This guide explains how to activate the deferred pipeline, configure PBR materials, and understand the underlying architecture implemented in the clibequilibrium/equilibriumengine repository.

Architecture Overview

The deferred rendering architecture consists of three primary layers that work together to separate geometry processing from lighting calculations.

Core Components and Systems

Data Flow

  1. Initialization – InitializeDeferredRenderer verifies GPU support via renderer_supported(true) and aborts if the hardware cannot handle the deferred path.
  2. G-Buffer Creation – The system creates a G-Buffer with five attachments defined in GBufferAttachment: diffuse-roughness, normal, F0-metallic, emissive-occlusion, and depth.
  3. PBR Setup – The PBRShader component holds uniform handles for material properties and manages the albedo LUT (generate_albedo_lut), bound as PBR_ALBEDO_LUT.
  4. Frame Execution – DeferredRendererBeginFrame clears buffers and sets view-projections. DrawOpaqueMeshes submits geometry using the deferred geometry program. The depth texture is blitted to light_depth_texture before DrawPointLights processes illumination. Finally, DrawTransparentMeshes runs a forward pass for blending.

Enabling Deferred Rendering

The engine selects the rendering path based on the presence of a DeferredRenderer component on the main engine entity. To switch from forward to deferred rendering, add the component immediately after initializing BGFX:

/* Attach the DeferredRenderer component to the engine entity */
entity_t engine_entity = {world->entities[0], world};
DeferredRenderer *deferred = entity_get_or_add_component(engine_entity, DeferredRenderer);
(void)deferred; // the system will initialise it on the next frame

If you prefer forward rendering, simply omit this component. The system uses flecs observers, so InitializeDeferredRenderer runs automatically when the Bgfx component is set.

Configuring PBR Materials

Materials are defined in renderer_components.h via the Material struct. The deferred path requires valid texture handles or relies on automatic fallback to a 1×1 white default texture.

Material mat = {
    .base_color_factor   = {1.0f, 1.0f, 1.0f, 1.0f},
    .metallic_factor     = 0.0f,
    .roughness_factor    = 0.5f,
    .normal_scale        = 1.0f,
    .occlusion_strength  = 1.0f,
    .blend               = false,           // opaque
    .double_sided        = false,
    .base_color_texture  = my_albedo_texture,
    .metallic_roughness_texture = my_mr_texture,
    .normal_texture      = my_normal_texture,
    .occlusion_texture   = my_ao_texture,
    .emissive_texture    = my_emissive_texture,
};
ecs_set(world, mesh_entity, Material, {mat});

The bind_material function in pbr_system.c handles the binding logic:

  • Uploads factor vectors as uniforms (u_baseColorFactor, u_metallicRoughnessNormalOcclusionFactor, u_emissiveFactorVec)
  • Sets texture samplers (s_texBaseColor, s_texMetallicRoughness, s_texNormal, s_texOcclusion, s_texEmissive)
  • Packs a bit-mask into u_hasTextures so shaders skip sampling missing textures

Rendering Pipeline Execution

The main loop in launcher/launcher.cc calls ecs_progress(world, dt). The deferred system registers four flecs callbacks that execute in sequence:

  1. OnBeginRender → DeferredRendererBeginFrame – Sets up views and clears geometry/light buffers.
  2. OnBeginRender → DrawOpaqueMeshes – Geometry pass using deferred_renderer->geometry_program.
  3. OnRender → DrawPointLights – Light pass using the fullscreen lighting program and point-light bounding boxes via deferred_renderer->light_index_vec_uniform.
  4. OnRender → DrawTransparentMeshes – Forward transparency pass into the accumulation buffer.

No additional user code is required beyond adding the DeferredRenderer component.

Practical Implementation Example

Below is a minimal, reproducible snippet that starts an engine instance with deferred PBR rendering enabled:

#include "equilibrium/equilibrium.h"
#include "components/renderer/renderer_components.h"

int main(void) {
    world_t *world = ecs_init_w_args(0, NULL);
    ecs_set_name(world, "EquilibriumEngine");

    /* Load core systems (SDL, BGFX, etc.) */
    ecs_import(world, "Equilibrium");

    /* Create the root entity that represents the application */
    entity_t app = {ecs_new_id(world), world};

    /* Attach window & BGFX components – normally done by the Launcher */
    ecs_set(world, app.entity, AppWindow, {800, 600, "Deferred PBR Demo"});
    ecs_set(world, app.entity, Bgfx, {});

    /* *** Enable deferred rendering *** */
    entity_get_or_add_component(app, DeferredRenderer);   // <-- key line

    /* Load a glTF scene (Sponza) – utils will fill Mesh, Material, Transform, etc. */
    load_gltf_scene(world, "launcher/sandbox/models/Sponza/glTF/Sponza.bin");

    /* Main loop */
    while (app_is_running(world)) {
        ecs_progress(world, 0.016f);   // 60 Hz tick
    }

    ecs_fini(world);
    return 0;
}

The load_gltf_scene utility in utils/cgltf_utils.c creates entities with Mesh, Material, and Transform components, automatically populating the PBR material fields required by the deferred pipeline.

Advanced Configuration

Tuning the G-Buffer Layout

The G-Buffer layout is defined by GBufferAttachment in renderer_components.h. The format array gBufferAttachmentFormats maps each attachment to a BGFX texture format. To increase precision (e.g., HDR normals), modify the array and update the corresponding sampler names in InitializeDeferredRenderer. Remember to synchronize changes with fs_deferred_geometry.sc in launcher/sandbox/shaders/shading/.

Adding Custom Light Types

The current implementation supports point lights (LIGHTS_POINTLIGHTS). To extend it:

  1. Add a new uniform block in the LightShader struct (e.g., spot_light_program).
  2. Create a new program with appropriate vertex/fragment shaders.
  3. Add a system similar to DrawPointLights that submits geometry for the new light type.
  4. Register the system in DeferredRendererSystemImport following the point light pattern.

Debugging Common Issues

Symptom Likely Cause Fix
Black screen, no geometry G-Buffer not created (GPU unsupported) Verify renderer_supported(true) returns true; check BGFX renderer type.
Missing textures on materials set_texture_or_default fell back to default white texture Ensure texture handles are valid (bgfx_is_texture_valid).
Light contributions missing light_depth_texture not blitted before light pass Confirm bgfx_blit call (lines 286-288 in deferred_renderer_system.c) executes without error.
Transparent objects appear over everything Transparent pass bound to wrong view Ensure DrawTransparentMeshes uses vTransparent and the same accumulation buffer.

All debug output routes through ecs_trace/ecs_err, configured in bgfx_system.c.

Summary

  • Enable deferred rendering by adding the DeferredRenderer component to the main engine entity after BGFX initialization.
  • PBR materials use the Material struct; the bind_material function in pbr_system.c automatically handles texture samplers and uniform uploads.
  • Pipeline stages include G-Buffer creation, opaque geometry pass, depth blit, light accumulation, and forward transparency pass.
  • Runtime toggling is possible by adding or removing the DeferredRenderer component dynamically.
  • Customization requires editing GBufferAttachment formats and corresponding shader code in the sandbox shaders directory.

Frequently Asked Questions

How do I switch between forward and deferred rendering at runtime?

Remove or add the DeferredRenderer component on the engine entity. When removed, the system automatically falls back to the forward renderer. Use entity_get_or_add_component to enable deferred mode and ecs_remove to disable it.

Can I override the default albedo LUT for artistic control?

Yes. After the PBRShader component is created, replace the albedo_lut_texture handle with a custom texture created via create_texture_2d, then call bind_albedo_lut(shader, false) to bind it for subsequent render passes.

What texture formats does the G-Buffer use by default?

The default formats defined in gBufferAttachmentFormats are BGRA8 for diffuse-roughness, F0-metallic, and emissive-occlusion; RG16F for normals; and a hardware-specific depth format. These are declared in renderer_components.h and consumed by the geometry shader (fs_deferred_geometry.sc).

Why are my transparent objects rendering incorrectly?

The transparent pass must use the same accumulation buffer and the vTransparent view ID. Verify that DrawTransparentMeshes executes after the light pass and that depth testing is configured correctly for forward rendering within the deferred pipeline.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →