Monograph · 04 · Materials & PBR
Foundations · full depth
Materials & PBR
Follow one material from a GLB field all the way into GBuffer lighting and path-tracer NEE — pack layout, BRDF math as coded, light structs, and the failure modes when the two pipelines disagree.
End-to-end material pipeline
Loaders never touch Vulkan. Upload + pack is GPU chapter territory; this chapter owns the language both sides speak.
- Loader reads baseColor / metallic / roughness / texture URIs (glTF metallic-roughness or Assimp)
MaterialComponentholds runtime scalars + logical texture names- On scene upload: images →
BindlessTextureManager→ integer indices - RT path: pack 3×vec4 into matColors SSBO; per-triangle matID; closesthit samples
- Deferred path: per-draw push constants carry scalars + tex indices; gbuffer.frag samples
- Lighting or NEE multiplies by BRDF (GGX + Lambert)
CPU representation
Typical fields after load (names vary slightly by loader):
- baseColor / albedo — linear RGB (or sRGB textures sampled then used as albedo)
- metallic, roughness — scalars in \([0,1]\); maps multiply later
- normal map, ORM (occlusion-roughness-metallic), emissive — optional
- alpha / cutout — any-hit path for cards; some OBJ materials filtered entirely
glTF’s default PBR model: metalness blends dielectric F₀≈0.04 into base color; roughness spreads the specular lobe. OHAO does not use a separate specular-glossiness path as the beauty SoT (though tinygltf can read the extension).
Texture upload & bindless
- Create/update VkImage + view + sampler for each unique texture
- Write into descriptor array
sampler2D textures[](PT binding 12; GBuffer similar) - Store index on material;
0xFFFFFFFFmeans “no texture” - Shaders:
nonuniformEXT(idx)when indexing dynamically
Multi-material GLBs would thrash descriptor sets per draw. One array + indices scales to thousands of textures with one layout.
MaterialGpuPack — 48 bytes / material
// matColors[matID * 3 + …] +0 vec4(baseColor.rgb, asfloat(diffuseTexIdx)) +1 vec4(roughness, metallic, normalTexIdx, emissiveTexIdx) +2 vec4(roughMetalTexIdx, unused, unused, unused) // kNoTexture = 0xFFFFFFFFu // static_assert pack == 48 bytes
Texture indices are bit-cast through floats so a single vec4 stream stays std430-friendly. OHAO_ASSERT_GPU_LAYOUT / concepts catch C++ vs GLSL drift at compile time.
matID / instance order
TLAS instances and material rows are built together in rt_build.cpp. Reordering one list without the other swaps materials across the scene silently.
Deferred push-constant path
GBuffer draws do not use the full matColors SSBO for every parameter. Per draw (~224–240 B PC under NVIDIA’s 256 B limit):
- model, viewProj, prevMVP (velocity)
- materialParams: metallic, roughness, roughMetalTexIdx, albedoTexIdx
- albedoColor.rgb + normalTexIdx in .a
- emissive tex index + strength
gbuffer.frag then: sample albedo; optional normal map; if ORM index < 4096 multiply ao/rough/metal (R/G/B); rough = max(rough, 0.04); write MRT packing (Ch. 05).
GGX / Cook-Torrance as shipped
File: shaders/includes/brdf/brdf_ggx.glsl. Specular:
Geometry: height-correlated Smith (Frostbite-style). Diffuse: Lambert \(c/\pi\) energy-scaled by \((1-m)(1-F)\). Path-tracer NEE evaluates a compatible GGX path in raygen (including optional anisotropy helper). Roughness floor exists so mirrors do not NaN and NRD sign-bits survive.
Industry default for real-time + offline PBR assets. Matches glTF expectations so DamagedHelmet-class models look “right” without custom artist tuning.
Light structs & budgets
| Path tracer | Deferred | |
|---|---|---|
| Struct | GPULight 80 B (5×vec4) | LightData 128 B (+ light-space matrix) |
| Count | up to 64 SSBO | up to 8 UBO |
| Types | sphere, dir, spot, area | dir / point / spot (+ CSM) |
| Env | miss + CDF IS + MIS | equirect IBL LOD |
Same scene lights are repacked differently. Realtime budget is intentional — many-light PT uses NEE sampling; deferred loops a small UBO.
Failure modes
Deferred metal black: missing IBL / LUT (Ch. 05) — not necessarily pack failure.
Wrong material on mesh: TLAS/mat order desync.
Pink/black textures: bindless index OOB or 0xFFFFFFFF mishandled.
PT vs deferred mismatch: different roughness multiply or missing ORM on one path.
layout_meta.hppPack + byte sizesbrdf/brdf_ggx.glslD/F/G evalgbuffer.fragDeferred sample + MRTpt_closesthit.rchitRT sample → payloadgpu_light.hpp / light_upload.cppLightsbindless_texture_manager.*IndicesDesign units in this module
Each card is a focused design page (what / how / why + sources). Full tree: Sitemap.
PBR metallic-roughness
F0, ORM convention, roughness floors.
GGX implementation
D/F/G terms, Smith correlated, energy helpers.
GPU material pack
3×vec4 matColors, texture bit-cast indices.
Advanced material includes
advanced_brdf, material_sampling, material_types.
Lights dual packing
GPULight SSBO (64) vs deferred LightData UBO (8).