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
DeferredRenderercomponent (equilibrium/components/renderer/renderer_components.h) – Stores G-Buffer textures, shader programs, and light-pass resources.deferred_renderer_system.c(equilibrium/systems/rendering/deferred_renderer_system.c) – Creates the G-Buffer, manages view IDs, and orchestrates the geometry, light, and transparent passes.pbr_system.c(equilibrium/systems/rendering/pbr_system.c) – Prepares per-material uniforms, binds PBR textures, and generates the pre-computed multiple-scattering albedo LUT.
Data Flow
- Initialization –
InitializeDeferredRendererverifies GPU support viarenderer_supported(true)and aborts if the hardware cannot handle the deferred path. - G-Buffer Creation – The system creates a G-Buffer with five attachments defined in
GBufferAttachment: diffuse-roughness, normal, F0-metallic, emissive-occlusion, and depth. - PBR Setup – The
PBRShadercomponent holds uniform handles for material properties and manages the albedo LUT (generate_albedo_lut), bound asPBR_ALBEDO_LUT. - Frame Execution –
DeferredRendererBeginFrameclears buffers and sets view-projections.DrawOpaqueMeshessubmits geometry using the deferred geometry program. The depth texture is blitted tolight_depth_texturebeforeDrawPointLightsprocesses illumination. Finally,DrawTransparentMeshesruns 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_hasTexturesso 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:
OnBeginRender→DeferredRendererBeginFrame– Sets up views and clears geometry/light buffers.OnBeginRender→DrawOpaqueMeshes– Geometry pass usingdeferred_renderer->geometry_program.OnRender→DrawPointLights– Light pass using the fullscreen lighting program and point-light bounding boxes viadeferred_renderer->light_index_vec_uniform.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:
- Add a new uniform block in the
LightShaderstruct (e.g.,spot_light_program). - Create a new program with appropriate vertex/fragment shaders.
- Add a system similar to
DrawPointLightsthat submits geometry for the new light type. - Register the system in
DeferredRendererSystemImportfollowing 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
DeferredRenderercomponent to the main engine entity after BGFX initialization. - PBR materials use the
Materialstruct; thebind_materialfunction inpbr_system.cautomatically 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
DeferredRenderercomponent dynamically. - Customization requires editing
GBufferAttachmentformats 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →