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:
- Update –
UpdatePointLightspacks activePointLightcomponents into the GPU buffer - Bind – The
OnRenderphase triggersBindPointLightsto upload uniforms and bind buffers - 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_systemmanages the entire lifecycle of GPU lighting resources, from initialization through per-frame updates. - It converts
PointLightcomponents intoPointLightVertexstructures stored in a dynamic compute-readable buffer. - Two uniform handles (
light_count_vec_uniformandambient_light_irradiance_uniform) feed metadata to shaders during theOnRenderphase. - 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, andBindPointLights.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →