Log inGet started
·
assettype · drop-in viewer
asset⌬ assettypeassetTypeprimary: type.yaml·originates fromworld 07158574-5…

material.assetType

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 a shader to bind to. Renderable entities reference materials, not shaders, so…

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

Material (asset type)

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 a shader to bind to. Renderable entities reference materials, not shaders, so the same geometry can wear different materials and the same material can be reused across many entities.

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

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:

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.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/asset-type/v1
⌬ Spec
suffix.materialcontainernoplural dirmaterialsrequired filesmat.yaml, preview.pngoptional filesREADME.md, .metadata, textures, shaders
Exposed API
⌬ Instance methods

preview(self: ?, opts: { [string]: any }?)

Render a preview of this material on a unit sphere.

argtypedescription
self?
opts{ [string]: any }?`{ size? = { width, height } }`.

examples

local p = matRef:preview()

inspector(self: ?)

The material's editing surface for a host UI: titled sections of field rows, each carrying its editing kind and the closure that writes the change back. Kinds come from the SHADER's declared vocabulary (its `properties.yaml` descriptors), values from the live cache (the shader's defaults overlaid by this material's overrides). The host renders the rows in its own field language; this never builds widgets. Writes go through `setProperty` / `setTexture` / `setShader`, so every edit takes the same GPU-push + persistence path a scripted write takes.

argtypedescription
self?

examples

for _, section in ipairs(matRef:inspector()) do print(section.title) end

getPropertyNames(self: ?)

List the property names the material exposes (its shader's property vocabulary, from `properties.yaml`). Sorted for stable output.

argtypedescription
self?

examples

local props = matRef:getPropertyNames()

getProperty(self: ?, propertyName: string)

Read one material property at the value it currently holds — what was last written to it, otherwise this material's own `mat.yaml` override, otherwise the shader's declared default. Pure-Luau (cached).

argtypedescription
self?
propertyNamestringProperty name (`"base_color"`, `"roughness"`, …).

examples

local r = matRef:getProperty("roughness")

getProperties(self: ?)

Read every property the material exposes, each at the value it currently holds — the shader's declared defaults (from its `properties.yaml`) overlaid by this material's own `mat.yaml` overrides, overlaid in turn by every value since written to one of those properties, whichever call wrote it. This is the value the surface is drawn with; `getDefinition` returns the authored `mat.yaml` bytes. Pure-Luau after the first build (cached on the ref's runtime).

argtypedescription
self?

examples

for k, v in pairs(matRef:getProperties()) do print(k, v) end

setProperty(self: ?, propertyName: string, value: any) → boolean

The cache carries `props` + `textureSlots`, which is the shape `MaterialSchema.route` reads, so a write resolves its key against the same vocabulary the create path authors by: the friendly spellings (`color` for `base_color`) reach the uniform the shader declares, and a texture — named by a declared slot or recognised by its own value — goes to `setTexture`, which files it in the material's `textures` map (→ GPU texture slot + `textures:` in mat.yaml), the surface the bind group samples and the serializer keeps.

argtypedescription
self?
propertyNamestring
valueany

setProperties(self: ?, patch: { [string]: any }) → number

Set many properties at once, resolving each key against the shader's vocabulary the way `setProperty` does. Entries the material's shader doesn't expose are skipped (a generic patch table won't error on shader differences).

argtypedescription
self?
patch{ [string]: any }Table of `{ [propertyName] = value }` pairs.

examples

matRef:setProperties({ roughness = 0.2, metallic = 0.9 })

setTexture(self: ?, slot: string, textureRef: any)

Bind a texture to one of the material's slots — files it in the live textures map and pushes the slot to the GPU material. Runtime, like `setProperty`: `saveDefinition` is what writes the binding into `mat.yaml`. Accepts a ref string or an `AssetRef<texture>` envelope (its `__ref`/`guid`/`identity` is stored). is no resident GPU key is written and bound as given, and reported as such — the slot renders the shader's declared fallback until something answers to the name.

argtypedescription
self?
slotstringTexture slot name (`"base_color_texture"`, `"normal_texture"`, …).
textureRefanyTexture reference string or AssetRef envelope.

examples

matRef:setTexture("base_color_texture", "@builtin::textures.gold")

getShader(self: ?) → string

Read the shader identity currently bound to this material — parses `mat.yaml` and returns the `shader:` field as a string. The result is whatever the YAML names (a built-in like `"pbr"`, a guid, or a full identity like `"@builtin::shaders.pbr"`); pass it to `asset.resolve(..., "shader")` to get a ref. mat.yaml on disk (runtime-only material).

argtypedescription
self?

examples

local s = matRef:getShader()

setShader(self: ?, shaderRef: any)

Switch this material to a different shader by **rewriting the material asset itself** (`mat.yaml`), then letting the asset-reload pipeline mark every instance dirty so the renderer repacks. The full `mat.yaml` is regenerated for the new shader's property vocabulary: current property values and texture links are carried across by canonical ROLE (via `modules.material_remap`), so `MAIN_TEX`→`albedo`→`base_color_texture` and friends survive the swap to our best ability. Properties the new shader does not expose are dropped; the `name`, `builtin`, `parent`, `description`, and `render:` block are preserved verbatim. This modifies the on-disk asset (or, in play mode, its runtime copy) — not a throwaway registry entry — so the change persists and propagates to all instances. (`"pbr"`, `"@builtin::shaders.unlit"`). dropped, textures }`. On failure: `false, errorString`.

argtypedescription
self?
shaderRefanyAn `AssetRef<shader>` (preferred) or a shader-identity string

examples

matRef:setShader(asset.resolve("@builtin::shaders.unlit", "shader"))
matRef:setShader("pbr")

applyToEntity(self: ?, entityId: string) → boolean

Apply this material to an entity by setting the `material` field on its Model / SkinnedModel component (where the renderer reads it). A material only renders where there is a mesh.

argtypedescription
self?
entityIdstringTarget entity ID.

examples

matRef:applyToEntity(playerId)

getDefinition(self: ?) → string

Read the on-disk `mat.yaml` body as raw text. Use `vfs.write(self.path .. "/mat.yaml", ...)` to write the file directly, `:setProperty` / `:setTexture` followed by `:saveDefinition` to write the current values into it, or `:setShader` to rewrite it for a new shader.

argtypedescription
self?

examples

local yaml = matRef:getDefinition()

isRegistered(self: ?) → boolean

Whether this material exists as an asset — its `mat.yaml` is present. Writing the asset is what registers the material, so file presence IS the registration check.

argtypedescription
self?

examples

if matRef:isRegistered() then ... end

saveDefinition(self: ?) → boolean

Persist the material's live runtime overrides into its `mat.yaml` (the lazy/explicit flush). Property/texture sets are frame-fast and transient by default (they live on the ref's runtime, wiped on mode change); call this to bake the current values into the asset so they survive a restart. Routes through the `mat.yaml` write → `RegisterMaterial` path. The changed values are written where they already stand in the authored file, so its comments, the order of its keys and blocks, and the spelling of the literals it was written with all survive the save.

argtypedescription
self?

examples

matRef:saveDefinition()

handle(self: ?) → void

argtypedescription
self?
⌬ Hooks

onCreate(name: string, opts: CreateOpts, opts.shader: ?, opts.textures: ?, opts.properties: ?, opts.bindings: ?, opts.render: ?)

Generic-creation hook for `asset.create("material", name, opts)`. Pure: returns the `mat.yaml` content for the caller to persist to the authored destination; the write registers the material definition (CPU-side) — GPU resources are built later, only on actual use. Properties resolve from the shader's `properties.yaml` overlaid by these overrides; no shader compile happens at create. Flat non-reserved top-level keys on opts are shader-property overrides (e.g. `base_color = {1,0,0,1}`, `roughness = 0.3`), admitted by the open schema and resolved against the shader's properties.yaml inside the hook. default opts.shader "pbr" reads. `blend`, also spelled `type` the way a `mat.yaml` render block writes it ("opaque" | "transparent" | "alpha" | "alphaBlend" | "additive" | "add" | "cutout" | "alphaCutout" | "premultiplied" | "premultipliedAlpha" | "multiply"), `cull` ("back" | "front" | "none" | "off" | "disabled"), `topology` ("triangleList" | "lineList" | "lines" | "line"), `depth_compare` ("less" | "lessEqual" | "lequal" | "equal" | "greater" | "greaterEqual" | "gequal" | "always" | "never"), `depth_write` and `instancing` (booleans), `queue` (number). An unknown key or value is refused rather than resolved to the default. `premultiplied` composites `src + dst * (1 - src.a)`, taking a source that already carries its own coverage; `multiply` composites `src * dst`, which is how a soot, grime or shadow card darkens what stands behind it.

argtypedescription
namestringMaterial identity (the instance name).
optsCreateOpts
opts.shader?Which shader backs this material.
opts.textures?Texture slot bindings: maps slot names to string asset refs.
opts.properties?Nested property overrides (merged with flat top-level keys).
opts.bindings?Group-2 storage-buffer bindings: array of `{ group = 2, binding = N, kind = "storage", buffer = "<compute buffer name>" }`.
opts.render?Render state — how the mesh is drawn, rather than what the shader

examples

asset.create("material", "Gold", { shader = "pbr", base_color = {1, 0.84, 0, 1}, metallic = 1, roughness = 0.2 })
asset.create("material", "Glass", { shader = "pbr", base_color = {0.2, 0.9, 0.4, 0.45}, render = { blend = "alphaBlend", depth_write = false } })

onChange(v: any) → void

argtypedescription
vany

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

3items
This part has no composite children. See the Files segment for its leaf payloads.
backing path · assetTypes/material.assetType

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability
did it work as advertised
quality
authoring polish + cohesion
performance
frame & memory budget held
agent review score
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.