Materials & Textures
Material System Overview
Each material is a MaterialData struct holding GPU texture resources and a descriptor set:
Materials are stored in materials_ vector. Material ID 0 is always the default (1x1 white) texture.
Maximum materials: MAX_MATERIALS = 64 (descriptor pool limit).
Descriptor Set 1 (Per-Material)
Bound per-entity in the draw loop:
Default Texture
createDefaultTexture() creates a 1x1 white RGBA pixel ({255, 255, 255, 255}) as VK_FORMAT_R8G8B8A8_SRGB. This is used for meshes without textures — the fragment shader multiplies texture(baseColorTex, fragUV).rgb * fragColor, so a white texture means vertex color passes through unchanged.
Texture Sampler
createTextureSampler() creates a shared sampler:
- Filter:
VK_FILTER_LINEAR(mag + min) - Address mode:
VK_SAMPLER_ADDRESS_MODE_REPEAT(all axes) - Anisotropy: disabled
- Mipmap mode:
VK_SAMPLER_MIPMAP_MODE_LINEAR - Format: textures use
VK_FORMAT_R8G8B8A8_SRGB
loadTextureFromMemory()
loadTextureFromMemory(const uint8_t* data, size_t size) loads a texture from encoded image data (PNG, JPG, etc.):
- Decode:
stbi_load_from_memory()→ RGBA pixels, forced 4 channels - Create image:
VK_FORMAT_R8G8B8A8_SRGB, optimal tiling, device-local - Upload: staging buffer →
transitionImageLayout(UNDEFINED → TRANSFER_DST) →copyBufferToImage→transitionImageLayout(TRANSFER_DST → SHADER_READ_ONLY) - Create image view: standard 2D view with COLOR aspect
- Create material: allocates descriptor set, writes sampler + image view
- Returns material ID (index into
materials_)
glTF Texture Extraction
In loadMesh(), after parsing geometry, the loader scans for base_color_texture:
Embedded (GLB):
External URI:
Only the first material with a base color texture is loaded. If no texture is found, the mesh uses defaultMaterialId_ (1x1 white).
Cleanup
cleanupMaterialResources() destroys:
- All materials that
ownsTexture— destroys image view, image, and frees memory - Default texture (image view, image, memory)
- Texture sampler
- Material descriptor pool
- Material descriptor set layout
Adding PBR maps (normal, metallic-roughness, etc.): Add additional VkImage/VkImageView fields to MaterialData. Expand the descriptor set layout to include new COMBINED_IMAGE_SAMPLER bindings (binding 1, 2, etc.). Load additional textures in loadMesh() and update shader.frag to sample them.
Changing sampler settings: Modify createTextureSampler() in scene/material.cpp. For example, enable anisotropy with anisotropyEnable = VK_TRUE and set maxAnisotropy (query device limits first).
Adding material properties: Add fields to MaterialData (e.g., roughness, metallic values). To make them available in the shader, either expand the material descriptor set with a UBO or use push constants.