Skip to content

Latest commit

Β 

History

History
365 lines (282 loc) Β· 11.6 KB

File metadata and controls

365 lines (282 loc) Β· 11.6 KB

Materials Guide

Page Map

Header Link
Purpose Purpose
Use Cases Use Cases
Example Example
Reference Reference
Live Params Live Params

Purpose

This guide covers the whole material workflow: author a .pmat (or use a glTF material), get a MaterialID in scripts, assign it on MeshInstance3D/MultiMeshInstance3D, and mutate or generate materials at runtime. Materials decide how surfaces react to light, so this is where you set a prop's color, metalness, roughness, transparency, or hook in a custom shader. It also explains glTF sub-asset addressing so one .glb can supply both mesh and material without unpacking.

Use Cases

  • Skinned character with distinct body parts: surfaces = [...] on MeshInstance3D, one .pmat per surface (body, eyes) plus per-surface overrides like roughness or shade_flat.
  • Shared material across many props: material_load!/material_reserve! returns one MaterialID reused by every instance through the source cache.
  • Damage or team-color flash: material_get_data! then material_write! to swap base_color_factor at runtime.
  • Procedural or downloaded surfaces: material_create! for a transient Material3D::Standard, or material_create_from_bytes! for in-memory .pmat/glTF bytes.
  • Pull a material out of an imported model: res://models/robot.glb:mat[0] on the material field, no separate file.
  • Custom look with live textures: a custom material binding create_from_rgba textures via CustomMaterialImage3D::named_texture, read in-shader with custom_image_sample.

Choice Guide

Reuse one material ID when many nodes share appearance and lifetime. Use node overrides for small per-instance differences. Create or rewrite material data only when appearance truly changes at runtime; duplicating full materials for a color flash increases resource work and hides the authored base material.

Example

Author res://materials/crate.pmat:

type = "standard"

base_color_factor = (0.8, 0.2, 0.2, 1.0)
metallic_factor = 0.1
roughness_factor = 0.7
alpha_mode = "OPAQUE"
double_sided = false

Assign it to a mesh surface in a scene:

[Crate]
    [MeshInstance3D]
        mesh = "res://models/crate.glb:mesh[0]"
        material = "res://materials/crate.pmat"
    [/MeshInstance3D]
[/Crate]

Load it, then dim its roughness at runtime:

let mat = material_load!(res, "res://materials/crate.pmat");
if let Some(mut mat_data) = material_get_data!(res, mat) {
    if let Material3D::Standard(params) = &mut mat_data {
        params.roughness_factor = 0.2;
    }
    let _ = material_write!(res, mat, mat_data);
}

Reference

Materials Guide

This guide shows the normal material path:

  1. Author a .pmat file or use a glTF material.
  2. Load or reserve a MaterialID when scripts need one.
  3. Assign material sources in MeshInstance3D or MultiMeshInstance3D.
  4. Create or mutate material data at runtime when needed.
  5. Let static builds bake supported material sources for release.

For exact file syntax, see .pmat Format. For API details, see Materials Module. For custom shader authoring, see Shaders.

glTF / GLB Sub-Assets

A .gltf or .glb file can hold many resources in one container. Perro treats those inner resources like addressable sub-assets. Use a suffix after the file path to pick the item you want:

res://models/robot.glb:mesh[0]
res://models/robot.glb:mat[0]
res://models/robot.glb:skeleton[0]

This means a model file can act like a small resource folder. You do not need to extract each mesh or material into separate files before using it. Keep the .glb together, then point scene fields or resource APIs at the specific sub-asset.

Common suffixes:

  • :mesh[index] targets a mesh.
  • :mat[index] targets a material.
  • :material[index] also targets a material.
  • :skeleton[index] targets a skeleton/skin.
  • :tex[index], :texture[index], and :img[index] target glTF texture/image data through texture loading.

If a mesh or material source omits the suffix, Perro uses index 0 for that resource type. For textures, prefer .glb or embedded glTF image data; external .gltf image dependency support is not the main documented path.

Author .pmat

type must be the first non-empty entry. Comments can appear above it.

Standard material:

type = "standard"

base_color_factor = (0.8, 0.2, 0.2, 1.0)
metallic_factor = 0.1
roughness_factor = 0.7
alpha_mode = "OPAQUE"
double_sided = false

Unlit material:

type = "unlit"

base_color_factor = (0.2, 0.8, 1.0, 1.0)
emissive_factor = (0.1, 0.2, 0.3)
alpha_mode = "OPAQUE"
double_sided = false

Toon material:

type = "toon"

base_color_factor = (0.4, 1.0, 0.4, 1.0)
band_count = 3
rim_strength = 0.35
outline_width = 0.02
alpha_mode = "OPAQUE"
double_sided = false

Hand-drawn material:

type = "hand_drawn"

base_color_factor = (0.9, 0.8, 0.65, 1.0)
band_count = 4
hatch_scale = 24.0
grain_strength = 0.06

Pixel-surface material:

type = "pixel_surface"

base_color_texture = 0
pixel_count = 32
color_levels = 8
dither_strength = 0.08

Add vertex_modifiers to any preset or custom material for built-in mesh motion and warps. Stacks run in order and support wind, wave, bend, twist, inflate, jitter, and clip-space pixel snapping. See .pmat Vertex Modifiers for syntax and limits.

Custom material:

type = "custom"
shader_path = "res://shaders/custom.wgsl"
# output = "surface" (default) lets Perro add standard lighting
# output = "final" uses exact shader output

params = {
    glow = 1.25
    tint = (1.0, 0.2, 0.4, 1.0)
}

images = {
    mask = "res://textures/mask.png"
    noise = "res://textures/noise.png"
}

Assign In Scenes

Use a .pmat source on material:

[Crate]
    [MeshInstance3D]
        mesh = "res://models/crate.glb:mesh[0]"
        material = "res://materials/crate.pmat"
    [/MeshInstance3D]
[/Crate]

Use a glTF material sub-asset:

[Crate]
    [MeshInstance3D]
        mesh = "res://models/crate.glb:mesh[0]"
        material = "res://models/crate.glb:mat[0]"
    [/MeshInstance3D]
[/Crate]

Use per-surface material sources:

[Robot]
    [MeshInstance3D]
        mesh = "res://models/robot.glb:mesh[0]"
        surfaces = [
            "res://materials/body.pmat",
            {
                material = "res://materials/eyes.pmat"
                modulate = (1.0, 0.9, 0.9, 1.0)
                overrides = [
                    { name = "roughness", value = 0.25 },
                    { name = "shade_flat", value = true }
                ]
            }
        ]
    [/MeshInstance3D]
[/Robot]

Inline materials also work in scenes. String values must be quoted in .scn material objects:

material = {
    type = "standard"
    base_color_factor = (0.8, 0.2, 0.2, 1.0)
    metallic_factor = 0.1
    roughness_factor = 0.7
    alpha_mode = "OPAQUE"
    double_sided = false
}

Load Or Create In Scripts

Use source-backed calls for authored materials:

let mat = material_load!(res, "res://materials/crate.pmat");
let reserved = material_reserve!(res, "res://models/crate.glb:mat[0]");
let _ = material_drop!(res, "res://materials/old.pmat");

Use material_create! for generated or transient materials:

let mat_id = material_create!(
    res,
    Material3D::Standard(StandardMaterial3D {
        base_color_factor: [0.8, 0.2, 0.2, 1.0],
        roughness_factor: 0.4,
        metallic_factor: 0.1,
        ..StandardMaterial3D::default()
    })
);

Use material_get_data! and material_write! to replace an existing material value:

if let Some(mut mat_data) = material_get_data!(res, mat_id) {
    if let Material3D::Standard(params) = &mut mat_data {
        params.roughness_factor = 0.2;
    }
    let _ = material_write!(res, mat_id, mat_data);
}

load and reserve use a source cache. create bypasses the source cache and returns a new material id. get_data returns a copied value. write replaces the full material data for that id.

Imported glTF standard materials keep the source texture channel layout. Metallic-roughness uses green for roughness and blue for metallic. Occlusion uses red. Normal maps use tangent space with normal_scale applied to X/Y. Emissive texture color multiplies emissive_factor. These rules match on regular mesh and dense multimesh draws.

Live Params

Changing a custom shader param does not rebuild anything. Only a shape change -- shader path, images, lighting, or surface -- recompiles a pipeline. Param values are free of that, so they are the intended channel for anything animated.

Use the narrow setters. Do not read the material back to change one value:

// Every surface using this material.
set_material_param!(ctx.res, mat, "glow", 0.7);

// Just this node, still sharing the material.
set_node_material_param!(ctx, MeshInstance3D, id, "glow", 0.7);

Materials().get_data() clones the whole material, including every param, so the get_data -> mutate -> write round trip costs time proportional to the param count -- paid again every frame. The narrow setters send only the changed value and are flat in the param count:

Path 1 param 8 params 32 params 128 params
get_data + write 600 ns 1.24 us 2.64 us 9.18 us
set_param ~200 ns ~200 ns ~200 ns ~200 ns

Per write, measured by cargo bench -p perro_runtime --features bench --bench material_params. At 2048 writes per frame the round trip costs 19.2 ms on a 128-param material -- past a 60 fps frame -- against 0.44 ms for set_param.

Writing the value a param already holds queues nothing, so re-asserting state every frame is free and there is no need to track what changed.

Overrides are per surface, not per instance. A crowd where every instance of one multimesh needs its own value is not covered by either setter yet.

Static Builds

Static builds bake supported material sources into generated lookup data. Release runtime loading can resolve baked .pmat sources and glTF material sub-assets without reparsing the original text path at runtime.

Generic files that do not have static bake support still belong in assets.perro. See Performance + Flexibility Philosophy.

Caveats

  • .pmat type must be the first non-empty entry.
  • .scn inline material strings must be quoted.
  • glTF material refs use res://path/to/model.glb:mat[index].
  • glTF mesh refs use res://path/to/model.glb:mesh[index].
  • a single .glb can provide both mesh and material refs for the same scene node.
  • texture slots are material-local/glTF texture indices, not global texture IDs.
  • custom images bind up to 8 texture sources for custom_image_sample(in, index, uv).
  • use CustomMaterialImage3D::named_texture(name, id) or unnamed_texture(id) to bind mutable textures from create_from_rgba.
  • write_rgba + write_rgba_region invalidate custom image bindings before the next draw.
  • custom materials use output = "surface" by default.
  • set output = "final" for exact shader output.
  • legacy lighting = "raw" remains valid.
  • custom material parameter order binds shader indices: custom_f_param(in, 0u) reads the first param.
  • per-frame param changes belong in set_material_param! / set_node_material_param!, never get_data + write.
  • custom param names are metadata for humans and tooling; order controls shader access.
  • vertex modifiers affect rendering, depth, and shadows; physics and navigation meshes stay unchanged.