How to Implement Forward Rendering in Equilibrium Engine: A Complete Guide
Forward rendering in Equilibrium Engine is implemented through a series of ECS systems that validate hardware capabilities, initialize the ForwardRenderer component, prepare the view each frame, and iterate over visible meshes to submit draw calls with PBR materials.
The Equilibrium Engine (clibequilibrium/equilibriumengine) provides a data-driven forward rendering pipeline built on top of bgfx. This guide walks through the exact implementation steps found in the source code, from hardware validation to the final mesh draw loop.
Prerequisites and Hardware Validation
Before initializing the forward renderer, the engine confirms that the target device supports the required color attachments for standard or HDR rendering.
Checking Renderer Support
The renderer_supported function in equilibrium/utils/bgfx_utils.h performs this check. It validates that the current bgfx renderer can handle the forward rendering path before any resources are allocated.
// From bgfx_utils.h - returns false if forward rendering is unsupported
if (!renderer_supported(false)) {
ecs_err("Forward rendering not supported");
return;
}
If this check fails, initialization aborts immediately to prevent runtime errors.
Initializing the Forward Renderer
Once hardware support is confirmed, the engine creates the necessary ECS components and registers the rendering systems.
Creating the ForwardRenderer Component
The InitializeForwardRenderer function in equilibrium/systems/rendering/forward_renderer_system.c handles component creation. It compiles the forward shader program (vs_forward.bin and fs_forward.bin) and stores the handle in the ForwardRenderer component.
// From forward_renderer_system.c
static void InitializeForwardRenderer(ecs_iter_t *it) {
if (!renderer_supported(false)) {
ecs_err("Forward rendering not supported");
return;
}
entity_t e = { it->entities[0], it->world };
ForwardRenderer *fr = entity_get_or_add_component(e, ForwardRenderer);
fr->program = create_program(e, program, ForwardRenderer,
"vs_forward.bin", "fs_forward.bin");
}
The ForwardRenderer component itself is defined in equilibrium/components/renderer/renderer_components.h and contains only the compiled program handle.
Registering ECS Systems
The ForwardRendererSystemImport function registers the renderer within the ECS world. It sets up the initialization observer and two critical systems: ForwardRendererBeginFrame (triggered on OnBeginRender) and DrawMeshes (triggered on OnRender).
// From forward_renderer_system.c
void ForwardRendererSystemImport(ecs_world_t *world) {
ECS_MODULE(world, ForwardRendererSystem);
// Import dependencies
ECS_IMPORT(world, BaseRenderingSystem);
ECS_IMPORT(world, CameraSystem);
ECS_IMPORT(world, PBRSystem);
ECS_IMPORT(world, LightSystem);
ECS_IMPORT(world, BgfxSystem);
// Register systems
ECS_OBSERVER(world, InitializeForwardRenderer, EcsOnSet, [in] Bgfx);
ECS_SYSTEM(world, ForwardRendererBeginFrame, OnBeginRender,
[in] ForwardRenderer, [in] AppWindow, [in] FrameData, [in] Camera);
ECS_SYSTEM(world, DrawMeshes, OnRender,
[in] FrameData, [in] PBRShader, [in] ForwardRenderer);
}
This registration ensures the forward renderer integrates with the engine's camera, PBR, and lighting systems.
Per-Frame Rendering Pipeline
With systems registered, the engine executes the forward rendering pipeline every frame through two main phases.
Preparing the View
The ForwardRendererBeginFrame function in forward_renderer_system.c prepares the rendering surface. It clears the color and depth buffers, sets the viewport dimensions, binds the frame buffer, and updates the view-projection matrix using set_view_projection from bgfx_utils.h.
// From forward_renderer_system.c
static void ForwardRendererBeginFrame(ecs_iter_t *it) {
ForwardRenderer *fr = ecs_field(it, ForwardRenderer, 1);
AppWindow *win = ecs_field(it, AppWindow, 2);
FrameData *fd = ecs_field(it, FrameData, 3);
Camera *cam = ecs_field(it, Camera, 4);
for (int i = 0; i < it->count; ++i) {
bgfx_set_view_name(0, "Forward render pass");
bgfx_set_view_clear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH,
0x303030FF, 1.0f, 0);
bgfx_set_view_rect(0, 0, 0, win[i].width, win[i].height);
bgfx_set_view_frame_buffer(0, fd[i].frame_buffer);
bgfx_touch(0);
set_view_projection(0, &cam[i], win[i].width, win[i].height);
}
}
Drawing Meshes
The DrawMeshes function executes the core rendering logic. It queries all entities with Mesh, Material, and Transform components, then iterates through mesh groups to submit draw calls.
For each mesh group, the system:
- Applies the world transform via
bgfx_set_transform - Sets the normal matrix via
set_normal_matrix - Binds vertex and index buffers
- Applies PBR material parameters via
bind_materialfrom the PBR system - Submits the draw call with the forward shader program
// From forward_renderer_system.c
static void DrawMeshes(ecs_iter_t *it) {
FrameData *fd = ecs_field(it, FrameData, 1);
PBRShader *pbr = ecs_field(it, PBRShader, 2);
ForwardRenderer *fr = ecs_field(it, ForwardRenderer, 3);
ecs_iter_t q = ecs_query_iter(it->world, it->ctx);
while (ecs_query_next(&q)) {
Mesh *m = ecs_field(&q, Mesh, 1);
Material *mat = ecs_field(&q, Material, 2);
Transform *tr = ecs_field(&q, Transform, 3);
for (int i = 0; i < q.count; ++i) {
for (size_t g = 0; g < ecs_vector_count(m[i].groups); ++g) {
Group *grp = ecs_vector_get(m[i].groups, Group, g);
bgfx_set_transform(&tr[i].value, 1);
set_normal_matrix(fd, tr[i].value);
bgfx_set_vertex_buffer(0, grp->vertex_buffer, 0, UINT32_MAX);
bgfx_set_index_buffer(grp->index_buffer, 0, UINT32_MAX);
uint64_t matState = bind_material(pbr, &mat[i]);
bgfx_set_state(BGFX_STATE_DEFAULT & ~BGFX_STATE_CULL_MASK | matState, 0);
bgfx_submit(0, fr->program, 0,
~BGFX_DISCARD_BINDINGS | BGFX_DISCARD_INDEX_BUFFER |
BGFX_DISCARD_VERTEX_STREAMS);
}
}
}
bgfx_discard(BGFX_DISCARD_ALL);
}
Key Source Files and Architecture
The forward rendering implementation spans several modules within the clibequilibrium/equilibriumengine repository:
equilibrium/systems/rendering/forward_renderer_system.c– Core implementation containingInitializeForwardRenderer,ForwardRendererBeginFrame, andDrawMeshes.equilibrium/systems/rendering/forward_renderer_system.h– Public API exposingForwardRendererSystemImportfor module registration.equilibrium/components/renderer/renderer_components.h– Definition of theForwardRenderercomponent storing the shader program handle.equilibrium/utils/bgfx_utils.h– Utility functions includingrenderer_supportedfor hardware validation andset_view_projectionfor matrix updates.equilibrium/systems/rendering/base_rendering_system.c– Provides the underlying bgfx context and frame buffer management that forward rendering depends upon.equilibrium/systems/rendering/pbr_system.c– Supplies thebind_materialfunction used to apply PBR parameters during the mesh draw loop.
Summary
- Validate hardware using
renderer_supportedinbgfx_utils.hbefore allocating resources. - Initialize the component with
InitializeForwardRendererto compile forward shaders and create theForwardRenderercomponent. - Register systems via
ForwardRendererSystemImportto hook intoOnBeginRenderandOnRenderphases. - Prepare the frame in
ForwardRendererBeginFrameby clearing buffers, setting viewports, and updating view-projection matrices. - Render meshes in
DrawMeshesby iterating ECS queries, binding PBR materials, and submitting draw calls with the forward shader program.
Frequently Asked Questions
What is the difference between forward rendering and deferred rendering in Equilibrium Engine?
Forward rendering processes lighting calculations directly during the mesh draw call using the DrawMeshes system, while deferred rendering separates geometry and lighting into distinct passes. The engine implements forward rendering in forward_renderer_system.c and deferred rendering in deferred_renderer_system.c, with both systems sharing the same base rendering infrastructure but differing in how they handle lighting calculations and frame buffer attachments.
How does the forward renderer handle PBR materials?
The forward renderer delegates material binding to the PBR system through the bind_material function defined in pbr_system.c. During the DrawMeshes execution, the system calls bind_material with the PBR shader context and material component, which returns a render state mask. This mask is then combined with default bgfx states before submitting the draw call, ensuring proper metallic-roughness workflow support in the forward pass.
Can I use custom shaders with the forward rendering system?
Yes, you can extend the forward renderer by modifying the shader compilation step in InitializeForwardRenderer. The system loads compiled shader binaries (vs_forward.bin and fs_forward.bin) using the create_program utility. To use custom shaders, replace these binary names with your own compiled vertex and fragment shaders, or create additional ForwardRenderer variants with different program handles stored in the component defined in renderer_components.h.
What ECS components are required for an entity to render with the forward renderer?
An entity must possess three core components to be processed by the DrawMeshes system: Mesh (containing vertex and index buffer groups), Material (PBR material parameters), and Transform (world transformation matrix). Additionally, the world must have entities with ForwardRenderer, AppWindow, FrameData, Camera, and PBRShader components to satisfy the system queries defined in forward_renderer_system.c.
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 →