- name
- forge-pbr-textures
- description
- Add PBR texture loading with separate roughness/metallic support to an SDL GPU project using forge_scene.h
Load PBR material texture sets (albedo, normal, roughness, metallic, AO,
emissive) from the asset pipeline and render them with a Cook-Torrance BRDF
shader. Supports both packed metallic-roughness (glTF convention) and
separate single-channel textures (ambientCG convention).
## When to use
- Loading PBR materials from `.fmat` sidecar files
- Rendering textured PBR materials on scene models
- Supporting both packed and separate metallic-roughness workflows
- Adding multiple material support to a scene
## Material loading pattern
```c
/* Parse .fmat sidecar */
ForgePipelineMaterialSet mat_set;
if (!forge_pipeline_load_materials("assets/materials/Rock026/Rock026.fmat", &mat_set)
|| mat_set.material_count == 0) {
SDL_Log("Failed to load Rock026 material");
return false;
}
const ForgePipelineMaterial *mat = &mat_set.materials[0];
/* Load textures from processed assets.
* vram tracks GPU memory usage — must be non-NULL. */
ForgeSceneVramStats vram = {0};
ForgeSceneModelTextures tex = {0};
/* Base color — sRGB (authored color) */
tex.base_color = forge_scene_load_pipeline_texture(
scene, &vram, "assets/materials/Rock026/Rock026_Color.png", true, false);
/* Normal map — linear, is_normal_map=true for BC5 */
tex.normal = forge_scene_load_pipeline_texture(
scene, &vram, "assets/materials/Rock026/Rock026_NormalGL.png", false, true);
/* Separate roughness — linear */
tex.roughness = forge_scene_load_pipeline_texture(
scene, &vram, "assets/materials/Rock026/Rock026_Roughness.png", false, false);
/* AO — linear, R channel */
tex.occlusion = forge_scene_load_pipeline_texture(
scene, &vram, "assets/materials/Rock026/Rock026_AmbientOcclusion.png", false, false);
```
## 8-sampler PBR pipeline
The shader uses 8 texture slots to support both packed and separate workflows:
| Slot | Texture | Color space | Fallback |
|------|---------|-------------|----------|
| 0 | Base color | sRGB | White |
| 1 | Normal map | Linear | Flat (+Z) |
| 2 | Packed metallic-roughness | Linear | White |
| 3 | Occlusion (AO) | Linear | White |
| 4 | Emissive | sRGB | Black |
| 5 | Shadow map | Depth | Scene shadow |
| 6 | Separate roughness | Linear | White |
| 7 | Separate metallic | Linear | White |
## Separate MR flag
The `shininess` field in `ForgeSceneModelFragUniforms` (offset 80, unused by
PBR) is repurposed as `use_separate_mr`:
```c
ForgeSceneModelFragUniforms fu;
forge_scene__fill_model_frag_uniforms(scene, mat, &fu);
fu.shininess = (tex.roughness || tex.metallic) ? 1.0f : 0.0f;
```
In the shader:
```hlsl
if (use_separate_mr > 0.5) {
roughness = roughness_tex.Sample(roughness_smp, input.uv).r * roughness_factor;
metallic = metallic_tex.Sample(metallic_smp, input.uv).r * metallic_factor;
} else {
float2 mr = mr_tex.Sample(mr_smp, input.uv).bg;
metallic = mr.x * metallic_factor;
roughness = mr.y * roughness_factor;
}
```
## sRGB vs linear
| Data type | Color space | Why |
|-----------|-------------|-----|
| Base color, emissive | sRGB | Authored as visible color — GPU linearizes on sample |
| Normal, roughness, metallic, AO | Linear | Physical parameters — gamma would distort values |
## Common mistakes
- **Loading roughness as sRGB** — makes 0.5 appear as ~0.22, materials look
too shiny
- **Loading normal maps with sRGB** — corrupts direction vectors, surface
detail is lost
- **Forgetting the use_separate_mr flag** — shader reads packed MR texture
(white fallback = all 1.0), making everything fully metallic and rough
- **Not checking `metallic_roughness_texture` before loading separate** —
if both packed and separate exist, prefer packed
## Reference
See [Lesson 52 — PBR Textures](../../../lessons/gpu/52-pbr-textures/) for
the full implementation.
GitHubで見る