How the Equilibrium Engine light_system Powers Scene Lighting and Effects

The Equilibrium Engine light_system bridges high-level PointLight components with GPU resources by managing dynamic vertex buffers and shader uniforms to enable real-time illumination in both forward and deferred rendering pipelines.

The light_system in the Equilibrium Engine (clibequilibrium/equilibriumengine) serves as the central orchestrator that transforms abstract light data into real-time visual effects. This ECS-driven module automatically handles buffer creation, per-frame data uploads, and shader binding to deliver physically-based illumination across scene renderers.

Architecture of the Light System

The light_system operates through three distinct stages that convert entity component data into shader-ready GPU resources. Each stage is implemented in equilibrium/systems/rendering/light_system.c and registered via LightSystemImport.

Initialization Stage

When a LightShader component is first added to an entity, the initialization observer creates the necessary GPU handles. The InitializeLightShader function (lines 21-38 in light_system.c) constructs a dynamic vertex buffer using BGFX_BUFFER_COMPUTE_READ | BGFX_BUFFER_ALLOW_RESIZE flags, enabling compute shader access and runtime resizing. It also initializes two critical uniform handles for passing metadata to shaders.

Binding Stage

During every render pass tagged OnRender, the BindPointLights function (lines 45-70 in light_system.c) uploads the light count and ambient irradiance to the GPU. This stage binds the dynamic vertex buffer as a compute-shader resource, ensuring the illumination data is available before geometry passes execute.

Update Stage

The UpdatePointLights system (lines 73-89 in light_system.c) runs on EcsOnUpdate and traverses all entities with PointLight components. It packs each light's position, intensity, and radius into the dynamic vertex buffer, pushing fresh data to the GPU every frame to support dynamic lighting scenarios.

Light Data Representation

PointLight Component

Light sources are defined by the PointLight component declared in equilibrium/components/scene/scene_components.h (lines 22-28). This struct holds world-space position, luminous flux, and physical parameters that describe realistic light sources.

GPU Vertex Format

The light_system converts each PointLight into a PointLightVertex struct defined within light_system.c. This layout matches the GPU shader expectations exactly:

typedef struct PointLightVertex {
    vec3  position;   // world-space centre of the light
    float padding;    // alignment filler
    vec3  intensity; // radiant intensity (W·sr⁻¹)
    float radius;    // calculated attenuation radius
} PointLightVertex;

The struct uses 16-byte alignment to satisfy GPU buffer layout requirements, ensuring efficient compute shader reads.

GPU Resource Management

Dynamic Vertex Buffer

The system maintains a bgfx_dynamic_vertex_buffer_handle_t buffer that stores an array of PointLightVertex structures. Created with compute-read permissions and resize capabilities, this buffer scales automatically as lights are added or removed from the scene.

Shader Uniforms

The LightShader component defined in equilibrium/components/renderer/renderer_components.h (lines 73-78) tracks two uniform handles:

bgfx_uniform_handle_t light_count_vec_uniform;
bgfx_uniform_handle_t ambient_light_irradiance_uniform;

These uniforms are populated each frame by BindPointLights, feeding the shader pipeline with the current light count and global ambient contribution.

Integration with Rendering Pipelines

Forward Renderer Integration

In equilibrium/systems/rendering/forward_renderer_system.c, the forward pipeline creates a LightShader entity attached to the camera. Shaders sample the dynamic vertex buffer through the compute view LIGHTS_POINTLIGHTS, iterating lights per pixel to accumulate illumination.

Deferred Renderer Integration

The deferred pipeline in equilibrium/systems/rendering/deferred_renderer_system.c utilizes the same light_system resources but employs geometry-based light culling. It renders lights as axis-aligned bounding boxes using the vertex buffer, while reading identical uniform data to maintain consistency across rendering paths.

Runtime Execution Flow

World Startup Registration

The LightSystemImport function registers three ECS systems during world initialization:

void LightSystemImport(world_t *world) {
    ECS_TAG(world, OnRender);
    ECS_MODULE(world, LightSystem);
    ECS_IMPORT(world, SceneComponents);
    ECS_IMPORT(world, RendererComponents);
    ECS_OBSERVER(world, InitializeLightShader, EcsOnSet, renderer.components.LightShader);
    ECS_SYSTEM(world, UpdatePointLights, EcsOnUpdate, scene.components.PointLight);
}

This registration establishes the observer pattern for component initialization and the update cycle for per-frame light processing.

Per-Frame Operations

Each frame executes the following sequence:

  1. Update – UpdatePointLights packs active PointLight components into the GPU buffer
  2. Bind – The OnRender phase triggers BindPointLights to upload uniforms and bind buffers
  3. Render – Geometry passes sample the bound resources to calculate final pixel illumination

Practical Implementation Examples

Creating a Point Light

Instantiate a light source by adding the PointLight component to an entity:

entity_t light = ecs_new(world, 0);
ecs_set(world, light, PointLight, {
    .position = { 5.0f, 3.0f, -2.0f },
    .flux     = { 150.0f, 150.0f, 150.0f }   // luminous flux in lumens
});

Adding this component automatically schedules UpdatePointLights to process the new light on the next frame.

Attaching LightShader to a Camera

Enable lighting in a forward renderer by attaching the shader component to the camera entity:

entity_t camera = ecs_lookup(world, "MainCamera");
ecs_set(world, camera, LightShader, {
    .light_count_vec_uniform          = BGFX_INVALID_HANDLE,
    .ambient_light_irradiance_uniform = BGFX_INVALID_HANDLE
});

The InitializeLightShader observer detects this component assignment and creates the underlying GPU resources automatically.

Frame Rendering Sequence

Coordinate the lighting pipeline within your render loop:

void OnRenderFrame(world_t *world, float delta_time) {
    // 1. Update point light data
    ecs_progress(world, delta_time); // runs UpdatePointLights

    // 2. Bind uniforms & buffers before the light pass
    ecs_run_pipeline(world, OnRender); // runs BindPointLights

    // 3. Submit draw calls for geometry
    //    – shaders read the bound point-light buffer
}

Summary

  • The Equilibrium Engine light_system manages the entire lifecycle of GPU lighting resources, from initialization through per-frame updates.
  • It converts PointLight components into PointLightVertex structures stored in a dynamic compute-readable buffer.
  • Two uniform handles (light_count_vec_uniform and ambient_light_irradiance_uniform) feed metadata to shaders during the OnRender phase.
  • The system supports both forward and deferred rendering pipelines through consistent buffer and uniform management.
  • ECS integration ensures automatic resource creation and updates via InitializeLightShader, UpdatePointLights, and BindPointLights.

Frequently Asked Questions

How does the light_system handle multiple point lights in a single scene?

The light_system aggregates all PointLight components into a single dynamic vertex buffer each frame. The UpdatePointLights function traverses every light entity and packs the data sequentially, while BindPointLights uploads the total count to the light_count_vec_uniform shader uniform, allowing shaders to iterate the correct number of entries.

What is the difference between PointLight and PointLightVertex?

The PointLight component (defined in scene_components.h) stores high-level physical properties like position and flux that game logic manipulates. The PointLightVertex struct (internal to light_system.c) is the GPU-optimized binary format containing position, intensity, and radius that shaders consume directly, ensuring proper memory alignment for compute operations.

Can the light_system accommodate dynamic light counts at runtime?

Yes. The dynamic vertex buffer is created with BGFX_BUFFER_ALLOW_RESIZE flags in InitializeLightShader, enabling the engine to expand or contract the buffer as lights are added or removed. The UpdatePointLights system recalculates the active light count every frame, ensuring the shader always receives current data without requiring manual buffer recreation.

How does the light_system integrate with custom rendering pipelines?

Custom renderers must include the LightShader component on their camera or view entity to trigger initialization. During the render loop, pipelines should execute the OnRender tag to invoke BindPointLights, then sample the LIGHTS_POINTLIGHTS compute view in shaders. Both forward and deferred implementations in forward_renderer_system.c and deferred_renderer_system.c demonstrate this pattern.

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 →