# How to Define and Apply PBR Materials Using the pbr_system in Equilibrium Engine

> Learn how to define and apply PBR materials with the pbr_system in Equilibrium Engine. Understand material data binding and shader uploads for efficient GPU rendering.

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

---

**The pbr_system bridges C-side material data with GLSL shaders by binding `Material` components to `PBRShader` components via `bind_material()`, which uploads uniform factors and texture handles each frame before GPU evaluation.**

The Equilibrium Engine provides a data-driven physically based rendering pipeline that separates material asset definitions from shader execution. Understanding how to define and apply PBR materials using the pbr_system enables developers to leverage glTF 2.0 assets or manual material creation while ensuring correct uniform binding and texture sampling in both forward and deferred rendering paths.

## Architecture Overview

The pbr_system operates across four distinct layers that connect scene data to GPU execution:

**Scene Data Layer** – Defined in [`scene_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/scene_components.h), the `Material` struct stores raw factor values (base color, metallic, roughness, normal scale, occlusion strength, emissive) and texture handles for each entity.

**Renderer Components Layer** – The `PBRShader` struct in [`renderer_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/renderer_components.h) maintains shader program handles, uniform locations for factor uploads, default texture references, and the albedo LUT for multiple-scattering correction.

**PBR System Layer** – Implemented in [`pbr_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr_system.c), this layer provides `bind_material()` and `set_texture_or_default()` to synchronize `Material` data with shader uniforms each frame.

**Shader Execution Layer** – The [`pbr.sh`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr.sh) library defines the `PBRMaterial` struct and evaluation functions (`pbrMaterial`, `pbrInitMaterial`) that compute final material properties from bound textures and uniforms.

## Defining PBR Materials

Materials can originate from glTF assets or manual component initialization.

### Manual Material Creation

To define a material programmatically, create an entity and populate the `Material` component:

```c
/* Create entity and attach Material component */
ecs_entity_t entity = ecs_new(world, 0);
Material *mat = ecs_emplace(world, entity, Material);

/* Configure base color texture and factor */
mat->base_color_texture = bgfx_create_texture_2d(
    1024, 1024, false, 1,
    BGFX_TEXTURE_FORMAT_RGBA8,
    BGFX_TEXTURE_NONE,
    bgfx_make_ref(image_data_basecolor, image_size));
mat->base_color_factor = (vec4){1.0f, 1.0f, 1.0f, 1.0f};

/* Configure metallic-roughness */
mat->metallic_roughness_texture = bgfx_create_texture_2d(...);
mat->metallic_factor = 1.0f;
mat->roughness_factor = 0.5f;

/* Configure normal mapping */
mat->normal_texture = bgfx_create_texture_2d(...);
mat->normal_scale = 1.0f;

/* Configure occlusion and emissive (optional) */
mat->occlusion_texture = BGFX_INVALID_HANDLE;
mat->occlusion_strength = 1.0f;
mat->emissive_texture = BGFX_INVALID_HANDLE;
mat->emissive_factor = (vec3){0.0f, 0.0f, 0.0f};

```

### Loading from glTF Assets

For glTF 2.0 files, [`cgltf_utils.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/cgltf_utils.c) automatically parses `pbrMetallicRoughness` properties into the `Material` component (lines 242-269). The loader populates texture references and factor values directly from the glTF JSON and binary buffers, eliminating manual configuration.

## The PBR System Workflow

Once materials are defined, the pbr_system binds them to shaders during rendering.

### PBRShader Component Initialization

The renderer creates a `PBRShader` component containing uniform handles and default textures:

```c
ECS_OBSERVER(world, InitializePBRShader, EcsOnSet, renderer.components.PBRShader);
ECS_SYSTEM(world, UpdatePBRShader, OnRender, renderer.components.PBRShader);

```

`InitializePBRShader` generates uniform handles for `base_color_factor_uniform`, `metallic_roughness_normal_occlusion_factor_uniform`, `emissive_factor_uniform`, and `has_textures_uniform`. It also creates a default 1×1 white texture and the albedo LUT for multiple-scattering correction.

### Binding Materials with bind_material

During the draw loop, the system calls `bind_material()` (implemented in [`pbr_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr_system.c), lines 29-66):

```c
/* In the mesh draw loop */
uint64_t material_id = bind_material(pbr_shader, material);
bgfx_set_state(state, material_id);
bgfx_submit(view_id, pbr_shader->program, 0, 0);

```

The `bind_material` function performs three critical operations:

1. **Uniform Upload** – Sets factor uniforms for base color, packed metallic-roughness-normal-occlusion, and emissive values.
2. **Texture Validation** – Packs a bit-field into `has_textures_uniform` indicating which of the five PBR texture slots (base color, metallic-roughness, normal, occlusion, emissive) contain valid data.
3. **Sampler Binding** – Calls `set_texture_or_default()` to bind each texture to its corresponding sampler slot (`PBR_BASECOLOR`, `PBR_METALROUGHNESS`, `PBR_NORMAL`, `PBR_OCCLUSION`, `PBR_EMISSIVE`), falling back to the default white texture when handles are invalid.

## Shader-Side Material Evaluation

After binding, the GPU executes PBR logic defined in [`pbr.sh`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr.sh). The fragment shader (`fs_forward.sc` or `fs_deferred_geometry.sc`) retrieves the material:

```glsl
#include "pbr.sh"

void main()
{
    // v_texcoord0 passed from vertex shader
    PBRMaterial mat = pbrMaterial(v_texcoord0);
    
    // Lighting evaluation
    vec3 color = BRDF(viewDir, lightDir, normal, NoV, NoL, mat);
    gl_FragColor = toneMap(color);
}

```

The `pbrMaterial` function (lines 36-53) constructs the material by sampling bound textures when the corresponding bit in `has_textures_uniform` is set, otherwise falling back to uniform factors. It then calls `pbrInitMaterial` (lines 57-74) to precompute `diffuseColor`, Fresnel reflectance `F0`, and remapped roughness `a` (roughness²) for the BRDF evaluation.

## Key Source Files

| File | Role |
|------|------|
| [`scene_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/scene_components.h) | Declares `Material` struct with texture handles and factor values |
| [`renderer_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/renderer_components.h) | Declares `PBRShader` with uniform handles and default textures |
| [`pbr_system.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr_system.h) | Public API header declaring `bind_material()` |
| [`pbr_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr_system.c) | Implements material binding, uniform uploads, and texture fallback logic |
| [`pbr.sh`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr.sh) | GLSL library defining `PBRMaterial` and evaluation functions |
| `fs_forward.sc` | Forward rendering fragment shader consuming PBR materials |
| `fs_deferred_geometry.sc` | Deferred rendering geometry pass fragment shader |
| [`cgltf_utils.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/cgltf_utils.c) | glTF 2.0 loader parsing `pbrMetallicRoughness` into `Material` |
| [`forward_renderer_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/forward_renderer_system.c) / [`deferred_renderer_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/deferred_renderer_system.c) | Create `PBRShader` components and invoke the PBR system |

## Summary

- The **pbr_system** connects CPU-side `Material` components to GPU shaders through the `bind_material()` function in [`pbr_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr_system.c).
- **Material definition** happens either manually by filling the `Material` struct in [`scene_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/scene_components.h) or automatically via [`cgltf_utils.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/cgltf_utils.c) for glTF assets.
- **Shader binding** uploads factor uniforms, packs texture availability bits into `has_textures_uniform`, and binds samplers to slots like `PBR_BASECOLOR` and `PBR_METALROUGHNESS`.
- **GPU evaluation** uses [`pbr.sh`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr.sh) functions to sample textures or fall back to uniforms, then precomputes BRDF inputs through `pbrInitMaterial`.

## Frequently Asked Questions

### What is the difference between the Material and PBRShader components?

The `Material` component defined in [`scene_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/scene_components.h) stores per-entity material data including texture handles and factor values on the CPU side. The `PBRShader` component defined in [`renderer_components.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/renderer_components.h) stores shader program handles, uniform locations, and default textures that are shared across multiple entities. The pbr_system bridges these components during rendering by copying material data into shader uniforms.

### How does the system handle missing textures?

When a texture handle is invalid (such as `BGFX_INVALID_HANDLE`), the `set_texture_or_default()` function in [`pbr_system.c`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr_system.c) binds a default 1×1 white texture instead. Simultaneously, the `has_textures_uniform` bit-field is updated to indicate which slots contain valid data, allowing the shader code in [`pbr.sh`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr.sh) to fall back to uniform factor values when the corresponding bit is not set.

### Can I use the pbr_system with both forward and deferred rendering?

Yes, the pbr_system is pipeline-agnostic. Both [`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) create `PBRShader` components and invoke `bind_material()` during their respective draw loops. The shader-side code in [`pbr.sh`](https://github.com/clibequilibrium/equilibriumengine/blob/main/pbr.sh) is included by both `fs_forward.sc` and `fs_deferred_geometry.sc`, ensuring consistent material evaluation across rendering paths.

### Where does the multiple-scattering albedo LUT get initialized?

The albedo LUT used for multiple-scattering correction is created during `PBRShader` initialization in the renderer systems. The `InitializePBRShader` observer generates this LUT texture and stores its handle in the `PBRShader` component alongside uniform handles and the default white texture. This initialization occurs once when the shader component is first created, not per-material or per-frame.