Skip to content

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.
rust
// 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:

GeneratedDescription
GPU uniform structHandles 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 trackingProperty or texture changes mark dirty state automatically, triggering invalidation and rebuild of the corresponding pipeline cache.
DefaultsThe 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 optional default expression provides the initial value.
  • #[texture]: marks a TextureSlot field. When a binding is present, the engine defines the matching HAS_*_MAP flag 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:

  • MaterialBody mode (default): you write only the core shading logic (the internals of vs_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
  • Template mode: 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.

rust
// 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

Released under the MIT / Apache-2.0 dual license