Material System
Myth's material system is designed around one goal: make custom materials easy to author without adding overhead on the rendering hot path. This chapter covers the current material architecture, what the #[myth_material] macro generates, and how to implement your own material.
1. Architecture Overview
Every material in the engine is a strongly-typed, memory-compact Rust struct. Material properties are laid out per the std140 rules so they can be uploaded directly as a uniform; textures are declared through dedicated texture slots.
This yields three concrete benefits:
- Cache-friendly: building bind groups across thousands of material instances every frame touches contiguous, compact memory — no string hashing or hash-map lookups.
- Compile-time validation: field types, alignment, and the shader contract are fixed at compile time, so mistakes are caught at build.
- Automatic synchronization: when a property or texture changes, the engine invalidates and rebuilds the corresponding pipeline cache automatically — no manual bookkeeping.
// A minimal custom material definition
#[myth_material(shader = "examples/holo", shader_src = HOLO_SHADER)]
pub struct HoloMaterial {
#[uniform(default = "Vec4::new(0.1, 0.8, 1.2, 1.0)")]
pub base_color: Vec4,
#[uniform(default = "1.0")]
pub opacity: f32,
#[texture]
pub normal_map: TextureSlot,
}2. The #[myth_material] Macro
#[myth_material] is the bridge between CPU-side data and the GPU-side shader contract. At compile time it generates all the boilerplate each material needs:
| Generated | Description |
|---|---|
| GPU uniform struct | Handles std140 field alignment and padding automatically; directly uploadable with zero runtime overhead. |
| WGSL mapping | #[uniform] fields map to members of u_material in the shader; #[texture] fields generate texture/sampler bindings and define conditional-compilation flags such as HAS_NORMAL_MAP. |
| Version tracking | Property or texture changes mark dirty state automatically, triggering invalidation and rebuild of the corresponding pipeline cache. |
| Defaults | The default = "..." expression is evaluated at construction, removing the need to hand-write Default. |
Field Attributes
#[uniform]: marks a numeric field (f32,Vec3,Vec4,Mat4, …) that is packed into the uniform buffer. An optionaldefaultexpression provides the initial value.#[texture]: marks aTextureSlotfield. When a binding is present, the engine defines the matchingHAS_*_MAPflag in the shader so you can branch on it in WGSL.
In the macro header, shader identifies the material category (it participates in the pipeline cache key) and shader_src points to the material's WGSL source (an inline constant or an external file).
3. Shader Template System
To avoid forcing developers to write large amounts of lighting/geometry boilerplate WGSL, material shaders support two modes:
MaterialBodymode (default): you write only the core shading logic (the internals ofvs_main/fs_main). The compiler injects for you:- Scene lighting structs (
scene_lighting_structs) - Clustered lighting definitions (
clustered_lighting_structs) - The vertex input struct (
VertexInput), auto-generated from the geometry layout
- Scene lighting structs (
Templatemode: use this when you need full control over the entire shader (entry points and binding layout included); the engine performs only the minimal necessary injection.
Because injection is per-pipeline, the same material code is reused seamlessly across forward rendering, the depth prepass, and shadow passes — no need to rewrite it for each pipeline.
4. Implementing a Custom Material
The full flow is three steps: define the struct, write the WGSL, use it in a scene.
// 1. Define the material (CPU side)
#[myth_material(shader = "examples/holo", shader_src = HOLO_SHADER)]
pub struct HoloMaterial {
#[uniform(default = "Vec4::new(0.1, 0.8, 1.2, 1.0)")]
pub base_color: Vec4,
#[uniform(default = "1.0")]
pub opacity: f32,
}
// 2. Write the shading logic (MaterialBody mode, core only)
const HOLO_SHADER: &str = r#"
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// u_material is generated by the macro from the fields
let glow = u_material.base_color.rgb * (0.6 + 0.4 * sin(globals.time * 3.0));
return vec4<f32>(glow, u_material.opacity);
}
"#;
// 3. Use it in a scene
let mat = HoloMaterial::default();
let mesh = scene.spawn_box(1.0, 1.0, 1.0, mat, &engine.assets);The engine creates and caches the pipeline for this material, handling uniform upload, binding, and version tracking. Mutating fields like mat.opacity at runtime is synced to the GPU automatically on the next frame.
Next Steps
- Use the built-in PBR materials → PBR Materials
- Author more complex custom materials and post FX → Custom Shaders & Post FX