---
title: "Material (asset type)"
description: "A material is a named, configured instance of a .shader. The shader defines the surface look (lighting model, available property fields, texture slots); the material picks values for those fields and…"
section: "Types"
slug: "types-material"
canonical: "https://origozero.ai/docs/types-material"
updated: "2026-09-03T19:02:14.944938186+00:00"
tags: ["asset-type", "reference"]
---

# Material (asset type)

Entities sharing one material can still differ in a value: every drawn
object carries four `vec4` lanes of its own that a surface shader reads
as `input.shader_data[lane]` and `renderer.instanceData.set` writes. Use
that for a dissolve at its own progress per subject, a hit flash at its
own age, a fill level per instance — a material per entity is for when
the LOOK differs, not when a number does.

> New to how a material relates to its shader, its textures, and the live GPU
> material behind it? Read `core/resource-model` first. It also covers the
> difference between an authored `.material` asset (this type) and a runtime GPU
> material made with `renderer.material.create`.

## When to use one

- You want a named surface configuration an entity wears by referencing it
  from its `Model` component (`material = "@.../<Name>"`).
- You want runtime property tweaks (`asset.resolve(ref):setProperty(...)`) to
  affect every entity using the material at once.
- You want to publish a curated look (`Gold`, `RedGlow`,
  `BrickWall`) other authors can reuse without re-typing shader
  bindings.

If you need a brand-new lighting model or a custom visual effect, you
need a `.shader` first; the material wraps it.

## Where it lives

- Source: `/zero/source/.../<Name>.material/`
- Identity: `<Name>` (the `.material` suffix strips).
- Folder shape:
  - `mat.yaml` — canonical material spec (shader + property values).
    **Required.**
  - `preview.png` — the material's rendered shader-ball still, its
    persisted visual description. **Required.** Regenerated on every
    `mat.yaml` write; it syncs, publishes, feeds image-based search,
    and gives browsers a thumbnail.
  - `README.md` / `.metadata` — optional prose + tags, indexed when an
    author writes real ones.
  - `textures/` — optional folder for embedded textures. Validator
    allows the folder; its interior is unrestricted.
  - `shaders/` — optional folder for a co-located custom shader.
    Same rule.

## How to create one

```luau
asset.create("material", "<Name>")
-- Creates: /zero/source/<Name>.material/
--   mat.yaml    (shader binding + property scaffold)
--   preview.png (rendered moments later from the mat.yaml write)

-- `folder` places it in a subfolder of /source instead of the root:
asset.create("material", "<Name>", { folder = "materials" })
```

Pass the shader and initial values through `asset.create`, then tune with
`setProperty` and keep what you tuned with `saveDefinition`:

```luau
local mat = asset.create("material", "<Name>", { shader = "@builtin::shaders.pbr" })
mat:setProperty("base_color", { 0.8, 0.6, 0.1, 1 })
mat:setProperty("roughness", 0.3)
mat:saveDefinition()   -- writes the tuned values into mat.yaml
```

This mints the `.material` asset and returns a handle you tune with
`setProperty`. A set is a runtime change: it reaches the GPU material and every
entity wearing it, and `saveDefinition` is the call that writes it into
`mat.yaml`, so a property driven every frame costs no file write and leaves the
authored file as its author wrote it. A purely runtime `asset.create` lives only
for the session.
For an ephemeral, runtime-only GPU material that is not an asset at all, use
`renderer.material.create` instead (see `core/resource-model`).

## How it operates

1. **Property reflection.** The bound shader's uniform struct fields
   become the material's configurable properties — no manual property
   declarations needed. `naga` introspects the shader at load.
2. **Property values.** `mat.yaml` groups values by type, each block
   keyed by the shader's field names: `properties:` holds scalars and
   colors/vectors, `ints:` holds integer-typed fields, `bools:` holds
   boolean-typed fields, and `textures:` holds texture slots. Omitted
   fields take the shader's default. A scalar is a bare number
   (`roughness: 0.5`); a color or vector is an array
   (`base_color: [1, 1, 1, 1]`). `shader:` is the shader's identity
   string. This is the shape every material the engine writes uses. A
   `saveDefinition` writes the changed values into the file where they
   already stand and leaves the rest of it — its comments, the order of
   its keys and blocks, the spelling of its literals — untouched. The older
   `floats:` / `colors:` blocks (with `{ r, g, b, a }` map colors) still
   load for materials hand-authored that way.
3. **Render state.** `render:` holds how the mesh is drawn rather than
   what the shader reads: `blend` (the equation the fragment composites
   with, also spelled `type`), `cull`, `depth_write`, `depth_compare`
   and `topology`. The `.shader` type README's "The render block"
   section lists every value each key takes, and `asset.create`'s
   `render` argument takes the same keys and values.
4. **Application.** An entity wears a material by referencing it from its
   `Model` component (`material = "@.../<Name>"`). Materials affect every entity
   that references them; runtime `asset.resolve(ref):setProperty(...)` changes
   propagate to all of them immediately, and last until the next mode change
   unless `saveDefinition` writes them into `mat.yaml`.
5. **Texture binding.** Texture slots accept `color:r,g,b,a` for
   solid colors, `default:white` / `default:black` / `default:normal` for the
   engine's own fallback texels, `@builtin::textures.<name>` for builtins,
   `path/to/image.png` for file textures, or a render-target handle
   for camera renders. The GPU texture cache reads the `color:` and
   `default:` forms from the string itself, so a slot written in one names no
   asset and carries nothing with the material when it travels; every other
   form names a texture asset the slot depends on.
6. **Hot reload.** Editing `mat.yaml` repacks the GPU uniform buffer
   on the next frame; visible immediately.

## Discovery

- `asset.list("material")` — every registered material.
- `asset.inspect("<name>")` — shader binding, property values,
  source path, this type README.
- `cat /zero/source/<Name>.material` — same summary.
- `asset.resolve("<name>"):getProperties()` — the property fields exposed by
  the bound shader, each at the value it currently holds: what `mat.yaml`
  authored until something writes to the material, and the written value from
  then on, whichever call wrote it. That is the value the surface is drawn
  with. Read them back rather than guessing names; which fields exist comes
  from the shader.
- `asset.resolve("<name>"):getDefinition()` — the authored `mat.yaml` bytes,
  the value the material rests at.
- `asset.resolve("<name>"):getShader()` — the shader the material binds.

## Authoring conventions

- Use lowercase, descriptive names (`brushed_steel`, `red_glow`,
  `tarmac_wet`). Avoid shader-name suffixes in the material name —
  the binding lives in `mat.yaml::shader`.
- Drive every property the shader exposes; defaults are a fallback,
  not a contract for which fields exist.
- Embed textures alongside the material (`textures/` subfolder) when
  they're used only by this material; reference shared textures from
  `@builtin::textures.*` or a library.

## Common pitfalls

- **Property names must match the shader's uniform struct exactly**
  (case-sensitive). Misnamed properties silently get the shader's
  default.
- **Broken shaders break the material.** A material whose shader has a
  parse error renders as the magenta checkerboard. Inspect the shader
  first if the material looks wrong.
- **`setProperty` targets the material, not the entity.** Changes affect
  every entity that references the material.
- **`setProperty` is a runtime change.** It reaches the GPU, not the file:
  call `saveDefinition` to write the current values into `mat.yaml`. For a
  per-entity override that no other entity sees, give the entity its own copy
  with `Model:applySessionMaterial(renderer.material.create(...))`.
- **Renaming the folder changes the identity.** Update every `Model`
  component and `asset.create` callsite that references it.

## Related types

- `.shader` — defines what properties exist; required before a
  material can bind to it.
- `.preset` — captures a component configuration; conceptually
  similar to a material, but for components instead of shaders.
