# How the Equilibrium Engine light_system Powers Scene Lighting and Effects

> Discover how the Equilibrium Engine light_system powers scene lighting and effects using dynamic vertex buffers and shader uniforms for real-time illumination in any rendering pipeline. Optimize your graphics today.

- Repository: [Alexander/equilibriumengine](https://github.com/clibequilibrium/equilibriumengine)
- Tags: internals
- Published: 2026-02-27

---

**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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/light_system.c). This layout matches the GPU shader expectations exactly:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/equilibrium/components/renderer/renderer_components.h) (lines 73-78) tracks two uniform handles:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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:

```c
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:

```c
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:

```c
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:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/scene_components.h)) stores high-level physical properties like position and flux that game logic manipulates. The `PointLightVertex` struct (internal to [`light_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/forward_renderer_system.c) and [`deferred_renderer_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/deferred_renderer_system.c) demonstrate this pattern.