| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| Example | Example |
| Reference | Reference |
| Live Params | Live Params |
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.
- Skinned character with distinct body parts:
surfaces = [...]onMeshInstance3D, one.pmatper surface (body, eyes) plus per-surfaceoverrideslikeroughnessorshade_flat. - Shared material across many props:
material_load!/material_reserve!returns oneMaterialIDreused by every instance through the source cache. - Damage or team-color flash:
material_get_data!thenmaterial_write!to swapbase_color_factorat runtime. - Procedural or downloaded surfaces:
material_create!for a transientMaterial3D::Standard, ormaterial_create_from_bytes!for in-memory.pmat/glTF bytes. - Pull a material out of an imported model:
res://models/robot.glb:mat[0]on thematerialfield, no separate file. - Custom look with live textures: a
custommaterial bindingcreate_from_rgbatextures viaCustomMaterialImage3D::named_texture, read in-shader withcustom_image_sample.
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.
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 = falseAssign 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);
}This guide shows the normal material path:
- Author a
.pmatfile or use a glTF material. - Load or reserve a
MaterialIDwhen scripts need one. - Assign material sources in
MeshInstance3DorMultiMeshInstance3D. - Create or mutate material data at runtime when needed.
- 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.
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.
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 = falseUnlit 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 = falseToon 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 = falseHand-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.06Pixel-surface material:
type = "pixel_surface"
base_color_texture = 0
pixel_count = 32
color_levels = 8
dither_strength = 0.08Add 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"
}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
}
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.
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 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.
.pmattypemust be the first non-empty entry..scninline 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
.glbcan provide both mesh and material refs for the same scene node. - texture slots are material-local/glTF texture indices, not global texture IDs.
- custom
imagesbind up to 8 texture sources forcustom_image_sample(in, index, uv). - use
CustomMaterialImage3D::named_texture(name, id)orunnamed_texture(id)to bind mutable textures fromcreate_from_rgba. write_rgba+write_rgba_regioninvalidate 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!, neverget_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.