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

shader.assetType

A shader is a WGSL program that runs on the GPU. Materials reference shaders and supply property values; renderable entities reference materials, not shaders directly. This README is the canonical reference for writing **every** kind of shader — it covers all domains, the surface…

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

Shader (asset type)

A shader is a WGSL program that runs on the GPU. Materials reference shaders and supply property values; renderable entities reference materials, not shaders directly. This README is the canonical reference for writing every kind of shader — it covers all domains, the surface authoring contract, the material property schema, the engine library functions you can call, and how a shader registers and compiles.

New to how shaders, materials, meshes, and textures relate as assets and live GPU resources? Read core/resource-model first.

Folder shape

A .shader is a folder asset:

<name>.shader/
  shader.wgsl        # the WGSL body (required)
  properties.yaml    # the material property schema (optional; surface shaders)
  README.md          # a one-paragraph description of THIS shader (required)
  .metadata          # asset metadata

Each file carries a committed .meta sidecar pinning its stable guid. The identity is <name> (the .shader suffix strips).

A shader's material properties are exactly the ones its properties.yaml declares: a shader with no properties.yaml declares none, and the engine reads no property from the WGSL body. A .wgsl file standing on its own, outside a .shader folder, is WGSL source rather than a shader: a material draws with the .shader folder that holds it as shader.wgsl. Materials and other assets reference a shader by AssetRef (a typed, guid-backed handle), never by a raw string.

Domains

The first line of shader.wgsl declares the domain:

// @domain: surface
DomainWhat it isYou write
surfaceA material on a mesh (the common case).fragment() (+ optional vertex()/vertex_clip()). The engine generates everything else.
post-processA full-screen pass over the rendered frame, in the space it declares (bloom, vignette, grade).fragment(in: PostInput) -> vec4 only. The engine generates the fullscreen vs_main/fs_main and the group(1) interface from properties.yaml.
screenA standalone full-screen visual (UI background, procedural).fragment(in: ScreenInput) -> vec4 only. The engine generates the fullscreen vs_main/fs_main and the group(1) interface from properties.yaml.
skyThe sky background (a fullscreen material).fragment(in: SkyInput) -> vec4 only. The engine generates the fullscreen vs_main/fs_main and the group(1) interface from properties.yaml.
computeGPU data processing, no rasterization.An @compute @workgroup_size(...) entry over storage buffers; you write the @group/@binding lines yourself. For compute work, the .computeShader assetType (see below) generates them for you.

// @domain: resolves the names above and postprocess / screenspace / ui as further spellings of post-process and screen. A body carrying no @domain: line is surface. shaderRef:domain() answers the domain a body declares, a further spelling as the domain it names. Any other name is refused where the shader registers, with an error naming the spellings that resolve, so a shader reaches the renderer only under a domain the compiler scaffolds.

Particles are drawn and simulated through these domains like anything else: the particle package's particle_sprite and particle_mesh are surface shaders, and its simulation, culling and sorting steps are .computeShader assets.

Texture coordinate space

A shader that samples a texture through a coordinate it builds itself declares which space that coordinate lives in, on a line beside @domain::

// @domain: surface
// @uv_space: world
ValueThe coordinateA material's uv_scale means
mesh (the default)The mesh's own UV attribute, input.uv.How many times the texture repeats across a surface.
worldThe fragment's world position, as a triplanar projection samples.How many times the texture repeats per world unit, so a larger surface carries more.

Texture residency reads the declaration to work out how finely a frame samples each texture: a material laying eight copies of its texture across a surface samples each copy at an eighth of the surface's size, and the mip level that serves it is three levels coarser than the surface's own footprint implies. Leaving the line out puts the shader in mesh space, which is what sampling input.uv is. renderer.textureStreaming().textures reads back the span each texture was measured at and the level that span asked for.

Compute shaders have their own assetType. A .computeShader/ folder gives compute the same zero-scaffolding contract surface/sky/post/screen get: you write only @compute fn main (+ helper fns/structs, no @group/@binding lines) and declare bindings in a sibling bindings.yaml (storage buffers, textures, samplers, storage textures, plus a params: uniform); the engine generates the @group(0)/@binding(N) declarations. Dispatch it through a resolved reference (asset.resolve("name", "computeShader"):dispatch(...)). See the .computeShader assetType README and man compute.

Surface, sky, post-process, and screen shaders all get engine-generated scaffolding — you write only fragment() and declare properties in properties.yaml; the engine generates the entry points and the group(1) material interface. (Each domain's fragment takes a domain-specific input instead of FragmentData: SkyInput, PostInput, or ScreenInput — see "Sky shaders" and "Post-process & screen shaders" below.) Compute shaders get the same treatment via the separate .computeShader assetType (write only @compute fn main, declare bindings in bindings.yaml). The compute domain here writes its own entry points directly — see man compute.

Surface shaders

A surface shader writes only the parts that are unique to it. The engine owns all the scaffolding — the vertex stage, every render-mode entry point (vs_main / fs_forward / fs_gbuffer), group(0) (camera, transforms, lights, shadows, and the material rows), and the material interface generated from properties.yaml (material and your texture slots). Adding a render path never touches your shader.

Inside fragment() you have access to exactly four things — nothing is hidden:

  1. input: FragmentData — the interpolated per-fragment surface data the engine hands you: world position, world normal, uv, view direction, vertex colour, and more. Every field is in the FragmentData table below.
  2. material.* and your textures — the material inputs you declare in properties.yaml. The mapping is mechanical: a scalar property named foo becomes material.foo; a texture property named bar becomes bar (the texture) and bar_sampler (its sampler). material has exactly the fields your properties.yaml declares and no others — to add an input, add a property. List what a shader exposes with shaderRef:getProperties() or asset.inspect("<name>").
  3. Engine state — the scene's camera, lights, time, and shadows that the renderer binds for every surface shader. uniforms.* (sun direction/colour, ambient, camera_pos, time) and the punctual lights (zero_point_light(i)) are always in scope; the shadow functions come from #include "@builtin::shaderModules.shadows". This is how you light a surface without the prebuilt PBR — see "Engine state" and "Writing your own shading" below.
  4. Library functions you #include (see "Library includes").

A shader and its property schema go together. The two files below are a complete surface shader:

properties.yaml — declares the material inputs:

properties:
  - { name: base_color, type: color, default: [1, 1, 1, 1] }
  - { name: roughness,  type: float, default: 0.5, min: 0, max: 1 }
  - { name: metallic,   type: float, default: 0.0, min: 0, max: 1 }
  - { name: base_color_texture, type: texture, default: white }

shader.wgsl; those declarations are read as material.base_color, material.roughness, material.metallic, and the base_color_texture / base_color_texture_sampler pair:

// @domain: surface
#include "@builtin::shaderModules.pbr_shading"   // the PBR helpers — see "Library includes"

fn fragment(input: FragmentData) -> vec4<f32> {
    var s = zero_pbr_default_surface(input);
    s.albedo = material.base_color.rgb
        * textureSample(base_color_texture, base_color_texture_sampler, input.uv).rgb;
    s.perceptual_roughness = material.roughness;
    s.metallic = material.metallic;
    return vec4<f32>(zero_pbr_shade(input, s), material.base_color.a);
}

fragment() returns the final colour. The engine forces no lighting model on you — flat, toon, PS1, dithered, or PBR are all just different fragment() bodies. Calling zero_pbr_shade is opt-in.

The two surface bodies, and what each gives up

A surface shader exposes one of two entry points, and which one decides how much of the shader the engine can see:

BodyThe engine getsPBR debug passes read
fn fragment(input: FragmentData) -> vec4<f32>the finished pixel, and the PbrSurface too when the body hands one to zero_pbr_shadethe real channel where a surface was handed over; magenta, the "no data here" marker, where the body shades by its own math
fn surface(input: FragmentData) -> PbrSurfacethe surface (albedo, roughness, metallic, emissive, normal, occlusion), which the engine then lightsthe real channel

Both are first-class: fragment() is what you want when the shader owns its own look, surface() when you want engine lighting. capture { pass = "albedo" } (and roughness / metallic / ao / emissive) answers about your material for either body wherever the engine has the surface — writing the same PbrSurface behind surface() or behind fragment() + zero_pbr_shade gives one answer per channel. A body that lights itself by its own math draws those channels magenta. pbr.shader exposes surface().

capture { pass = "normal" } is a geometry read rather than a material one, so it answers for every shader: the normal of the surface where one was handed over, and the interpolated normal of the triangle where none was. It is the same normal the frame's normal buffer carries, which is what a screen-space pass reading @scene.normal sees.

shaderRef:shadingModel() reports which one a shader ended up with — "engine-lit" for surface(), "self-shading" for fragment().

Optional vertex hooks

// Displace geometry in OBJECT space. Runs on skinned meshes too (it sees the
// already-skinned, morph-applied local position). Return the modified vertex.
// The `v` parameter is immutable (WGSL function params are), so copy it into a
// `var` first and modify that.
fn vertex(v: VertexData) -> VertexData {
    var out = v;
    out.position += out.normal * sin(out.time) * 0.1;
    return out;
}

// Post-transform vertex hook (runs AFTER the engine's world/skin/projection). Two
// things you can do here, separately or together:
//   * move `clip_position` — vertex jitter, PS1 snapping, render-resolution lowering;
//   * write `color` — PER-VERTEX (Gouraud) shading: light the vertex from its
//     world_normal + the light context and the engine forwards the result into the
//     interpolated `vertex_color` the fragment reads.
fn vertex_clip(v: ClipVertex) -> ClipVertex {
    var out = v;
    // Gouraud: Lambert sun + ambient, evaluated once per vertex (flat-banded shading).
    let n_dot_l = max(dot(v.world_normal, -normalize(v.light_direction)), 0.0);
    let lit = v.ambient_color * v.ambient_intensity + v.light_color * v.light_intensity * n_dot_l;
    out.color = vec4<f32>(v.color.rgb * lit, v.color.a);
    out.clip_position = round(out.clip_position * 80.0) / 80.0;  // PS1 snap
    return out;
}

Declaring how far a hook moves geometry: vertexBoundsDilation

Static geometry is culled per cluster against its bounds as authored. A hook that moves vertices — wind sway, an outline hull, a vertex wave — can move them outside those bounds, and the cull then drops clusters whose displaced triangles are still on screen. Declare the farthest the hook moves any vertex, in world units, as a vertexBoundsDilation float property, with a default that covers the shader's own properties across their ranges:

properties:
  - { name: sway, type: float, default: 0.5, min: 0, max: 4 }
  # 0.4 x sway, at sway's largest value.
  - { name: vertexBoundsDilation, type: float, default: 1.6, min: 0, max: 100 }

Each material sets its own value — a material that sways less can set less, one moved farther sets more — and a material that sets none takes the default. The cull adds the material's value to every bound in its frustum and occlusion tests (not to the level-of-detail cut). A shader with a hook and no vertexBoundsDilation property is named once in the engine log when it compiles; its geometry is culled against its undisplaced bounds.

Structs the engine passes you

FragmentData (input to fragment()):

FieldTypeMeaning
world_positionvec3<f32>fragment world-space position
world_normalvec3<f32>interpolated geometric normal (normalized)
world_tangentvec4<f32>xyz = world tangent, w = handedness (mikktspace)
uvvec2<f32>primary texture coordinates (perspective-correct)
affine_uvvec2<f32>the same UVs interpolated without perspective correction — the PS1 texture "swim". Always available; sample with it instead of uv for affine texture mapping
vertex_colorvec4<f32>interpolated per-vertex colour (white if unset; carries per-vertex lighting if vertex_clip() wrote it — see Gouraud below)
view_positionvec3<f32>camera world position
view_directionvec3<f32>normalized world-space direction to the camera
light_directionvec3<f32>directional (sun) travel direction, unit length; surface→sun is -light_direction. Where no sun reaches the camera it points straight down
light_colorvec3<f32>sun colour
light_intensityf32sun intensity; 0 where no sun reaches the camera
ambient_colorvec3<f32>ambient colour
ambient_intensityf32ambient intensity
frag_coordvec4<f32>framebuffer pixel coords (xy) + depth (z) — for screen-space dither
receives_shadowsf32per-instance: > 0.5 if this surface receives shadows
material_indexu32per-instance: the index standing for the material this surface draws with — the number renderer.materialIdentity() maps to a name
shader_dataarray<vec4<f32>, 4>per-instance: this entity's own four vec4 lanes — its ecs.ShaderData component's lane0..lane3, or what renderer.instanceData.set put in them (zero until something does). A procedural graph's elements set them through the data0..data3 attributes
front_facingbooltrue for front faces (double-sided shading)
timef32gameplay seconds: advances with the world at engine.timeScale and stands still while gameplay is paused (edit mode starts paused), so an effect slows and stops with the world

shader_data is the per-instance channel, and it is what lets ONE material serve many entities that differ in a value — a dissolve at its own progress per subject, a hit flash at its own age, a fill level per instance. The alternative is a material per entity, which is a separate pipeline binding and a separate row in the material table for every one of them.

const DISSOLVE_LANE: u32 = 0u;

fn fragment(input: FragmentData) -> vec4<f32> {
    let progress = input.shader_data[DISSOLVE_LANE].x;
    ...
}
-- one material on twenty entities, each at its own progress
renderer.instanceData.set(subject, 0, progress)
renderer.instanceData.clear(subject)     -- back to zero lanes

The engine attaches no meaning to a lane: a shader and whatever writes it agree what each one carries. Name the indices in the module that writes them. renderer.instanceData.laneCount() is how many there are. The same lanes reach a hardware-instanced population through renderer.mesh.drawInstanced's instanceDataBuffer, on slots no entity owns.

VertexData (in/out of vertex()): position (object-local, model-scale + morph applied), normal, uv, color, time.

ClipVertex (in/out of vertex_clip()) — the post-transform vertex. Read context: clip_position, world_position, world_normal, uv, view_position, view_direction, light_direction, light_color, light_intensity, ambient_color, ambient_intensity, time, view_proj (the view-projection of the frame being evaluated), and where the vertex stands on its instance: local_position (its position in the mesh, after vertex() and before the instance's world transform), object_origin (the world position of the instance's origin), object_scale (the world length of the instance's x, y and z axes) and object_axes (mat3x3<f32>, the world direction of those axes, unit length) — what a camera-facing card, a shard grown about its own base, or anything placed per instance is built from. Writable: clip_position (snap / jitter) and color (write a lit colour here for per-vertex Gouraud shading; the engine forwards it to the interpolated vertex_color — left untouched, the mesh's own vertex colour passes through).

vertex() and vertex_clip() run in the vertex stage, which reads the frame uniforms (uniforms.*, and the ClipVertex members built from them), the scene buffer (zero_object_data, zero_object_visuals, the node transforms), the material interface and a read buffer property. The point, spot and area lights (zero_point_light, zero_punctual_projection), the shadow maps, the environment and light probes, the feature textures, the scene depth, colour and reflection copies and a @group(2) buffer the shader declares read_write are visible to the fragment stage alone: read them in fragment() or surface(). A shader whose vertex hook reaches one fails to compile, and the error names the stage, the binding and the calls that reach it.

Each hook runs twice: this frame and the one before it

A vertex hook decides where the surface IS, so it also decides where the surface WAS. The engine runs every hook a shader declares a second time, over the same vertex standing one frame back — the previous-frame position stream, with time = the clock that frame was drawn at and (for vertex_clip()) view_proj = the view-projection it was drawn with. The motion vector the renderer publishes is taken between the two results, so a displacement that moves with time reports the velocity of the displaced surface, and a displacement that does not move reports the motion of the transform alone because the identical offset appears on both evaluations and cancels.

What that asks of a hook:

  • Take the frame from the argument. v.time is the clock of the evaluation being run; uniforms.time is always this frame's. A displacement built on uniforms.time is identical on both evaluations, so its motion vector cancels to exactly zero. The same holds for v.view_proj against uniforms.view_proj: a clip hook that rebuilds its position from world space through uniforms.view_proj reprojects the previous frame with this frame's camera, and reports no camera motion at all. The wind helpers come in both shapes for this reason — zero_wind_sway_at_time(world_pos, stiffness, v.time) inside a hook, zero_wind_sway(world_pos, stiffness) outside one.
  • Anchor a wrap to uniforms.time. A displacement that jumps discontinuously — a falling drop that restarts at the top, a looping offset — differences its jump as velocity on whichever frame it wraps. Compute the wrap COUNT from uniforms.time and the position within the cycle from v.time, and both evaluations share the same cycle, so the surface reports the speed it fell at. @builtin::shaders.rain_material and snow_material do exactly that.

The per-vertex attributes (world_normal, uv, color) and the lighting and camera-position context are this frame's on both evaluations, so a hook that displaces along its normal reports the motion of the transform plus this frame's displacement.

Engine state — point lights & shadows

The common lighting state — the sun (input.light_direction / light_color / light_intensity), the ambient (input.ambient_color / ambient_intensity), the camera (input.view_position / view_direction), and input.time — is already on FragmentData, so most shaders never touch the raw engine state. The one thing not on FragmentData is the punctual-light array (its length varies), which is always in scope:

ExpressionTypeMeaning
uniforms.point_light_countu32number of active point/spot lights
zero_point_light(i)PointLightDataone point or spot light (fields below)
zero_punctual_projection(i)PunctualProjectiona spot or area light's projection: view_proj, the frustum its cookie and shadow are placed by, and cookie_layer

PointLightData: position, radius, color, intensity, direction, cone_outer_cos, cone_inner_cos, kind (0 = point, 1 = spot).

Shadows come from #include "@builtin::shaderModules.shadows":

  • compute_directional_shadow_factor(world_pos, normal, light_dir) -> f32 — 1.0 lit, 0.0 fully shadowed.
  • compute_spot_shadow_factor(...) / compute_point_shadow_factor(...) / compute_area_shadow_factor(...) — one shadow shape each.
  • compute_punctual_shadow_factor(light_index, world_pos, normal) -> f32 — the row's own shape, read off its kind, for a loop that walks the punctual lights and does not want the switch.
  • zshadow_light_reach(light_index, world_pos, normal, object_channels) -> f32 — how much direct light that row puts on a surface on those light channels: the channel mask, falloff, cone or rect orientation, and the cosine, times intensity. Zero exactly where the light contributes nothing.

Writing your own shading

The engine forces no lighting model — zero_pbr_shade is just one option. You can light a surface yourself from the state above. Here is a complete custom-lit (Lambert diffuse) surface with no PBR:

// @domain: surface
#include "@builtin::shaderModules.shadows"   // exposes compute_directional_shadow_factor

fn fragment(input: FragmentData) -> vec4<f32> {
    let n = normalize(input.world_normal);
    let albedo = material.base_color.rgb;

    // Directional (sun) light: Lambert N·L, attenuated by its shadow.
    let sun = normalize(-input.light_direction);          // surface -> sun
    let sun_shadow = compute_directional_shadow_factor(input.world_position, n, input.light_direction);
    var lit = input.light_color * input.light_intensity * max(dot(n, sun), 0.0) * sun_shadow;

    // Point + spot lights (the punctual-light array, varying length).
    for (var i = 0u; i < uniforms.point_light_count; i = i + 1u) {
        let pl = zero_point_light(i);
        let to_light = pl.position - input.world_position;
        let dist = length(to_light);
        let l = to_light / max(dist, 1e-4);
        let atten = max(1.0 - dist / max(pl.radius, 1e-4), 0.0);
        lit += pl.color * pl.intensity * max(dot(n, l), 0.0) * atten;
    }

    // Ambient fill.
    lit += input.ambient_color * input.ambient_intensity;

    return vec4<f32>(albedo * lit, material.base_color.a);
}
  • Unlit: skip the lights and return your colour directly.
  • Toon / PS1 / custom: start from the loop above and band, quantize, or warp it however you like.
  • PBR: build a PbrSurface and call zero_pbr_shade (see "Library includes") instead of writing the BRDF by hand.

Material properties — properties.yaml

A surface shader declares its material properties in a sibling properties.yaml. The engine GENERATES the entire group(1) interface from it — the uniform struct (material.<name>) plus a texture/sampler pair per declared texture. You write no @group/@binding lines.

properties:
  - { name: base_color, type: color, default: [1, 1, 1, 1] }
  - { name: roughness,  type: float, default: 0.5, min: 0, max: 1 }
  - { name: metallic,   type: float, default: 0.0, min: 0, max: 1 }
  - { name: emissive,   type: color, default: [0, 0, 0, 1] }
  - { name: base_color_texture, type: texture, default: white }
  - { name: normal_texture,     type: texture, default: flat_normal }
typeWGSL field typeNotes
floatf32optional min/max for editor sliders
float2/float3/float4vec2/3/4<f32>
colorvec4<f32>sRGB colour, default is [r,g,b,a]
inti32
boolu32
texturetexture_2d<f32> + samplerdefault: is a built-in texture name (white, black, flat_normal)

In the shader you read scalars as material.<name> and sample textures as textureSample(<name>, <name>_sampler, uv). A texture slot with no bound texture samples its default, so a shader can always sample — no presence flag needed.

vertex() reads the same material interface, so a height or deformation map drives displacement from the slot it was bound to. A vertex stage has no implicit derivatives, so sample it at an explicit mip there: textureSampleLevel(<name>, <name>_sampler, uv, 0.0).

Sky shaders

A // @domain: sky shader draws the sky as a fullscreen pass. You write only fn fragment(in: SkyInput) -> vec4<f32> — the colour along the view ray for each pixel — and declare tunables in properties.yaml exactly like a surface shader (read as material.<name>, textures via textureSample(<name>, <name>_sampler, uv)). The engine generates the fullscreen vs_main/fs_main, the group(1) interface, and reconstructs the world-space ray; there is no separate sky renderer — a sky material runs through the same material pipeline, just on a fullscreen triangle. A sky pipeline binds group(0) and the group(1) interface alone, so a sky that reads a buffer property or any other @group(2) binding fails to compile, and the error names the binding and the calls that reach it.

SkyInput (the only argument) carries:

FieldTypeMeaning
ray_dirvec3<f32>normalized world-space ray through this pixel
ndcvec2<f32>screen position in NDC [-1, 1]
sun_directionvec3<f32>unit vector TOWARD the sun (from the directional light)
camera_posvec3<f32>world-space camera position
timef32gameplay seconds: advances with the world at engine.timeScale and stands still while gameplay is paused (edit mode starts paused), so an effect slows and stops with the world
// @domain: sky
fn fragment(in: SkyInput) -> vec4<f32> {
    let up = max(in.ray_dir.y, 0.0);
    let col = mix(material.horizon_color.rgb, material.zenith_color.rgb, up);
    return vec4<f32>(col * material.exposure, 1.0);
}

Day/night comes from sun_direction.y (the directional light's elevation), so a sky shader never needs a time-of-day uniform — orient the light and the sky follows. The builtin sky shaders (procedural_sky, solid_sky, cubemap_sky, equirect_sky) are all authored this way; author a custom sky material and point a Skybox component at it.

Post-process & screen shaders

// @domain: post-process and // @domain: screen shaders are authored exactly like surface and sky — you write only fn fragment(in: PostInput) -> vec4<f32> (post) or fn fragment(in: ScreenInput) -> vec4<f32> (screen) and declare tunables in properties.yaml (read as material.<name>, textures via textureSample(<name>, <name>_sampler, uv)). The engine generates the fullscreen vs_main/fs_main and the group(1) material interface. There are no uniforms.params[i] slots — that convention is retired; every input is a named property.

A post-process shader's properties.yaml carries one further, top-level key beside properties: — space, either light (the default) or display:

space: display
properties:
  - { name: amount, type: float, default: 1.0, min: 0, max: 1 }

light is the scene's linear radiance — what every surface, the sky and every afterLighting feature write, unbounded above 1.0. display is the code values the __display__ pass leaves behind after applying exposure and its tone curve — the 0-1 range a colourist's tool and a dithered or quantized effect expect. postprocess.add's opts.space overrides what the asset declares for one registration. The chain converts at both ends of an effect that runs on a segment in the other space, so a light-space vignette still darkens a frame graded after __display__, and a lookup table built from a .cube export always sees the code values it was authored against, wherever the chain runs it.

space also decides which layer a registration that names none lands on: a light shader grades scene, the segment holding the scene's own light, and a display shader grades all, the picture after __display__ with the UI on it. So postprocess.add("glow", asset.resolve("~.shaders.bloom", "shader")) thresholds highlights that still stand above 1.0, and a lookup table registered the same way reads the code values it was built for. opts.layer names one explicitly — "all" is what an effect that must grade the interface says.

A post-process shader's properties.yaml carries a further, optional top-level key — passes:, the pass list the frame draws through, in order:

space: light
passes:
  - { name: bright,    entry: bright,     scale: 0.5,    inputs: [scene] }
  - { name: down1,     entry: downsample, scale: 0.25,   inputs: [bright] }
  - { name: up0,       entry: upsample,   scale: 0.5,    inputs: [down1, bright] }
  - { name: composite, entry: composite,                 inputs: [scene, up0] }

Every pass's entry — fn <entry>(in: PostInput) -> vec4<f32> — lives in the one shader.wgsl the assetType recompiles on. scale is the fraction of the view a pass draws at; the LAST pass takes none, since it draws at the view's size and writes the chain's output. inputs names what a pass reads — "scene", the chain's input for this effect, or the name of an EARLIER pass — and inputs[0] is what zero_sample_scene(uv) reads inside that pass's body; every input, inputs[0] included, is also reachable by index through zero_pass_input(i, uv) and zero_pass_input_size(i), below. The one place the two reads of inputs[0] disagree: in an effect whose declared space differs from the segment it draws on, zero_sample_scene converts a scene input into that space while zero_pass_input(0, uv) hands back the segment's own, unconverted value. A shader declaring no passes: is one pass whose entry is fragment, and its generated WGSL is byte-identical to what it is without the key.

A pass that is not the last writes a scratch target the effect owns — in the space the shader declared, view-sized times its own scale — allocated on the first draw and replaced whenever the size changes. engine.params, below, carries the drawing pass's own index, its scale, and its first input's texel size, so one shader body serves every level of a chain built this way.

The worked example is the bloom: @builtin::shaders.post.bloom declares twelve passes — a threshold pass, five downsamples, five tent upsamples and a composite — out of four entries, since the five downsamples share one and the five upsamples share another, each level reading the one below it through zero_pass_input and telling itself apart by engine.params. Its own README walks the declaration.

A declaration is refused at compile time — reported through compileStatus(), postprocess.add's second return value, or a live effect's status()[i].error — for: an entry passes: names that shader.wgsl does not define; an input naming a later pass, or naming neither scene nor an earlier pass; a scale outside (0, 1]; a scale declared on the last pass; a buffer property (group(2) carries the pass inputs); or a shader whose group(0) plus declared texture slots plus widest pass's inputs ask for more than the 16 FRAGMENT sampled textures one shader stage has.

A post-process shader runs as a full-screen pass over the already-rendered frame. Its fragment argument and framework helpers:

PostInput:

FieldTypeMeaning
frag_coordvec4<f32>framebuffer pixel coords (@builtin(position))
uvvec2<f32>screen UV, (0,0) top-left … (1,1) bottom-right

Framework helpers (always in scope — no include needed):

CallReturnsMeaning
zero_sample_scene(uv)vec4<f32>the rendered scene colour at uv, converted into the space this effect declared (zero_scene_light is the same call, for content that reads it by that name)
zero_scene_depth(uv: vec2<f32>)f32scene depth at uv — a screen UV here, where the surface domain's zero::scene_depth module takes a pixel coordinate under the same name (exact texel — an R32Float copy). Reversed: 1.0 is the near plane, 0.0 the far plane and empty sky, so a nearer surface reads GREATER and d > 0.0 means "a surface was drawn here"
zero_linear_depth(d, near, far)f32that sample as a distance in metres — the one inversion of the reversed range
zero_scene_motion(uv)vec2<f32>per-pixel screen-space motion vector at uv
zero_pass_input(i, uv)vec4<f32>the pass's declared input i, at uv. Input 0 is what zero_sample_scene reads
zero_pass_input_size(i)vec2<f32>that input's size in texels, so one entry serves every level of a chain

Engine data uniform engine (a PostEngineData):

FieldTypeMeaning
resolutionvec4<f32>x=width, y=height, z=1/width, w=1/height
timevec4<f32>x=elapsed seconds, y=dt, z=frame index
view_projmat4x4<f32>this frame's view-projection
prev_view_projmat4x4<f32>last frame's view-projection (motion)
inv_view_projmat4x4<f32>inverse view-projection (world-space reconstruction)
paramsvec4<f32>x=this pass's own index, y=the scale it draws at, zw=1/size of its first input — for a pass the post-process chain draws. A pass a renderFeature fragment pass draws carries that feature's spec.params here instead; a shader declaring passes: is always drawn by the chain, so the two never meet in one shader.

The engine fills all of it per render target. A registered effect runs over the live viewport's frame AND over every offscreen one — a capture from a world-space station or orbiting an entity, a capture of a named camera, a render-to-texture camera — and in each of those, view_proj, prev_view_proj and inv_view_proj are the camera THAT render was drawn from, while resolution is that target's own pixel size. So a pass reconstructing world space from zero_scene_depth(uv) reconstructs against the station and lens the capture asked for, and an offscreen frame is a picture of the effect as it stands at that station. (A render feature's passes reach an offscreen render the same way, against the same camera — the renderFeature assetType README carries that half, including @frame.camera, its own per-target camera input.)

An offscreen render keeps no view history of its own, so prev_view_proj there holds that same matrix rather than the frame before it, and a pass taking camera motion from the two reads none. On the viewport the pair is a frame apart, so camera motion is the one reading a capture answers differently from the screen.

postProcessing = false on a capture is the one control that takes the chain off the frame it returns; it names the camera property the ephemeral capture camera is built with, so it scopes to that one capture. What a capture opts out of is the registered effects; __display__ still runs on it, so the exposure and tone curve that turn the scene's light into a picture are on what a capture returns. A diagnostic capture (albedo, normal, motion_vectors, …) bypasses __display__ itself, since a channel like that is a reading of the scene rather than a graded picture of it. The environment-cube capture bypasses it too, and for the same reason: a probe face carries radiance into the cube array, and __display__ would fit that radiance to a display's code values — a picture of it rather than the value the PBR indirect term multiplies into its lighting.

A complete post-process effect — a vignette via fragment() + properties.yaml:

properties.yaml:

properties:
  - { name: intensity, type: float, default: 0.5,  min: 0, max: 1 }
  - { name: radius,    type: float, default: 0.75, min: 0, max: 1 }
  - { name: softness,  type: float, default: 0.45, min: 0, max: 1 }

shader.wgsl:

// @domain: post-process
fn fragment(in: PostInput) -> vec4<f32> {
    let color = zero_sample_scene(in.uv);
    let intensity = max(material.intensity, 0.01);
    let radius = max(material.radius, 0.01);
    let softness = max(material.softness, 0.01);

    let center = in.uv - vec2<f32>(0.5);
    let dist = length(center);
    let vignette = smoothstep(radius, radius - softness, dist);
    let darkened = color.rgb * mix(vec3<f32>(1.0), vec3<f32>(vignette), intensity);
    return vec4<f32>(darkened, color.a);
}

The asset above defines the effect. It enters the frame when something registers it:

postprocess.add("vignette", asset.resolve("~.shaders.vignette", "shader"), { priority = 200 })

The registration names no layer, so the effect grades the layer its space belongs to: this vignette reads light, so it runs on scene, over the picture before the UI reaches it. Registering it is the one step that makes it render, so a world's chain holds exactly the passes its content asked for. Editing the shader body afterwards recompiles every effect registered from it, in place — the pass keeps its enabled state, priority, layer, tuned property values and bound textures, and draws the new pixels on the next frame. postprocess.remove(name) takes it back out of the chain and leaves the asset alone.

Drive the named properties at runtime with postprocess.setProperty(name, prop, value) and bind texture properties with postprocess.setTexture(name, prop, path) (both keyed by the property name, never a slot index — see the post-process section of man shaders / the postprocess.* API).

Read them back with postprocess.describe(name): it carries the chain state postprocess.status() lists for that effect, the properties schema it was registered with, and the values each of those properties currently holds. Before the effect exists, shaderRef:getProperties() on the asset names the same schema, so what a pass takes is answerable either side of the registration.

A screen shader is a standalone full-screen visual (UI background, procedural pattern). ScreenInput mirrors PostInput (frag_coord + uv); there is no scene to sample, so it has no zero_sample_scene helpers. Its engine uniform is a ScreenEngineData:

FieldTypeMeaning
timef32gameplay seconds: advances with the world at engine.timeScale and stands still while gameplay is paused (edit mode starts paused), so an effect slows and stops with the world
scrollf32UI scroll offset
resolutionvec2<f32>render-target size in pixels
mousevec2<f32>mouse position
velocityf32scroll velocity
// @domain: screen
fn fragment(in: ScreenInput) -> vec4<f32> {
    let t = engine.time * 0.5;
    let col = vec3<f32>(
        sin(in.uv.x * 3.14159 + t) * 0.5 + 0.5,
        sin(in.uv.y * 3.14159 + t * 0.7) * 0.5 + 0.5,
        material.brightness
    );
    return vec4<f32>(col, 1.0);
}

Library includes

A library module is a .shaderModule asset: it only EXPOSES functions and structs and does nothing on its own (exactly like a Unity .cginc). You must call what you include. Include one by its identity, the same string you would hand asset.resolve:

IncludeExposes
@builtin::shaderModules.pbr_shadingstruct PbrSurface { albedo, normal (world), metallic, perceptual_roughness, emissive, occlusion }; zero_pbr_default_surface(FragmentData) -> PbrSurface; zero_pbr_shade(FragmentData, PbrSurface) -> vec3<f32> (the full forward PBR lighting)
@builtin::shaderModules.shadowscompute_directional_shadow_factor(...), compute_spot_shadow_factor(...), compute_point_shadow_factor(...), compute_area_shadow_factor(...), compute_punctual_shadow_factor(...), zshadow_light_reach(...)
@builtin::shaderModules.engine_bindingsthe group(0) declarations (camera, transforms, lights, shadow maps) — surface shaders get this implicitly; include it directly only for advanced raw access
@builtin::shaderModules.shadow_biaszshadow_offset_receiver(...), zshadow_directional_depth_bias(...) and the rest of the bias math, as pure functions
@builtin::shaderModules.texturingztex_stochastic(...) (tile without a visible repeat grid), ztex_blend_normals(...) (reoriented normal blending)
@builtin::shaderModules.depthzero_linear_depth(depth, near, far) — the one reading of a stored depth
@builtin::shaderModules.gbuffer_material (zero::gbuffer_material)struct ZeroGbufferMaterial { base_color, metallic, perceptual_roughness }; zero_gbuffer_unpack_material(texel: vec4<u32>) -> ZeroGbufferMaterial; zero_gbuffer_material(t: texture_2d<u32>, coord: vec2<i32>) -> ZeroGbufferMaterial, zero_gbuffer_base_color/zero_gbuffer_metallic/zero_gbuffer_roughness(t, coord) — the one unpacking of @scene.material's Rgba8Uint texel into what a surface reflects
zero::scene_depthzero_scene_depth(px: vec2<f32>), zero_scene_depth_uv(uv: vec2<f32>), zero_scene_depth_texel(px: vec2<i32>), zero_scene_depth_size(), zero_scene_depth_is_sky(d: f32), zero_scene_world_position(px: vec2<f32>), zero_scene_world_normal(px: vec2<f32>), zero_scene_depth_fade(px: vec2<f32>, world_position: vec3<f32>, fade_distance: f32) — every px is a pixel coordinate in two components, so a surface fragment passes input.frag_coord.xy, not the vec4<f32> FragmentData.frag_coord holds. The depth already drawn UNDER a surface fragment, and the world position and geometric normal of the surface standing there. What a decal projector box, a soft particle, a forcefield or water intersection foam shades against. Surface domain; the depth belongs to the view being drawn
zero::scene_colorzero_scene_color(px: vec2<f32>), zero_scene_color_uv(uv: vec2<f32>), zero_scene_color_texel(px: vec2<i32>), zero_scene_color_size(), zero_scene_color_refract(px: vec2<f32>, offset_px: vec2<f32>), zero_scene_color_refract_uv(px: vec2<f32>, offset_fraction: vec2<f32>) — every px is a pixel coordinate in two components, so a surface fragment passes input.frag_coord.xy, not the vec4<f32> FragmentData.frag_coord holds. The scene colour already composed BEHIND a surface fragment, and the displaced read that bends it. What heat haze, a refraction shockwave, a cloaking or shield warp, glass and ice shade with. Surface domain; the colour belongs to the view being drawn, and is the lit opaque scene as it stood immediately before the transparent pass — so a blend-queue surface reads this frame's, an opaque one reads the previous frame's, and two overlapping blended surfaces both read the same picture and do not bend each other

asset.list("shaderModule") enumerates the .shaderModule assets in this engine, builtin and world-authored alike, and each one's README says what it exposes. Those are the @builtin::shaderModules.* rows above; a module you write is included exactly the same way, by its own identity or by the ~.name form.

The zero::-named rows come from the renderer itself. Their WGSL is baked into the engine binary and registered under those names before any asset exists, so they answer an #include in every engine and appear in no asset index. A shader that includes one records no dependency on it: the engine reading the shader is the engine that already holds the module.

Two spellings reach the same .shaderModule: a module's declared alias answers as well as its identity, so a shader written against zero::pbr compiles and pins the asset that name resolves to. Prefer the identity: it says which asset travels with the content.

Whichever punctuation an include is written with — #include "name", #include 'name' or #include <name> — names the same module and records the same dependency.

The engine's own scaffolding — zero::surface, zero::sky, zero::post, zero::screen — is injected by the compiler around the matching domain and is never written by hand. The surface framework puts safe_normalize, mat3_from_mat4 and apply_normal_map(FragmentData, tangent_normal) in scope for every surface shader.

Shader features — compile-time variants

A property is runtime uniform data (a colour, a float, a texture): set per material, read as material.<name>, no recompile. A feature decides which code is in the program at all. Use a property when the value changes at runtime, a feature when it changes what the shader is.

Declare features in properties.yaml, alongside the properties:

properties:
  - { name: base_color, type: color, default: [1, 1, 1, 1] }
features:
  - { name: PARALLAX, default: false, description: silhouette parallax march }
  - { name: DETAIL,   default: true,  description: detail-normal overlay }

Guard the code each feature owns with #ifdef:

fn surface(input: FragmentData) -> PbrSurface {
    var s = zero_pbr_default_surface(input);
#ifdef PARALLAX
    s.normal = parallax_normal(input);       // absent when PARALLAX is off
#else
    s.normal = input.world_normal;
#endif
    return s;
}

#ifdef / #ifndef / #else / #endif, one per line, and they nest. The directives resolve before the WGSL reaches the compiler, so the program holds exactly the lines its feature set selected and every other line is an empty one, which keeps a compiler message on the line number the file shows. An unbalanced block is a compile error naming its line.

A material is what chooses a feature set, so features: shapes the shaders a material draws with — the surface, sky and screen-space domains.

A material names the features it wants in its mat.yaml:

name: worn_rock
shader: "@builtin::shaders.pbr_layered"
features:
  PARALLAX: true      # on, over the shader's default
  DETAIL: false       # off, against the shader's default

or through asset.create:

asset.create("material", "worn_rock", {
    shader = "@builtin::shaders.pbr_layered",
    features = { PARALLAX = true, DETAIL = false },
})

A property that switches a feature on

A feature can also be switched on by a material's property values. List the properties under the feature's properties:, and the feature is on for every material whose value for one of them stands away from that property's declared default:

properties:
  - { name: clearcoat, type: range, default: 0.0, min: 0, max: 1 }
features:
  - { name: CLEARCOAT, properties: [clearcoat], description: the coat lobe }
#ifdef CLEARCOAT
    s.clearcoat = material.clearcoat;
#endif

A material at clearcoat: 0 draws a program holding none of the coat's code; one that sets clearcoat: 0.6, in its mat.yaml, through asset.create or with renderer.material.setProperty, draws the CLEARCOAT variant, and setting it back to 0 returns it to the base program. The property stays a runtime value the program reads; the feature decides only whether the code reading it is there. This is how a shader with costly optional parts keeps every material that leaves them at rest on the cheap program without each material having to name a feature. A material naming the feature off under features: keeps it off whatever the property holds. Write the guarded code so that, with the feature off, the shader computes what it would with the property at its default: the material sees one look either way, and pays for the part only where it shows. Each distinct set of switched-on features is a program to compile the first time a material reaches it. A material keeps the programs its values switched it onto compiled while it lives, so a property crossing back and forth reuses them; when the shader's variant budget is spent, the ones no material draws with are given back first. A properties: entry is a value property (float, range, int, bool, color, float2 to float4) the shader declares; one naming no declared property, or a texture or buffer, is refused by name.

The engine compiles one program per feature set its materials ask for, named <shader>#<features>. Materials sharing a set share that program. A feature the shader does not declare selects nothing, so a stray name cannot make the engine compile anything, and the compile names both halves of a mismatch: a guard on a name features: does not declare, and a declared name no guard tests.

Permutations are the cost. A shader with n features can express 2^n programs, each its own compile and pipeline. One shader holds at most 32 variants; a material asking for a set past that draws with the shader's default features and the engine logs which shader ran out. Read what a scene is spending:

for _, s in renderer.shaderVariants() do
    print(s.shader, #s.variants, "of", s.budget)
    print(renderer.variantSource(s.base.program))          -- what it ships as
    for _, v in s.variants do
        print(v.program, renderer.variantSource(v.program))  -- what one asked for
    end
end

A shader registers under every key it answers to — its identity, its guid, each alias — and each key is a row. A variant lands on the row for the key the material named, which for authored content is the guid, so take a program name from the report rather than composing one.

Materials reference shaders

A .material applies a shader to entities. Its mat.yaml references the shader by its identity string and supplies property values:

name: my_material
shader: "@builtin::shaders.pbr"
render:
  blend: alphaBlend
  cull: back
  depth_write: false
properties:
  roughness: 0.5
  base_color: [1.0, 1.0, 1.0, 1.0]
textures:
  base_color_texture: { ref: "default:white" }

shader: is a plain identity string, and a colour is an array. A key's value is the rest of its line, so every value above stands alone on one.

The render block

Render behaviour (the blend equation, alpha cutout, which faces are drawn) lives in the render: block and drives the pipeline, not the fragment. These are the keys that reach the pipeline from both authoring routes, and the values each one takes:

KeyValuesSets
blendopaque transparent alpha alphaBlend additive add cutout alphaCutout premultiplied premultipliedAlpha multiplythe equation the fragment composites with
cullback front none off disabledwhich faces the rasterizer keeps
depth_writetrue falsewhether the draw writes depth
depth_compareless lessEqual lequal equal greater greaterEqual gequal always neverthe test a fragment's depth passes
topologytriangleList lineList lines linewhat consecutive vertices assemble into
queuea whole number: 2000 opaque, 3000 transparent, 4000 overlaythe draw order of the material; a number inside a queue orders within it

blend is also spelled type, and the two name one key. Its equations, over the fragment the shader returned (src) and the value already in the target (dst): opaque is src; transparent, alpha and alphaBlend are src * src.a + dst * (1 - src.a); additive and add are src + dst, the mode a glow, a light shaft or a spark card is written for; premultiplied and premultipliedAlpha are src + dst * (1 - src.a), taking a source that already carries its own coverage; multiply is src * dst, which is how a soot, grime or shadow card darkens what stands behind it. cutout and alphaCutout keep the fragment or discard it on its alpha and go on writing depth.

The same block is what asset.create's render argument takes, key for key and value for value, so one material named in Luau and the same material standing on disk ask for their pipeline with the same words. depth_write is the one key whose value takes the shape of its route: the word false on a mat.yaml line, the boolean false in the Luau table.

asset.create("material", "heat_plume", {
    shader = "@builtin::shaders.pbr",
    render = { blend = "alphaBlend", cull = "none", depth_write = false },
})

Where a value outside a key's set is reported is what the two routes differ on. asset.create refuses the call on any of these keys and names the set it measured the value against, or for queue, that it takes a number. A mat.yaml standing on disk registers either way, each key falling back to a value of its own: blend to opaque, cull to back, depth_write to true, depth_compare to less, topology to triangleList and queue to 2000. The engine log carries the blend fallback, naming the value and the set it was measured against, so a mat.yaml value is worth putting through asset.create once — that is where every key answers with its set.

Registration & hot reload

A .shader is compiled by the asset system, not by the renderer: writing shader.wgsl or properties.yaml fires the .shader assetType's onChange, which compiles it: the body + the property schema are read, the group(1) interface and (for surface) every render-mode entry point are generated, #includes are expanded, the result is validated, and the shader is registered. Validation happens at this compile step — the whole point of "compile". A shader with invalid WGSL (an unknown field, a type mismatch, an undefined function) fails to compile with the real reason — readable directly from Luau via shaderRef:compileStatus() ({ status = "compiled" | "failed" | "pending", error?, pending? }), and also in the author log and asset.inspect; it is not registered. This fires on create and on every edit, so saving a .wgsl recompiles with no world reload; the change is visible on the next frame. Compilation is async, so a compileStatus() of "pending" immediately after a write means check again next frame.

A surface shader is also checked for the material resolve, the pass that shades each pixel of the visibility buffer through the shader's own code. That check reaches its verdict a few frames after the raster compile. Until it does, compileStatus() reads "pending" with pending saying so, and the shader already draws. A shader the check refuses reads "failed", and error names the construct the resolve cannot evaluate and where it sits; its pixels resolve through the error shader until it changes.

The stage the compile checks comes from one of two places: the hook the engine scaffolds entry points around (fragment(), or surface() for an engine-lit surface), or the vs_main / fs_main pair the body writes itself. The pair is a pair: a body that writes its own entry points owes both stages. A body carrying neither route — a hook whose name is a letter off, say, or half a pair — holds no stage the rasterizer can enter, so the compile refuses it and names both the entry point it looked for and what the body carries, in the same compileStatus() error every other compile failure reads back through.

Reading a compile error against the text it is about

What the compiler reads is not the file you wrote. The engine generates the group(1) material interface from properties.yaml, wraps the body in its domain's framework and every entry point that domain needs, expands every #include, and resolves every #ifdef — so a 100-line body reaches naga as tens of thousands of lines. Every line number in a compile error, and every [N] handle index naga prints where it has no name to print, is a position in that composed module. A fault at module scope (no definition in scope for identifier: 'zero_scene_depth') reads fine without it; a fault inside a function body does not.

renderer.compiledSource(name) returns that composed module, and shaderRef:compiledWgsl() returns it for a ref you already hold. Both answer for a shader that declares no features, needs no material, entity or draw, and — the case they exist for — answer for a shader whose compile failed, holding the text the compiler was refused over for as long as compileStatus() reports that failure. renderer.compiledShaders() lists every name they answer for.

local ref = asset.resolve("myShader", "shader")
local status = ref:compileStatus()
if status.status == "failed" then
    print(status.error)              -- the line, the caret, and the cause
    local wgsl = ref:compiledWgsl()  -- the text those positions are in
    local line = tonumber(string.match(status.error, "wgsl:(%d+):"))
    print(string.split(wgsl, "\n")[line])
end

The composed text carries every zero::* module the shader included, so the signature a call was rejected against is in the same text as the rejection — which is the whole of what a "wrong argument type" error needs to be actionable.

A material bound to a broken (or missing) shader renders with the magenta error shader — never invisibly — so a shader problem is impossible to miss on screen as well as in the log. Fixing the shader and re-saving recovers it live, with no engine restart: the recompile clears the failed-shader state and the object returns to normal on the next frame.

Discovery

  • asset.list("shader") — every registered shader.
  • asset.inspect("<name>") — domain, status, source, parse errors, this README.
  • shaderRef:compileStatus() — did it compile? { status, error?, pending? }, no log needed. "pending" with pending set means the material resolve check is still reaching its verdict; a resolve refusal reads as "failed".
  • shaderRef:compiledWgsl() / renderer.compiledSource("<name>") — the WGSL the compiler received, which is what a compile error's positions index into. Answers for a failed compile too.
  • renderer.compiledShaders() — every name that call answers for.
  • shaderRef:getProperties() — the parsed properties.yaml schema.
  • shaderRef:listMaterialsUsing() — materials bound to this shader.

Common pitfalls

  • A compile error shows up in one of three ways at the draw. A shader that never compiled has no pipeline, so the renderer binds the magenta placeholder. A shader that compiled once and was then edited into brokenness KEEPS the pipeline that build produced — the surface goes on drawing the last good program, pixel for pixel, and looks entirely intact while the source is broken. A geometry-pass-only shader has no magenta tell either, since it is not on a forward mesh. shaderRef:compileStatus() gives the status + error text (as does asset.inspect("<name>")), and renderer.drawDiagnostics() names which renderables are in which of the three states — a row carries substituted for the placeholder, stale for a program that no longer compiles, and the cause either way.
  • A compile error's line numbers are lines of the composed module, not of your file. renderer.compiledSource("<name>") / shaderRef:compiledWgsl() is that module; resolve the position against it rather than counting lines in shader.wgsl. The [N] a message prints in place of a name is a naga handle index — it moves with the size of the body and is the same value for two unrelated faults in one file, so it is neither a line number nor a way to tell two failures apart.
  • Don't declare group(0) or group(1) in a surface shader. The engine owns group(0) and generates group(1) from properties.yaml. Re-declaring them is the fastest way to a layout mismatch.
  • #include does nothing by itself — it only exposes functions; you must call them.
  • Renaming a property changes the generated uniform field; materials setting the old name silently fall back to its default.
  • Compute shaders don't render — verify output via a buffer readback or a debug pass. Author them as a .computeShader (its own assetType), not a .shader — see man compute.

Related types

  • .material — the required wrapper that applies a shader to entities.
  • .style — UI cascade, not GPU shaders.

In the Inspector

Selecting a shader opens its Shader section: its domain and shading model (and, for a post-process shader, the space its fragment() reads and returns), whether it compiled (and the compiler's error when it did not), a table of its declared properties (type, default, range), its post-process passes when it has any, the bindings it declares, and openers for its source. The Materials section (collapsed at first) has a Find materials action that lists, as links, every material drawing with the shader; it scans every material, so it runs only when asked.

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.shadercontainernoprimary aliasesinit.luau, init.lua, shader.wgsl, shader.glslplural dirshadersrequired filesshader.wgsl, README.md, .metadataoptional filesshader.glsl, properties.yaml, preview.png
Exposed API
⌬ Instance methods

previewMaterial(self: ?) →

The material a surface preview paints this shader with: a transient one built from the shader at its `properties.yaml` defaults, which is the shader as it looks before anyone tunes it. Only a surface shader has a surface to paint; a post-process, sky or compute shader answers nil and the reason. `ref:preview()` paints the result on the mesh `type.yaml` names under `preview:` and releases the material after the render.

argtypedescription
self?

examples

local surface, why = shaderRef:previewMaterial()

getSourceCode(self: ?) → string

Read the primary shader body (`shader.wgsl`) as raw text.

argtypedescription
self?

examples

local src = shaderRef:getSourceCode()

getWgsl(self: ?) → string

Read the WGSL body directly, ignoring the GLSL fallback. Use when you need to detect "is this shader WGSL-native?" vs the generic `getSourceCode` lookup that auto-falls-back.

argtypedescription
self?

examples

local wgsl = shaderRef:getWgsl()

getGlsl(self: ?) → string

Read the GLSL body, when present. Returns nil for WGSL-only shaders.

argtypedescription
self?

examples

local glsl = shaderRef:getGlsl()

setWgsl(self: ?, source: string) → boolean

Overwrite the WGSL body on disk. Hot-reload picks the new body up on the next frame so any material using this shader recompiles. Returns true on success.

argtypedescription
self?
sourcestringNew WGSL source.

examples

shaderRef:setWgsl(myWgsl)

getProperties(self: ?) →

List this shader's declared material properties (parsed from `properties.yaml`). Each entry is `{ name, type, default, min?, max? }`. This is the editor-discovery surface, and the SAME parse the compile uses. Raises, naming the file, when `properties.yaml` is text the YAML decoder refuses (with the line) or one of its lists holds anything but a list of mappings (with the key).

argtypedescription
self?

examples

for _, p in ipairs(shaderRef:getProperties()) do print(p.name, p.type) end

getSpace(self: ?) → string

The space this post-process shader's `fragment()` reads and returns, from `space:` in its `properties.yaml`: `"light"` (the default) or `"display"`. The word the file declares, verbatim — an effect registered from a shader whose `space:` names something else renders in `"light"` and the engine log carries the spelling it was handed.

argtypedescription
self?

examples

print(asset.resolve("@builtin::shaders.post.lut", "shader"):getSpace())

getPasses(self: ?) →

The passes this post-process shader declares, from `passes:` in its `properties.yaml`, in the order they are drawn. Each entry is `{ name, entry, scale, inputs }`; `scale` is `0` on the last pass, which draws at the view's size. A shader that declares none is one pass.

argtypedescription
self?

examples

for _, p in ipairs(shaderRef:getPasses()) do print(p.name, p.entry, p.scale) end

shadingModel(self: ?) → string

Which shading model this shader's body uses, and with it what the engine can take the shader apart into. `"engine-lit"` — the body exposes `fn surface(...) -> PbrSurface`. It hands the engine a surface (albedo, roughness, metallic, emissive, normal, occlusion) and the engine lights it, so the capture tool's PBR debug passes read real material channels. `"self-shading"` — the body exposes `fn fragment(...) -> vec4<f32>` and same passes render what the body returns. `"unknown"` — no readable body, or one that exposes neither entry point. The test is the literal one the compiler applies to the same source, so this reports the model the shader was actually built as.

argtypedescription
self?

examples

if shaderRef:shadingModel() == "self-shading" then print("albedo pass shows final colour") end

domain(self: ?) → string

The render domain this shader's body declares with its first `// @domain: <name>` line — what entry points and framework the engine builds around it: `"surface"` (the default), `"post-process"`, `"screen"`, `"sky"`, `"compute"`. A further spelling (`postprocess`, `screenspace`, `ui`) answers as the domain it names; a name the compiler refuses answers as written.

argtypedescription
self?

examples

if shaderRef:domain() == "post-process" then print(shaderRef:getSpace()) end

compileStatus(self: ?) →

This shader's latest compile outcome, read without the engine log. Returns `{ status, error?, key?, pending? }` for the key the shader compiles under (`guid@checksum` of the version at its path): `status` is `"compiled"`, `"failed"` (with `error`, the compiler's message) or `"pending"` (not compiled yet, or compiled with part of its verdict still being reached, which `pending` names: the material resolve's gate runs off the frame thread). A `properties.yaml` that `getProperties` refuses reads `"failed"`, with `error` the same refusal, because the shader is not compiled against it. Compilation is async (a write queues it), so a `"pending"` right after editing means check again next frame. A write succeeding and `getProperties` returning a schema do not mean the WGSL compiled; this does.

argtypedescription
self?

examples

local s = shaderRef:compileStatus(); if s.status == "failed" then print(s.error) end

compiledWgsl(self: ?) → string

The WGSL the shader compiler received for this shader, exactly as it received it. What `getWgsl` returns is the body as written; this is what that body became — the generated group(1) material interface above it, the domain's framework and entry points around it, every `#include` expanded and every `#ifdef` resolved. A compile error's line numbers, and the handle index naga prints where it has no name, are positions in THIS text, so a `compileStatus()` of `"failed"` is read against it. It answers for a failed compile as well as a clean one, and needs no material, entity or draw.

argtypedescription
self?

examples

local s = shaderRef:compileStatus()
if s.status == "failed" then print(s.error, shaderRef:compiledWgsl()) end

listBindings(self: ?) →

List which top-level uniform / storage-buffer block names appear in the shader source — best-effort regex parse. Useful to cross-check against `material:getPropertyNames()` when debugging a "property doesn't exist" gap. Not a full WGSL parser; complex shaders with macros may report incomplete results.

argtypedescription
self?

examples

for _, name in ipairs(shaderRef:listBindings()) do print(name) end

listMaterialsUsing(self: ?) →

List the material identities currently bound to this shader, by scanning the registered material catalogue. O(n) over the material list; cache the result if you call it on a hot path.

argtypedescription
self?

examples

for _, m in ipairs(shaderRef:listMaterialsUsing()) do print(m) end
⌬ Hooks

compileKey(key: string) → string

Compile the shader key `key` (`guid@checksum`) for the materials waiting on it: the asset holding exactly that guid at that checksum (the guid's home when its bytes are that version, else a copy of that version held beside it) is compiled through the same path an edit runs. The engine calls this when a material first waits on a key, again when an asset under the key's guid arrives or changes, and again for a key answered `"absent"` once the asset index holds that version. While a world's content is being replaced, the index can state a version whose files are not all there to read; such a folder has nothing to compile yet, and the key is answered `"absent"` so it is asked for again. its program registers or its compile failure is recorded; `"absent"` when no asset holds the key, or the one that does holds nothing to compile yet. Raises with the refusal when the asset's `properties.yaml` is refused.

argtypedescription
keystringThe shader key, `guid@checksum`.

examples

require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("shader").compileKey(key)

schemaParseCount( ) → number

How many times a shader's `properties.yaml` has been parsed in this session, across every shader and every surface that reads one — the editor-discovery method, the compile, the alias registration and the alias lookup. A read whose bytes match the parse already held answers from it and leaves this unchanged, so the count rises once per distinct revision of a file rather than once per read.

examples

local m = require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("shader"); local before = m.schemaParseCount(); ref:getProperties(); print(m.schemaParseCount() - before)

onChange(ref: ?, change: ?) → void

Asset-type change callback: (re)compile the shader whenever its WGSL body or `properties.yaml` is written, and bring materials already bound to it onto the declared property list when that list changed shape. This is the ONLY thing that compiles a `.shader` — the implicit "WGSL written → recompile" path is gone — so it fires on the initial create (the template write) AND on every later edit, with no world reload. Convergent: see `compileShader`.

argtypedescription
ref?
change?

onDependencyChange(ref: ?, _change: ?) → void

Dependency callback: a reference the shader names bound to its target — an `#include` of a module that did not exist when the shader last compiled. The shader compiles again against the module now there.

argtypedescription
ref?
_change?

onDelete(ref: ?) → void

Asset-type delete callback: release what a removed `.shader` folder registered. The teardown counterpart of `onChange`'s compile: under the key the shader compiled under, its programs and their variants, its composed WGSL, its compile status and its diagnostic go with the asset, and so do the per-path records this module kept for it. A shader authored afresh at the same path compiles as a new one.

argtypedescription
ref?

Sub-parts

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

Showing the 199 items nearest this part. It holds more than this page lists.

199+items
·
assettype · born here
❒asset
# assetType (asset type) An asset type says what a kind of asset is: the files an asset of the kind is made of, the code that gives it behaviour, the interface a person uses to work with it, and how it is found, pictured and checked. A `<typename>.assetType/` folder declares a type called `<typename>`, and every `<name>.<typename>/` folder is an asset of it. `assetType` is itself a type, so a type is an asset like any other: it is created, versioned, installed and contributed the same way. ## Making a type `asset.create("assetType", "<typename>")` scaffolds a type. The type registers the moment its folder is written, anywhere under `/zero/source/`, and from then on `<name>.<typename>/` folders are its assets. An asset of a type that is not registered yet is not an asset, so make the type first. A type folder is a root: `~.x` in any file inside it names the type's own `x`, wherever the type is installed. Keep the type's whole implementation inside its folder and reach it that way. What a type folder must hold is set by this type's own `type.yaml`, the same way every type's `type.yaml` sets what its assets hold. `asset.validate` on a type reports anything missing, and the scaffold produces a folder that passes. ## What sets your type's schema (`type.yaml`) `type.yaml` is the structural contract every asset of the type is checked against. The keys below are the ones the engine reads; a key not listed here is ignored. | Key | What it controls | |---|---| | `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. | | `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. | | `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. | | `is_container` | Whether the suffix stays in the identities of what the asset holds. | | `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. | | `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. | | `is_text_default` | Whether the asset's files are text unless stated otherwise. | | `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. | | `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. | | `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. | | `files: { required, optional }` | The files an asset holds (below). | | `allow_unlisted` | `false` reports every file the type does not declare. | | `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. | | `settings:` | Typed per-asset settings (below). | | `inspector:` | The interface people use for the type and its assets (below). | | `preview:` | How an asset is pictured (below). | | `references:` | Names of other assets the type's files carry (below). | | `indexing:` | What search reads for each asset (below). | | `describe:` | How a missing description is generated (below). | ### The files an asset holds Each entry under `files.required` or `files.optional` is `{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches within one segment. Entries that share a `one_of_group` are satisfied by any one of them. `content` lists checks on the file's bytes: | Check | Passes when | |---|---| | `non_empty` | the file has content | | `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape | | `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML | | `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") | Each check takes a `message`, which is what the author is told when it fails. ### Settings `settings: { values, schema: { type | instance, export? } }` gives every asset typed settings. `values` names the file an asset's values live in; `schema.type` declares one schema for the whole type, and `schema.instance` names a file each asset declares its own in. `asset.create` writes the resolved values, and a write to the values file reaches `onChange` with what changed. ## The human side (`inspector.luau`) A type is used by people through its Inspector, and without one the type is usable only from code. The Inspector is the type's interface for a person: for a procedural graph it is the graph editor, for a kit it is the tracking view of what the kit holds and what is still missing. Writing it is part of writing the type. `inspector: { type, instance? }` in `type.yaml` names the files. `type` is the interface for the type itself and for its assets; `instance` names a file an asset may ship to add its own. Each returns: ```luau return { sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } }, replace = { contents = { ... } }, -- replace a default section by id order = { "graph", "contents" }, -- section order } ``` `build(ctx)` receives the asset and builds the section. A section's `title`, and the text its rows carry, is a string or an `i18n.text` value read from a catalog (`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType", ...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a type that names no inspector, or names one that does not load, reports it. ## Say what your type is for (`about.yaml`) Every type carries an `about.yaml` that says what the type is for, in three plain-language texts: ```yaml intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering." function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text." usage: "You choose it by name for your UI or your text, and the text appears in that typeface." ``` - **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes. - **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes. - **`usage`** is what a person or an agent does with one. Up to 1200 bytes. Write about the type as a whole, not one asset of it, and say what it does now. Agents read these texts to learn when to reach for the type and how; people searching find the type by them. The scaffold starts each text with `[[describe]]` and a note on what to write; a text that still starts that way counts as not written. `services describe` can write them. They show up in `asset.inspect` as `detail.about`, in `assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide, and at the top of the type's Inspector. ## Ask your assets to say what they are A type can require each asset to carry a description: a file, written by the author or generated, saying what the asset is and what it is for. Declare it with a `described` check: ```yaml files: required: - path: "README.md" kind: "file" content: - { check: "described", message: "Say what this widget is for and where it goes." } ``` A file that is missing, empty or still has a line starting with `[[describe]]` fails the check, and so does a YAML file whose named `keys` are missing or over their `max_bytes`, or a Markdown file missing a named heading. `asset.validate` names the asset, and a push that carries it is refused with `message`. `services describe` finds such assets and writes their descriptions. `describe:` steers what is generated: ```yaml describe: guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI." examples: - "<identity of an asset of this type whose description reads well>" ``` An example is an asset of your type whose description is a true, written one, never a placeholder and never the asset being described; with none, leave `examples` empty. A type whose assets the model cannot perceive (a sound, a motion) says so in `guidance`, and the description is drawn only from what the asset's own files state. Start the file unwritten in your `template/`: a heading and one line starting with `[[describe]]`, never example content, which every new asset would carry verbatim. A type whose assets say what they are through their own content (a mesh, a texture) declares no check. ## The type's code (`behavior.luau`) `behavior.luau` returns one table. These are the keys the engine reads from it; anything else on the table is the type's own. ### What the type's assets can do | Key | Shape | What the engine does with it | |---|---|---| | `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. | | `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. | | `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. | | `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). | | `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. | | `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. | Some `ref` names mean the same for every type, and the engine calls them: - **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below). - **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below). - **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe. - **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints. - **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode. Some names are answered for every asset before the type is asked, so a `ref` entry of the same name is never reached: `getSource`, `getBytes`, `getText`, `exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`. ### When the engine calls the type | Hook | Signature | When it runs | |---|---|---| | `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. | | `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. | | `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. | | `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. | | `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. | | `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. | | `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. | ### Settings hooks | Hook | When it runs | |---|---| | `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. | | `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. | | `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. | | `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. | | `settingsHealed(values, stored)` | On a read, to repair stored values. | | `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. | `creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options that are inputs to the type rather than settings. ### Reloading An edit to `behavior.luau` reaches existing assets with no restart. ## How an asset is pictured (`preview:`) | `kind` | The picture is | The type provides | |---|---|---| | `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` | | `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order | | `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` | | `custom` | whatever `ref.preview` returns | `ref.preview` | ```yaml preview: kind: surface mesh: sphere ``` `angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop and the framing, so an asset pictures the same in every world. A type with no `preview:` that defines `instantiate` still answers `ref:preview()`. A picture is rendered when something asks to view it (the editor showing the asset, `ref:preview()`), when previews are generated on request (`assets.generatePreviews`), and when the asset is staged. An edit alone renders nothing. The picture records what it was made from, so a stale one is told from a current one. ## The names your type's assets hold (`references:`) Where your type's own format or API carries the name of another asset, declare it here: ```yaml references: keys: # a top-level key of the type's files that holds a name, or a table of them - { key: "shader", type: "shader" } args: # an argument of a call, counting from 1 - { call: "g:node", position: 2, type: "procNode" } fields: # a field of the options table a call takes - { call: "fx.beam", field: "material", type: "material" } ``` `type` is the category the name resolves in. `asset.declareReferenceKey`, `asset.declareReferenceArg` and `asset.declareReferenceField` declare the same from a script. ## What search reads (`indexing:`) `indexing:` is how assets are found. Search reaches an asset only through what its type's `indexing:` names, and adds nothing of its own. That includes you: an asset indexed badly is one you cannot find later, when you are making something else and need it. A type whose assets are never looked for says `indexing: none`. ```yaml indexing: data: # text, read from the asset's files - role: identity extractor: verbatim source: { file: "README.md" } - role: interface extractor: symbols source: { files: ["init.luau"] } - role: implementation extractor: verbatim source: { files: ["init.luau"] } content: # the picture - role: identity modality: image source: { file: "preview.png" } facets: # what a query filters and re-ranks by file: ".metadata" rigged: { type: bool, from: { components: { contains: SkinnedModel } } } roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } } ``` **Roles.** Each `data` and `content` entry has a `role`, and a query matches in two slots: - **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is. - **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both. Ask what a person would recognise an asset of the type by, and what it does, and point each role at the file that says it. For text, that is the asset's description: the file your template starts with a `[[describe]]` block, which the author writes or `services describe` fills in (see "Ask your assets to say what they are"). **`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`. Never index a file name (`source: { field: name }`): one or two words carry nothing to match. **`content`.** `modality: image` embeds the picture itself, so a text query matches what the asset looks like. A type whose assets are looked at makes the picture its identity. **`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded whole, so keys nobody declared in it are still found. Each further key is computed for every asset and states a value for every one, including `false`: a file can imply "not rigged" but never say it, and a query separates rigged from unrigged only because the unrigged ones say `rigged: false`. A key is `{ type: bool | number | enum, filter?: range, from }`, where `from` is `{ file, field }` (a field of one of the asset's files, dotted for nesting), `{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the asset's entities carry). ## What blocks a push A push checks every asset it carries against its type. These refuse it: - a required file that is missing, a `content` check that fails, a `described` file not written yet; - an error from the type's `validate`. Unlisted files and warnings do not. A `described` file blocks only a push that changes the asset. When a push is refused, the message names each asset and what it lacks. `world.publishBlockers()` and `zm status` read the same list without pushing. ## Typing a result from the asset (`refShapes`) A `ref` method whose result depends on the asset (one entry per child folder, per row of a config, per control a map declares) can only be annotated as `{ [string]: Thing }`, which accepts any key. `refShapes` states the result for each asset instead: a function returning a Luau type expression for that asset, and the module whose types the expression uses. Read the asset; never run it. ```lua -- inputMap.assetType/behavior.luau M.refShapes = { activate = function(self): (string, string) local names = {} for _, control in ipairs(M.ref.controls(self)) do table.insert(names, control.name .. ": Handle") end if #names == 0 then return "", "" end return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme" end, } ``` With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported. A per-asset type applies where the call site names which asset: a component field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`. `asset.resolve(name, category)` carries only the category, and a name computed at runtime carries nothing, so calls on those keep the declared return. The type belongs to the call's result: index it there, or bind what you need from it there, and the names stay checked. `require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows what was published: `categories` (each type's method surface) and `returns` (per-asset results by identity, then method). An identity missing from `returns` was never published. Results are recomputed when an asset is written; at most 64 assets of one type are read. ## Putting an asset in a scene (`ref.instantiate`) A caller writes one piece of code against every instantiable type, so what `instantiate` returns is fixed: ```lua local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable") M.ref = { instantiate = function(self, target, opts) local root = Instantiable.root(self, target, opts) -- compose the asset under `root`, now return Instantiable.result(self, root, idMap) end, } ``` **In.** `target` is the entity the asset lands under, or onto for a hierarchy type; with none, the type spawns a new root. `position`, `rotation` (three numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the same for every type: `Instantiable.root` applies them to a root you make, `Instantiable.place` to one you adopt. Document any options of your own in the hook's `--!arg` docs. **Out.** `(root, idMap)`. `root` is the composed root, live when you return: build synchronously, never defer to a component's `awake`. `idMap` maps each original id to the spawned one, so a component that composes the asset again on every load can pass it back in and keep references into the composition. Every call goes through `Instantiable.result`, which refuses a root that is not live and turns a missing `idMap` into `{}`. ## Inspecting an asset (`ref.inspect`) `asset.inspect(ref)` builds a record every asset shares (`identity`, `name`, `guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then calls `ref.inspect(self)` for the type's own `detail`: ```lua M.ref = { inspect = function(self) -- read the asset's own files, never runtime state: inspect works on an asset -- that has not been compiled, uploaded or registered return { --[[ what makes this asset what it is ]] } end, } ``` Without it, the record is the shared part alone, and an agent inspecting the asset learns nothing of what it is. A hook that raises leaves `detail` empty and sets `record.warning`. ## Discovery - `asset.list("assetType")`: every registered type. - `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts. - `asset.resolve("<typename>", "assetType")`: the type itself.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_focusBars Four rows of bar pairs, 8 pairs across the top row and doubling on each row below: the focus chart a depth-of-field reading is taken off. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_skyRamp A sky-blue ramp of light from 0.02 to 0.6 across: a clear sky's gradient. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Chrome A polished chrome surface built on the `pbr` shader — fully metallic (`metallic: 1.0`) with a bright near-white base colour (`0.95, 0.95, 0.97`) and very low roughness (`0.05`) for mirror-sharp reflections. Opaque, back-face culled, instanced, with the `_METALLICSPECGLOSSMAP` keyword enabled. ## Inputs - `colors.base_color` (`0.95, 0.95, 0.97, 1.0`) — bright, faintly cool chrome tint. - `floats.metallic` (`1.0`) — fully metallic. - `floats.roughness` (`0.05`) — near-mirror sharpness. - `textures.base_color_texture` (`default:white`): the white base texture the colour multiplies against, so the surface renders `base_color` alone.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Snow Particle A soft, cold-white flake surface for falling-snow particle effects — a near-white base colour (`0.95, 0.97, 1.0`) with a cool blue-grey emissive glow (`0.25, 0.3, 0.4`) so flakes stay faintly visible in shadow. Transparent, double-sided (`cull: none`), and drawn in the transparent queue (`3000`). Built on `@builtin::shaders.snow_material`. ## Inputs - `colors.base_color` — the cold-white flake tint. - `colors.emissive` (`0.25, 0.3, 0.4`) — cool self-illumination that keeps flakes readable in dark scenes. - `floats.roughness` (`0.8`) — matte, non-glossy flakes. - `floats.metallic` (`0.0`) — fully dielectric.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Gold Polished gold — a fully metallic PBR surface with a warm yellow base colour (`1.0, 0.84, 0.0`) and low roughness (`0.25`) for bright, sharp reflections. Built on `@builtin::shaders.pbr`. ## Inputs - `colors.base_color` — the gold tint. - `floats.metallic` (`1.0`) — fully metallic. - `floats.roughness` (`0.25`) — sharpness of the reflection; raise for a brushed look.
▲ 0↑ born
◐
material · born here
❒asset
# Checkerboard A procedural checkerboard pattern rendered by `@builtin::shaders.checkerboard`, alternating a red base colour (`1.0, 0.2, 0.2`) with a blue emissive colour (`0.2, 0.2, 1.0`). Opaque and non-metallic (`metallic 0.0`) at mid roughness (`0.5`) — useful as a UV / scale test surface. ## Inputs - `colors.base_color` — the red square colour. - `colors.dark_color` — the blue square colour. - `floats.roughness` (`0.5`) — diffuse spread on the lit squares. - `floats.metallic` (`0.0`) — dielectric surface.
▲ 0↑ born
◐
material · born here
❒asset
# Custom Textured A textured opaque surface built on `@builtin::shaders.custom_material`, sampling the `array_texture` builtin over a warm peach base colour (`1.0, 0.8, 0.6`). Non-metallic (`metallic 0.0`) at mid roughness (`0.5`) — a worked example of pairing a custom shader with an array texture. ## Inputs - `textures.base_color_texture` (`@builtin/textures/array_texture`) — the sampled array texture. - `colors.base_color`: the warm peach colour the texture is multiplied by. - `floats.roughness` (`0.5`) — diffuse spread. - `floats.metallic` (`0.0`) — dielectric surface.
▲ 0↑ born
◐
material · born here
❒asset
# Sky Procedural A fully procedural atmospheric sky with a day/night cycle, built on `@builtin::shaders.procedural_sky`. Renders with no culling and no depth write behind all geometry (queue `1000`), computing gradient, sun, moon and stars analytically from its parameters — no texture required. ## Inputs - `properties.zenith_color` / `horizon_color` / `ground_color` — the atmospheric gradient from overhead (`0.1, 0.3, 0.6`) through the horizon (`0.5, 0.7, 1.0`) to the ground (`0.3, 0.25, 0.2`). - `properties.sun_intensity` (`20`) / `sun_size` (`0.02`) — brightness and disc size of the sun. - `properties.turbidity` (`4`) — atmospheric haze / scattering density. - `properties.stars_intensity` (`1`) / `moon_size` (`0.03`) — night-sky star brightness and moon disc size.
▲ 0↑ born
◐
material · born here
❒asset
# Sky Cubemap A cubemap sky/environment background built on `@builtin::shaders.cubemap_sky`. Renders with no culling and no depth write behind all geometry (queue `1000`), sampling the built-in `Ryfjallet_cubemap` with adjustable rotation and exposure. ## Inputs - `textures.sky_texture` — the cubemap sampled as the sky (`Ryfjallet_cubemap`). - `floats.rotation` (`0.0`) — yaw rotation of the sky around the vertical axis. - `floats.exposure` (`1.0`) — brightness multiplier applied to the sampled colour.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Default Unlit A flat, unlit surface that renders its base colour directly with no lighting response, built on the `unlit` shader. Uses a white base colour (`1.0, 1.0, 1.0`) over the `default:white` texture with `emissive` (`1.0`) for full brightness — the go-to default for UI, gizmos, and debug geometry that must stay legible regardless of scene lighting. ## Inputs - `colors.base_color` — the flat colour shown directly. - `textures.base_color_texture` (`default:white`) — the white base texture the colour multiplies against. - `floats.emissive` (`1.0`) — full, lighting-independent brightness.
▲ 0↑ born
▣
module · born here
❒asset
# yaml.module YAML decode + encode for authored engine content. Pure Luau, no engine dependencies — a general parsing library, not data-system plumbing. ```luau local Yaml = require("@builtin::modules.yaml") local doc = Yaml.decode(vfs.read(path)) vfs.write(path, Yaml.encode(doc)) ``` ## Supported subset - Block mappings (`key: value`) and block sequences (`- item`), nested to any depth. - Compact sequence-of-mapping entries (`- path: x` / ` kind: y`). - Flow collections — `{ a: 1, b: 2 }` and `[1, 2, 3]` — single line. - Plain scalars with type inference: `~` / `null` → nil, `true` / `false` → boolean, numeric literals → number, everything else → string. - Single- and double-quoted strings (`''` escapes a literal quote inside a single-quoted string; `\n` `\t` `\r` `\\` `\"` inside a double-quoted one). A quote opens a quoted scalar where a scalar begins — after `key:`, after a `- ` entry marker, or inside a flow collection — so a plain scalar carries an apostrophe as data (`note: the world's rules`). Quoted keys are read in flow mappings. - `#` comments — honored outside quotes, so a `#` inside a quoted string is kept as data. - Literal (`|`) and folded (`>`) block scalars, with a chomping indicator (`-` strips the final newline, `+` keeps every trailing blank line, none keeps one newline) and an indentation indicator (`1`-`9`: the content's indent past the parent's), in either order (`|-`, `>+`, `|2`, `>1-`). A block scalar's content is literal text: quotes (balanced or not), `#`, `key: value` and `---` inside it are data. - An optional leading `---` document marker. ## Loud-error contract Constructs outside the subset above raise instead of misparsing: anchors/aliases (`&`, `*`), tags (`!`), directives (`%`), multi-document streams (a second `---`), a block-scalar header it cannot read (`|x`, `|-+`, `|0`), tab indentation, and inconsistent indentation (both block structure and block-scalar bodies). Every error carries the offending line number (`yaml: line N: ...`), so a bad config points straight at the line to fix. Tab indentation is rejected everywhere — including block-scalar body lines whose leading whitespace contains a tab. A tab after the first non-space character of a line is content and passes through. ## API - `Yaml.decode(text: string): any` — decode a YAML document into a Luau value (table, scalar, or nil for an empty document). Raises on malformed input or an unsupported construct. - `Yaml.decodeWithComments(text: string): (any, boolean)` — decode as `decode` does, and report whether the text carries a comment by the decoder's own rule: a `#` at a line's start or after whitespace, outside a quoted scalar and outside a block scalar. Re-encoding the value drops such comments, so an editor that writes a document back reads this first. - `Yaml.encode(value: table): string` — encode a Luau table as YAML (block style, two-space indent, alphabetically sorted keys). Raises on values YAML can't represent (functions, userdata, non-string mapping keys). A top-level empty table encodes to an empty document, which decodes back to `nil` (the YAML empty-document ambiguity).
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Instance Color A lit white surface that every instance wearing it colours from its own shader-data lane 2, built on `@builtin::shaders.pbr` with `instance_color_lane = 2`. One material draws hundreds of members in hundreds of colours, and a colour that changes every frame costs one `renderer.instanceData.set` per member: a stress map over a bridge, a heat map, a damage flash, team colours, a selection highlight, a gauge. ```luau local LANE = 2 for i, id in members do local load = loads[i] -- 0 relaxed .. 1 at its limit renderer.instanceData.set(id, LANE, load, 1 - load, 0, 1) -- green to red, fully covered end ``` The lane's `rgb` is the colour and its `a` is how much of it covers the base colour and the emission, 0 none through 1 all; an instance whose lane was never written reads zero and draws plain white. The colour replaces the base colour before lighting, so the member still shades, casts its highlights and shows the lane colour in the `albedo` capture pass. Set `instance_emission` above 0 for a colour that glows where it covers. `renderer.mesh.drawInstanced` reads the same lane from its `instanceDataBuffer`, so a GPU-filled population colours each copy the same way. The writes are local to the engine that makes them and replicate nothing; `Model.tint` is the synced form of a tint, for a colour every participant must see. ## Inputs - `floats.instance_color_lane` (`2`): the lane read; lanes 0 and 1 carry a baked lightmap's rect and layer. - `floats.instance_emission` (`0`): how brightly the lane colour glows. - `colors.base_color` (white), `floats.roughness` (`0.5`), `floats.metallic` (`0`): the surface under the lane colour.
▲ 0↑ born
◐
material · born here
❒asset
# Emissive White A bright white light-emitting PBR surface built on the `pbr` shader with the `_EMISSION` keyword enabled. A white base colour (`1.0, 1.0, 1.0`) paired with a strong `emissive` (`20.0`) makes it glow and drive bloom — well suited to light fixtures, emitters, and glowing accents. Non-metallic (`metallic 0.0`) at mid roughness (`0.5`). ## Inputs - `floats.emissive` (`20.0`) — high emission strength that produces the glow. - `keywords: _EMISSION` — enables the emissive path in the PBR shader. - `colors.base_color` — the white surface and emission colour. - `floats.roughness` (`0.5`) — diffuse spread on the lit surface.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Water An animated water surface with a deep blue base colour (`0.1, 0.3, 0.8`) and low roughness (`0.2`) for glossy, reflective highlights, driven by a scrolling normal map for wave detail. Opaque, back-face culled, instanced, in the opaque queue (`2000`). Built on `@builtin::shaders.water_material`, sampling `@builtin/textures/water_normals`. ## Inputs - `colors.base_color` (`0.1, 0.3, 0.8`) — the water body colour. - `floats.roughness` (`0.2`) — low roughness for sharp specular reflections. - `floats.metallic` (`0.0`) — dielectric surface. - `textures.normal_texture` — the wave normal map (`@builtin/textures/water_normals`).
▲ 0↑ born
◐
material · born here
❒asset
# postReference_chrome A polished metal: its highlight of a bright light is a sub-pixel specular, the firefly a bloom and a temporal resolve are read on in the post-verification reference scenes.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Grass blades Swaying grass for a blade mesh, built on `@builtin::shaders.grass_material` with the same green as `@builtin::materials.grass` (`0.22, 0.62, 0.16`) and `sway` at `1.0`. The shader bends each vertex along its blade in proportion to `uv.y²`, so this material belongs on a mesh whose `uv.y` runs 0 at a blade's root to 1 at its tip — a merged field of blades, each a cross of two tapered quads. It inherits the Rust-side render defaults (opaque, back-face culled, queue `2000`) so its pipeline matches the library variant compiled at startup. ## Inputs - `colors.base_color` (`0.22, 0.62, 0.16, 1.0`) — the grass green tint. - `floats.sway` (`1.0`) — the full blade bend. `@builtin::materials.grass` leaves it at the shader's default of `0` for a ground surface.
▲ 0↑ born
◐
material · born here
❒asset
# Text Default The default surface for Text3D — a white base colour (`1.0, 1.0, 1.0`) drawn with alpha blending and no depth write so glyphs composite cleanly over the scene. Transparent, double-sided (`cull: none`), `depth_write: false`, in the transparent queue (`3000`). Built on the `pbr` shader. ## Inputs - `colors.base_color` (`1.0, 1.0, 1.0`) — the glyph colour. - `render.depth_write` (`false`) — glyphs blend without occluding each other. - `render.cull` (`none`) — text is visible from both sides. - `floats.roughness` (`0.5`) / `floats.metallic` (`0.0`) — dielectric defaults.
▲ 0↑ born
◐
material · born here
❒asset
# Grid Background A procedural background/reference-grid surface built on `@builtin::shaders.grid_bg`. It draws grid lines over a black base colour (`0.0, 0.0, 0.0`) with full roughness (`1.0`) and no metallic response, rendered opaque with culling disabled (`cull: none`) and instancing enabled so it can back many objects at once. ## Inputs - `colors.base_color` (`0.0, 0.0, 0.0, 1.0`) — black backdrop behind the grid lines. - `floats.roughness` (`1.0`) — fully diffuse, no highlights. - `render.cull` (`none`) — visible from both faces. - `render.instancing` (`true`) — supports instanced draws.
▲ 0↑ born
◐
material · born here
❒asset
# Standard Default The baseline PBR surface — a neutral white base colour (`1.0, 1.0, 1.0`), half roughness (`0.5`), non-metallic (`0.0`), wired with the full standard texture set (base colour, normal, roughness-metallic, emissive, occlusion) and the `_NORMALMAP` / `_OCCLUSIONMAP` keywords enabled. Opaque, back-face culled, instanced, in the opaque queue (`2000`). Built on `@builtin::shaders.pbr`. This is the starting point most materials copy and retint. ## Inputs - `colors.base_color` (`1.0, 1.0, 1.0`) — the albedo tint over the base colour texture. - `floats.roughness` (`0.5`) / `floats.metallic` (`0.0`) — dielectric, mid-gloss default. - `floats.normal_strength` (`1.0`) / `floats.ao_strength` (`1.0`) — normal-map and occlusion intensity. - `textures.normal_texture` / `textures.roughness_metallic_texture` / `textures.occlusion_texture` — the PBR map slots, seeded with neutral defaults.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# postReference_grey18 A lit 18% grey (middle grey) surface, rough and dielectric: what a light of known intensity is read off in the post-verification reference scenes.
▲ 0↑ born
◐
material · born here
❒asset
# Parallax Cube (Flat) A brick surface built on `@builtin::shaders.pbr_parallax_occlusion` with `parallax_depth_scale = 0`, so the ray-march is bypassed and the shared brick textures render as plain textured PBR (rough `0.7`, non-metallic, white base colour). Opaque, back-face culled, queue `2000`. Pair it with `parallax_cube` to A/B the parallax effect from a fixed viewpoint. ## Inputs - `floats.parallax_depth_scale` (`0.0`) — disables the occlusion ray-march, flattening the surface. - `floats.roughness` (`0.7`) — matte brick finish. - `floats.normal_scale` (`1.0`) — strength of the normal map. - `textures.height_texture` — the brick height map (unused while depth scale is `0`).
▲ 0↑ born
◐
material · born here
❒asset
# postReference_siemensStar A Siemens star of 36 spokes, 0.9 on 0.02: the spokes narrow below a pixel toward the centre, where a temporal resolve aliases. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
◐
material · born here
❒asset
# Skybox An unlit cubemap skybox rendered from the inside of the mesh, built on `@builtin::shaders.cubemap_unlit`. Uses front-face culling and no depth write so it draws behind all scene geometry (queue `1000`), sampling the built-in `Ryfjallet_cubemap` environment. ## Inputs - `textures.base_color_texture` — the cubemap sampled as the background (`Ryfjallet_cubemap`). - `floats.reflect_mode` (`0.0`) — samples the cube directly as a background rather than as a reflection. - `colors.base_color` (`1.0, 1.0, 1.0`) — tint applied to the sampled cubemap.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_greyWedge The grey wedge: one column per whole stop from -10 to +10, each middle grey (0.18) at that exposure. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Checkerboard White A static white/grey checkerboard rendered by `@builtin::shaders.checkerboard`, alternating a white base colour (`1.0, 1.0, 1.0`) with a dark grey check colour (`0.3, 0.3, 0.3`). Animation is disabled (`animation_speed 0.0`), giving a fixed, neutral grid — ideal as a level-design blockout or scale-reference floor. Opaque, non-metallic (`metallic 0.0`) at mid roughness (`0.5`). ## Inputs - `colors.base_color` — the white square colour. - `colors.check_color` — the dark grey square colour. - `floats.animation_speed` (`0.0`) — held at zero for a static pattern. - `floats.roughness` (`0.5`) — diffuse spread on the squares.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_white A flat unlit white of 0.8 light: the thin geometry of the temporal reference scene (wires, a fence, a fan's blades) drawn against the dark, so every pixel it touches stands out from its neighbours. Drawn by `@builtin::shaders.referenceChart`. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# parallax_cube Brick wall material using `@builtin::shaders.pbr_parallax_occlusion` with the parallax march enabled (`parallax_depth_scale = 0.08`, 48 ray-march layers). Pair with `parallax_cube_flat` for a same-textures, no-POM A/B comparison. ## Shader `@builtin::shaders.pbr_parallax_occlusion` — see that shader's README for the full parameter list. ## Textures All from `@builtin::textures.parallax_bricks` (height/normal derived from three.js's MIT-licensed `brick_bump` + `brick_diffuse`): - `base_color_texture` → `bricks_albedo` - `normal_texture` → `bricks_normal` - `roughness_metallic_texture` → `bricks_roughness` - `occlusion_texture` → `bricks_ao` - `height_texture` → `bricks_height` ## Usage ```luau Material.Apply(entityId, "parallax_cube") -- Override at runtime: Material.SetProperty("parallax_cube", "parallax_depth_scale", 0.12) ``` Pair it with `parallax_cube_flat` (same textures, `parallax_depth_scale = 0`) to compare the ray-marched surface against a flat-mapped baseline. ## Notes - `Material.SetProperty` mutations are global — every entity using `parallax_cube` sees the change. If you need per-entity variation, derive a new material name and override the property there. - Use under any standard camera (forward path). The deferred path is not yet POM-aware; materials applied to a deferred camera will render as plain textured PBR.
▲ 0↑ born
◐
material · born here
❒asset
# Neon A glowing cyan neon surface built on `@builtin::shaders.pbr` with emission enabled. It pairs a bright cyan-blue base colour (`0.2, 0.8, 1.0`) with a matching emissive colour driven hard by `emissive: 5.0`, so it reads as a self-lit light source. Low roughness (`0.2`), non-metallic, opaque, instanced, with the `_EMISSION` keyword enabled. ## Inputs - `colors.emissive` (`0.2, 0.8, 1.0, 1.0`) — cyan glow colour. - `floats.emissive_intensity` (`5.0`) — strong emissive intensity (feeds bloom). - `colors.base_color` (`0.2, 0.8, 1.0, 1.0`) — matching cyan base tint. - `floats.roughness` (`0.2`) — glossy surface under direct light.
▲ 0↑ born
❒
package · born here
❒asset
# oceans A sea and the ground under it, as one package: the wave simulation, the surface that renders it, the seabed it is seen through, and the answer to "how high is the water here" that gameplay needs to float things on it. ```lua local Ocean = require("@builtin::systems.oceans.modules.ocean") Ocean.create({ preset = "gale" }) ``` That is a whole ocean — waves out to the horizon, foam on the breaking crests, a seabed below, and a surface that follows whoever is looking at it. To float something on it: ```lua local ocean = Ocean.forEntity(oceanEntityId) boat.position = { x, ocean:sampleHeight(x, z), z } ``` It is a building block. The sun the water glitters under, the ambient its foam sits in and the sky it reflects all come from the scene, through the same directional light and reflection probes every other material in the engine reads — so an ocean drops into a world that already has lighting instead of bringing a second set, and changing the scene's light changes the water with it. ## What it does The simulation is **spectral**: instead of animating individual waves, it builds the sea's energy across every wavelength and direction from an oceanographic spectrum, then inverse-transforms that into a height field. A real sea is what comes out — wind speed alone decides wave height, and it rises with wind the way a fully developed sea does, following the Pierson–Moskowitz law. It runs in **cascades** — independent wave fields over different patch sizes, each band-limited to its own range of wavelengths. A tile of roughly two kilometres carries the long waves and one of a few hundred metres the short ones; they sum into a surface with detail at every scale, and because the bands are disjoint no wavelength is synthesised twice. Above them a **Gerstner bank** adds the swell analytically, which no cascade can hold: a wave longer than its own patch has nowhere to live, and swell arrives from a distant storm on its own heading anyway. **Foam** is not painted on. It comes from the Jacobian of the wave displacement: where the surface folds over itself, the wave is breaking. Whitecaps appear where and when the geometry actually breaks, accumulate into a persistent world-space field, and streak behind the crest as they decay. **Colour and transparency are the same mechanism.** Per-channel Beer–Lambert extinction over the distance light travelled through water gives both — red dies within a metre, blue survives tens. That is why shallow water over sand reads turquoise and the open sea reads near-black blue without switching materials. **The seabed** is one noise field read twice: scaled it is the terrain's relief, unscaled it picks between sand and rock, so outcrops stand on the high ground and sand fills the hollows. The sun's caustic web moves over both, bent by the horizontal displacement of the water directly above it. Bodies land on it: the pass that raises the ground writes the same field out as a height patch that travels with the viewer, so what a body stands on is what the frame drew. Nothing here ships an image. Every map the surface and the seabed sample — the foam patterns, the sand, the rock, the caustics — is grown on the GPU at startup. ## The pieces | Asset | What it is | |---|---| | `components/Ocean` | Puts a sea on an entity. Its fields are the sea state; the simulation is rebuilt from them, so an ocean survives a scene reload. | | `modules/ocean` | The substrate: `attach`, `create`, `forEntity`, `sampleHeight`, `sampleDisplacement`. | | `components/Buoyancy` | Floats a rigid body on any water, inside the physics step. Its fields are the hull: how dense it is, how quickly it settles, and what displaces water (its colliders, a stated box or what it draws). | | `modules/buoyancy` | The float: `attach`, `forEntity`, `list`, `clear`, `submergedFraction`, `restingY`. | | `modules/oceanPresets` | Nine named sea states, from `calmClear` through `gale`. | | `modules/oceanQuality` | The four tiers — how finely a sea is resolved, as distinct from what sea it is. | | `modules/oceanClipmap` | The camera-following ring geometry both the surface and the seabed are built on. | | `modules/oceanFloor` | The seabed: terrain, ground materials, caustics and the ground physics collides with. | | `modules/oceanScene` | The seam between the scene capture and the materials that sample it. | | `shaders/oceanSurface` | The water: cascade normals, absorption, exact dielectric Fresnel, subsurface scattering, sun glitter, foam, and the view from below. | | `shaders/oceanFloor` | The seabed: sand and rock blended by terrain noise, under a caustic web. | | `materials/oceanWater`, `materials/oceanSeabed` | The default looks. | | `renderFeatures/oceanScene` | Copies the frame's colour and depth so the surface can refract what is behind it and measure its own water column. | | `computeShaders/ocean_*` | The ten-stage GPU pipeline. | ## Wind is the control Significant wave height goes as the **square** of wind speed, so that one field does most of the work: | Wind | Sea | Wave height | |---|---|---| | 3 m/s | glassy, a mirror | ~0.2 m | | 11 m/s | working sea, occasional whitecap | ~2.5 m | | 24 m/s | gale, breaking and streaked | ~12 m | Everything else shapes character rather than size: `choppiness` pinches crests into peaks, `standingWaveRatio` runs waves both ways instead of only downwind, `jonswapGamma` sharpens a fetch-limited sea, and the foam layers decide how readily a breaking crest whitens and how long it streaks. Read `documentation/oceans.guide` for the full walkthrough, including the seabed, buoyancy, reflections and what each tier costs.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_checker A checker of 64 cells across, 0.8 on 0.03: the high-frequency background a fast object crosses. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# postReference_colorChecker The 24 patches of the ColorChecker Classic, each its published sRGB value read as linear reflectance. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Error Test Intentionally broken material used to exercise the renderer's error-handling and fallback paths. It targets `@builtin::shaders.error_shader` and carries a bright magenta base colour (`1.0, 0.0, 1.0`) — the classic "missing/failed" flag colour — with mid roughness (`0.5`), opaque render type, and back-face culling. ## Inputs - `colors.base_color` (`1.0, 0.0, 1.0, 1.0`) — magenta error flag tint. - `floats.roughness` (`0.5`) — neutral surface roughness. - `render.type` (`opaque`) / `render.queue` (`2000`) — drawn in the standard opaque pass.
▲ 0↑ born
◐
material · born here
❒asset
# Animated Color An opaque, back-culled surface whose colour animates over time, driven by `@builtin::shaders.animate_shader`. Starts from a white base colour (`1.0, 1.0, 1.0`) with an `emissive` of `1.0` so the shifting colour reads brightly, at mid roughness (`0.5`) and fully non-metallic (`metallic 0.0`). ## Inputs - `colors.base_color` — the starting colour the animation modulates. - `floats.emissive` (`1.0`) — self-illumination strength of the animated colour. - `floats.roughness` (`0.5`) — matte diffuse spread. - `floats.metallic` (`0.0`) — dielectric, non-metallic surface.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
❒
bundle · born here
❒asset
# Error Text A 3D "ERROR" text model used as the visible fallback when an asset fails to load — spawned in place of missing geometry so a broken reference is obvious in-scene instead of silently invisible. Decomposed from `ERRORText.fbx` into this bundle (mesh + material + texture + entity template). ## Composition - Root `error_text` → `Sketchfab_model` → the `ERRORText.fbx` node hierarchy. - Carries the extruded "ERROR" letter mesh, its material, and texture as private subassets. ## Usage Spawned automatically by the engine as the placeholder for an unresolved asset; can also be spawned directly to test the fallback path.
▲ 0↑ born
◐
material · born here
❒asset
# Plastic White A clean white plastic — a non-metallic PBR surface with a pure white base colour (`1.0, 1.0, 1.0`) and mid roughness (`0.5`) for a soft dielectric sheen. Built on `@builtin::shaders.pbr`. Opaque, back-face culled, queue `2000`. ## Inputs - `colors.base_color` (`1.0, 1.0, 1.0`) — the white plastic tint. - `floats.roughness` (`0.5`) — soft, semi-glossy highlight. - `floats.metallic` (`0.0`) — dielectric (non-metal) response. - `floats.emissive` (`0.0`) — no self-illumination.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_darkRamp A ramp of light from 0 at the left edge to 0.1 at the right: the dark gradient an undithered 8-bit display path bands on. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
◐
material · born here
❒asset
# Lit Checkerboard A static, lit checkerboard surface built on `@builtin::shaders.lit_checkerboard` — the default ground material for newly seeded worlds. It uses a neutral light-grey base colour (`0.85, 0.85, 0.85`) with a grey emissive tint (`0.35, 0.35, 0.35`) so the pattern stays readable in dim scenes, high roughness (`0.7`), no metallic, opaque, and instanced. ## Inputs - `colors.base_color` (`0.85, 0.85, 0.85, 1.0`) — neutral grey checker palette. - `colors.dark_color` (`0.35, 0.35, 0.35, 1.0`) — the dark square colour, close enough to the base that the pattern stays readable in low light. - `floats.roughness` (`0.7`) — mostly diffuse surface. - `render.instancing` (`true`) — supports instanced draws.
▲ 0↑ born
◐
material · born here
❒asset
# Decal Default The default material a `DecalProjector` stamps when none is assigned. A flat cyan tint (`0.05, 0.95, 1.0`) on `@builtin::shaders.pbr`. Assign a material with a `base_color_texture` to project a texture; the projector samples the material's base colour at the box-local XY of every surface inside its projection box.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Sky Equirectangular An equirectangular panorama sky/environment background built on `@builtin::shaders.equirect_sky`. Renders with no culling and no depth write behind all geometry (queue `1000`), with adjustable rotation and exposure. Assign an equirectangular image to `sky_texture` (via `material.setTexture` or a Skybox) to display a panorama. ## Inputs - `textures.sky_texture` — the equirectangular panorama image to sample. Falls back to black, so a sky with nothing assigned reads as night rather than as an error. Assign a `.texture` by guid, identity, name, or path: ```luau renderer.material.setTexture("@builtin::materials.sky_equirect", "sky_texture", "my_panorama.texture") ``` An image dropped into `/zero/source` imports into a sibling `<stem>.texture` container and the loose file is removed, so reference the container (or its identity) rather than the original image path. - `floats.rotation` (`0.0`) — yaw rotation of the panorama around the vertical axis. - `floats.exposure` (`1.0`) — brightness multiplier applied to the sampled colour.
▲ 0↑ born
◐
material · born here
❒asset
# error_marker The material of the ERROR sign a Model or SkinnedModel shows in place of a mesh that failed to resolve, to become GPU-resident, or to pose. It draws the `error_marker` shader's magenta checker.
▲ 0↑ born
◐
material · born here
❒asset
# Instance Color Unlit An unlit white surface that every instance wearing it colours from its own shader-data lane 2, built on the `unlit` shader with `instance_color_lane = 2`. It draws exactly the picture `Model.tint` gives an unlit surface for the same colour and blend, at the cost of one `renderer.instanceData.set` per member and with nothing replicated, so it is the material for flat per-instance colour that changes every frame: a heat map, a stress readout, indicator lights. ```luau renderer.instanceData.set(id, 2, 1, 0, 0, 1) -- this member red, fully covered renderer.instanceData.set(id, 2, 1, 0, 0, 0) -- back to the material's white ``` The lane's `rgb` is the colour and its `a` is how much of it covers the material's own colour, 0 none through 1 all; a lane never written reads zero and leaves it white. Set `instance_emission` above 0 to add the colour again on top, scaled, for a colour that blooms. ## Inputs - `floats.instance_color_lane` (`2`): the lane read. - `floats.instance_emission` (`0`): how much of the lane colour is added on top where it covers. - `colors.base_color` (white) over `textures.base_color_texture` (`default:white`): the colour under the lane colour.
▲ 0↑ born
◐
material · born here
❒asset
# Brushed Metal A brushed metallic surface built on the `pbr` shader — fully metallic (`metallic: 1.0`) with a neutral grey base colour (`0.6, 0.6, 0.6`) and moderate roughness (`0.3`) that softens reflections into the muted sheen of brushed steel or aluminium. Opaque, back-face culled, instanced, with the `_METALLICSPECGLOSSMAP` keyword enabled. ## Inputs - `colors.base_color` (`0.6, 0.6, 0.6, 1.0`) — neutral metal grey. - `floats.metallic` (`1.0`) — fully metallic. - `floats.roughness` (`0.3`) — softened, brushed reflections. - `textures.base_color_texture` (`default:white`): the white base texture the colour multiplies against, so the surface renders `base_color` alone.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_emitter A flat emitter answering with 16 times the display's white: the source a bloom reading is taken around. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
◐
material · born here
❒asset
# Sky Solid A flat solid-colour background built on `@builtin::shaders.solid_sky`. Renders with no culling and no depth write behind all geometry (queue `1000`), filling the sky with a single dark-grey colour (`0.2, 0.2, 0.2`) at unit exposure. ## Inputs - `colors.solid_color` (`0.2, 0.2, 0.2`) — the flat fill colour of the background. - `floats.exposure` (`1.0`) — brightness multiplier applied to the fill colour.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Grass A soft green ground surface built on `@builtin::shaders.grass_material`, with a natural green base colour (`0.22, 0.62, 0.16`), high roughness (`0.75`) for a diffuse, non-shiny look, and no metallic or emissive contribution. It inherits the Rust-side render defaults (opaque, back-face culled, queue `2000`) so its pipeline matches the library variant compiled at startup. It leaves the shader's `sway` at its default of `0`, so it shades whatever geometry it is applied to and leaves every vertex where the mesh puts it; `@builtin::materials.grass_blades` is the same green on a blade mesh that bends. ## Inputs - `colors.base_color` (`0.22, 0.62, 0.16, 1.0`) — the grass green tint. - `floats.roughness` (`0.75`) — matte, diffuse surface. - `floats.metallic` (`0.0`) / `floats.emissive` (`0.0`) — non-metallic, non-glowing.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Cubemap Env A chrome-ball environment surface that samples a cubemap for mirror-like reflections, built on `@builtin::shaders.cubemap_unlit`. `reflect_mode` (`1.0`) selects the reflection path, and the `Ryfjallet_cubemap` builtin texture supplies the environment. Opaque, with a white base colour (`1.0, 1.0, 1.0`) so the cubemap colours pass through untinted. ## Inputs - `textures.base_color_texture` (`@builtin/textures/Ryfjallet_cubemap`) — the environment cubemap that is reflected. - `floats.reflect_mode` (`1.0`) — selects the reflection sampling mode. - `colors.base_color` — white tint that leaves the reflected colours unchanged.
▲ 0↑ born
◐
material · born here
❒asset
# Solid Line A flat, opaque line surface with a bright green base colour (`0.0, 1.0, 0.5`) for drawing gizmos, paths, and debug geometry. Opaque, back-face culled, instanced, and rendered in the opaque queue (`2000`). Built on `@builtin::shaders.line_material`. ## Inputs - `colors.base_color` (`0.0, 1.0, 0.5`) — the line colour. - `floats.roughness` (`0.5`) — surface roughness. - `floats.emissive` (`0.0`) — no self-illumination. - `render.instancing` (`true`) — many lines draw in a single instanced batch.
▲ 0↑ born
◐
material · born here
❒asset
# Glass Clear glass — a smooth, transparent PBR surface with a faint blue tint (`0.9, 0.95, 1.0`) and low alpha (`0.3`). Rendered in the transparent queue with backface culling off (`cull: none`) so both faces of thin geometry show. Built on `@builtin::shaders.pbr`. ## Inputs - `colors.base_color` — tint and opacity (the alpha channel drives how much shows through). - `floats.roughness` (`0.0`) — perfectly smooth; raise for frosted glass.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Default The neutral fallback surface — a plain white, non-metallic PBR material with mid roughness (`0.5`), sampling the built-in white texture. This is what an object renders as when no material is assigned. Built on `@builtin::shaders.pbr`. ## Inputs - `colors.base_color` — the flat tint (white by default). - `floats.roughness` (`0.5`) / `floats.metallic` (`0.0`) — a neutral matte dielectric. - `textures.base_color_texture` — the built-in `default:white` texture.
▲ 0↑ born
◐
material · born here
❒asset
# Triplanar Checker A checkerboard surface projected triplanarly, so the pattern stays uniform across arbitrary geometry with no UVs and no stretching. Neutral white base colour (`1.0, 1.0, 1.0`), half roughness (`0.5`), non-metallic, with a projection sharpness of `4.0` controlling how tightly the three axis projections blend at edges. Opaque, back-face culled, instanced, in the opaque queue (`2000`). Built on `@builtin::shaders.triplanar`, sampling `@builtin/textures/array_texture`. ## Inputs - `floats.triplanar_sharpness` (`4.0`) — blend tightness between the X/Y/Z projections. - `colors.base_color` (`1.0, 1.0, 1.0`) — tint over the projected texture. - `textures.base_color_texture` — the projected checker source (`@builtin/textures/array_texture`). - `floats.roughness` (`0.5`) / `floats.metallic` (`0.0`) — dielectric, mid-gloss default.
▲ 0↑ born
▣
module · born here
❒asset
# shader_includes Module Resolves the `#include` lines of a WGSL source against the asset system, so a shader includes a `.shaderModule` by every name form `require` and `asset.resolve` accept. ## Purpose A shader names a module the way any content names any asset — a guid, an identity, a package-relative `~.tail`, a `.relative.tail`, an `@root::` or `@builtin::` path. The engine's include expansion is a flat key lookup, so a relative form means nothing on its own: `~.tail` is a different module depending on which package asked for it. This module closes that gap in the one place that knows both — the including asset, which holds its own VFS path. Every literal is resolved against that path and rewritten to the module's guid before the source reaches the compiler, so a relative include resolves exactly where a relative `require` would. The rewrite applies to the source handed to the compiler. What the author wrote stays on disk, which is what the reference extractor records, with its line, when the file is written. ## Usage An assetType canonicalizes its own WGSL, passing the asset's path as the base the relative literals expand against: ```luau local ShaderIncludes = require("modules.shader_includes") local wgsl = ShaderIncludes.canonicalize(vfs.read(path .. "/shader.wgsl"), path) ``` Given `path` inside a package, both of these reach the compiler as the same guid-keyed include: ```wgsl #include "~.ssr_common" #include "@builtin::systems.reflections.ssr_common" ``` A literal that resolves to no asset is left as it was written, so a framework name — `zero::surface` and its siblings, which are registry keys rather than assets — reaches the expander untouched. Source carrying no `#include`, or given no base path, is returned unchanged.
▲ 0↑ born
❒
package · born here
❒asset
# deprecatedParticles (deprecated) > **Deprecated.** This belongs to the earlier particle substrate. It stays loadable by identity and guid for content that already uses it; nothing new should use it, and search does not offer it. Particles are procgen sim nodes (`sim.spawn`, `sim.particles`, `sim.shader`) played through an effect: use `@builtin::assetTypes.effect`. The package holds the GPU particle engine module (`engine`), the `deprecatedParticles` module over it, the `DeprecatedParticleEmitter` and `DeprecatedSimpleParticleEmitter` components, the shaders, compute shaders and materials they draw with, and a demo scene. The package's `.metadata` carries the deprecation marker, so every asset inside is deprecated.
▲ 0↑ born
◐
material · born here
❒asset
# Plastic Red A glossy red plastic — a non-metallic PBR surface with a saturated red base colour (`0.8, 0.1, 0.1`) and mid roughness (`0.5`) for a soft dielectric sheen. Built on `@builtin::shaders.pbr`. Opaque, back-face culled, queue `2000`. ## Inputs - `colors.base_color` (`0.8, 0.1, 0.1`) — the red plastic tint. - `floats.roughness` (`0.5`) — soft, semi-glossy highlight. - `floats.metallic` (`0.0`) — dielectric (non-metal) response. - `floats.emissive` (`0.0`) — no self-illumination.
▲ 0↑ born
◐
material · born here
❒asset
# Cel Shaded A stylised, toon-shaded opaque surface built on `@builtin::shaders.extended_material`, with a warm red-orange base colour (`0.9, 0.3, 0.2`). The sun's light is cut into flat bands that multiply the base colour, so every band renders the hue of `base_color` and only its brightness steps; ambient light lifts the side facing away from the sun in the same hue. High roughness (`0.8`) keeps the surface matte: its specular term stays below the first band, so each lit band is one flat colour. ## Inputs - `colors.base_color`: the surface colour every band renders in, multiplied by `base_color_texture`. - `floats.roughness` (`0.8`): the size and strength of the specular highlight, which is banded with the rest of the sun's light; 0 is a small bright highlight. At four bands, a roughness of 0.5 or more keeps the highlight below the first band, so none shows. - `floats.quantize_steps` (`4`): the number of bands the sun's light is cut into; 0 shades smoothly. - `textures.base_color_texture`: an optional surface texture, drawn multiplied by `base_color`. Unset, it is white, and the surface renders `base_color` alone.
▲ 0↑ born
✦
shader · born here
❒asset
# eye.shader An eye: an iris refracting behind a clear cornea. The iris sample is displaced by refracting the view ray at the cornea and walking it `iris_depth` inward, so the iris slides as the viewing angle changes. The cornea itself is the engine's clearcoat lobe, which keeps one continuous specular highlight across the iris and the sclera. `iris_radius`, `pupil_radius` and `limbus_softness` shape the disc; `iris_texture` is sampled in the refracted frame, `sclera_texture` in the surface's own UVs.
▲ 0↑ born
✦
shader · born here
❒asset
# billboard Camera-facing billboard shader for textured quads — labels, sprites, and 3D text. All four corners of the quad sit at the same anchor, so the vertex's world position is the anchor under any entity or parent transform. Each corner carries its in-plane offset in the vertex colour; the vertex stage offsets the corner in world space facing whichever camera renders the pass, so the quad is visible and correctly framed from the main viewport, an offscreen capture, and a render-texture camera alike. Because the offset is applied in world space before projection, the quad keeps real extent and stays drawn when the anchor leaves the frustum. The horizontal axis stays world-level, so it tracks a pitching camera without rolling. The world size lives in the per-corner offset (resizing rebuilds the vertex colours), so there are no size material properties, and the mesh declares its own render bounds so frustum culling keeps it. The fragment mirrors `unlit`: `base_color * base_color_texture` with masked-transparency cutoff.
▲ 0↑ born
▣
module · born here
❒asset
# assetType runtime The machinery that reads a `<typename>.assetType/behavior.luau` and turns its declarations into live behaviour. A `behavior.luau` is a table of declarations — `ref` methods, `modules` shared code, an `events` schema, a `namePattern`, `refShapes`, and the `onCreate` / `onRegister` / `onChange` / `onDelete` / `validate` / `settingsDefaults` / `settingsCorrections` hooks. None of it runs itself. This is the code that runs it, and it lives here because reading a type's declarations is the **assetType type's own behaviour** — the same way `material.assetType` owns what a `.material` does. ## Sub-modules | Module | Reads | Responsibility | |---|---|---| | `ref.module` | `ref`, `modules` | Builds every `AssetRef` envelope, attaches the shared metatable, and dispatches per-type methods and shared modules onto it. Also owns edit-mode write-behind persistence. | | `create.module` | `onCreate`, `namePattern`, `validate` | Backs `asset.create`: resolves the type, checks the name against its pattern, runs `onCreate` (or clones `template/`), and writes the instance. `validate.module` beside it checks a creation `opts` table against the type's `createSchema`. Before running `onCreate`, resolves the type's settings chain (below) and hands the result to the hook as `opts.settings`. | | `settings.module` | `settings`, `settingsDefaults`, `settingsCorrections` | A type's declared settings schema and where its values live (`type.yaml`'s `settings:` block). `Settings.resolveForCreate` layers the schema's own defaults, the type's own `settingsDefaults(name, opts, asked)` hook (a DEFAULT the schema can't express — a name-derived convention, a fact read off a payload the caller handed it, a value following a field `asked` shows a higher layer naming; any later layer overrides it), the Preset Manager, an importer's settings, the caller's `settings` table, then the type's own `settingsCorrections(values, name, opts)` hook (a CROSS-FIELD CONSTRAINT the schema can't express, applied last over the fully merged values). A correction over a default/preset/importer value lands silently; a correction that would override what the caller's own `settings` table explicitly asked for refuses the creation instead, naming the field and why. Neither hook is ever handed a table to mutate; each returns what it wants changed (`settingsCorrections` may also return a `{ [field]: reason }` table for that refusal message). `Settings.cloneValues` deep-copies a values table — `create.module` hands `onCreate` a copy, never the table it stamps. `Settings.valuesOf` / `writeValues` read and write an asset's own values file. | | `typeYaml.module` | `type.yaml` | The folder a type lives in and its parsed `type.yaml`, decoded once per content version of the file. `settings.module` and `inspector.module` read their blocks through it. `about` / `aboutAt` read a type's `about.yaml` (`intent`, `function`, `usage`), leaving out any key that is empty or still starts with `[[describe]]`; `aboutRows` and `aboutMarkdown` lay the texts out for the Inspector and the `types/<name>` guide. | | `inspector.module` | `type.yaml`'s `inspector:` block | Which inspector a type declares for its assets (`inspector: { type: ... }` or `{ instance: ... }`), and the declaration that file returns — the sections it adds, the generic ones it replaces, and their order — or the problem that kept it from loading. | | `onRegister.module` | `onRegister` | Fires an instance's once-only registration hook — on arrival, on world load, and for `@builtin` library instances. | | `changeDispatch.module` | `onChange`, `onDelete`, `refShapes` | Routes a VFS source write or a folder delete to the enclosing typed asset's type hooks. | | `refShapes.module` | `refShapes`, `ref` | Publishes what every `AssetRef<category>` answers to, so a member read on an asset-typed value is checked against that category's real surface. | | `events.module` | `events` | The per-asset event runtime behind `ref.events`. | | `instantiable.module` | `ref.instantiate` | The instantiation contract an asset type opts into: `root` stands the root entity, `place` applies the base placement opts to one the type adopted, and `result` shapes the `(root, idMap)` every `instantiate` returns. Also registers the `sceneInstantiable` field-constraint validator. | | `declaration.module` | `type.yaml` | The type's decoded declaration, read once and held until a write inside an `.assetType` folder; what every question about a type as a whole reads. | | `previewRecipe.module` | `type.yaml` `preview:` | How an instance's `preview.png` is produced: which of the four recipes (`instantiate`, `image`, `surface`, `custom`) the type declares, and what that recipe needs. | ## Reaching it ```luau local rt = require("@builtin::assetTypes.assetType.shared") rt.ref.loadTypeBehavior("material") ``` Fields resolve lazily. The prelude requires the sub-modules directly, in dependency order (`ref`, then `changeDispatch`, `onRegister.install()`, `refShapes.install()`) — a table that required all six on load would take that ordering away from it.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.** - `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) -- `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. 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. 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`, `topology` and `queue` (the draw order, a whole number: `2000` opaque, `3000` transparent, `4000` overlay). 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. A texture in `base_color_texture` is multiplied by `base_color`: every built-in surface shader declaring both draws the texture's colour times `base_color`, channel by channel. Under the default white `base_color` the texture shows as itself, and a coloured `base_color` tints it. The two read a colour differently: `base_color` is linear, and the slot reads its texels as sRGB-encoded, a `color:` value's included, the way it reads an image. A colour stated in both places is therefore multiplied by a darker copy of itself: `color:0.62,0.14,0.05,1` in the slot decodes to `0.34, 0.017, 0.0039`, so beside `base_color` `0.62, 0.14, 0.05` the surface draws `0.21, 0.0024, 0.0002`, far darker than either. State a solid colour once, as `base_color`, and leave the slot at its default white. 6. **Hot reload.** Editing `mat.yaml` repacks the GPU uniform buffer on the next frame; visible immediately. **Properties that follow a named value.** `values:` binds a property to a named value of the entity wearing the material (`values.set(entity, name, v)`, the `topics/values` guide): `property: valueName`, or `property: { value: "name", scale: s, offset: o }`, and the property draws `value * scale + offset` on each entity holding the value, read on the draw. A boolean reads as 1 or 0, and a number fills every channel of a colour or vector property. The wearer's own value is read, else its nearest ancestor's, so a lamp mesh under a car lights from the car's `brake`; a wearer holding none draws the material's own value. A scalar, vector, colour or bool property binds (at most 32 per material); a texture slot does not. `asset.create("material", name, { values = { ... } })` and `renderer.material.create({ ..., values = { ... } }, key)` take the same map. ```yaml values: emissive_intensity: { value: "brake", scale: 3 } ``` 7. **Inspector.** `type.yaml` names `inspector: { type: "inspector.luau" }`, and `inspector.luau` declares the material's sections beside the generic ones: `shader` (the bound shader, swappable), `properties` (each shader field as its typed control) and `textures` (each texture slot as an asset chip). They read the material through `mat:getShaderRef()` (the bound shader's ref, `~` expanded) and `mat:getTextures()` (slot name → `{ ref, name }`: the bound texture's guid and the id the material names it by). An edit there is an authoring write: `mat:writeProperty(name, value)` and `mat:writeTexture(slot, texture)` write the value into `mat.yaml` (and drop the session's own value for it, so the material reads what the file holds), and `setShader` rewrites the file; inside an undo operation each file write is recorded, so undo puts `mat.yaml` back. `setProperty` / `setTexture` stay runtime writes that last until the next mode change. ## Discovery - `asset.list("material")` — every registered material. - `asset.inspect("<name>")` — shader binding, property values, source path, this type README. - `ls /zero/source/<Name>.material` lists the files the folder holds. - `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. - **`base_color_texture` multiplies `base_color`.** A `color:` texture holding the same value as `base_color` multiplies the colour by its own sRGB-decoded value, so the surface renders far darker than the value written. Write a solid colour as `base_color` alone. - **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.** A `Model` component or other reference to the material is recorded against the material itself, so renaming or moving it does not break them. ## 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.
▲ 0↑ born
◇
component · born here
❒asset
# ScreenSpaceReflections Reflections off the scene's own drawn frame, as a one-feature component. A scene lists its features together, each with its settings, on `RenderFeatures` (`{ feature = "@builtin::systems.reflections.ssr", settings = { ... } }`); this component carries the same knobs as fields for one feature. ```lua local id = entity.spawn("reflections").id entity(id).component.add("ScreenSpaceReflections", { intensity = 1.0, maxDistance = 40, quality = "high", }) ``` Polished floors start reflecting what stands on them, wet ground reflects what is above it, and metal picks up its surroundings — all from geometry the camera can already see, with nothing authored per surface. Roughness decides who gets them. A screen-space ray traces one direction, which is a mirror; `roughnessCutoff` is where that stops being a fair stand-in for a surface's real lobe, and past it the pass leaves the surface alone. Every field is live: edit one in the inspector and the reflections follow. Setting `intensity` to 0 turns them off without losing the rest of the settings. Several of these in one scene share one set of reflections: the one edited last is in force, removing it hands the reflections to the newest of the others, and only removing the last one takes them down. While one stands in the scene, the reflections come back if something else takes them down. How much a reflection shows through is the surface's own Fresnel term, so metal takes almost all of it and a dielectric floor takes a few percent head-on and more at a grazing angle — the same way a wet road reads dry underfoot and mirror-like down the street. For code that adjusts reflections at runtime, reach for `@builtin::systems.reflections.ssr` — same options, no component. The full description of what each knob does and how the passes work is in the package readme: `guides { path = "systems/reflections" }`.
▲ 0↑ born
·
scene · born here
❒asset
# Polished floor A polished stone floor with three coloured pillars standing on it, under an afternoon sky, with screen-space reflections listed on the scene's `RenderFeatures` entity — so the floor reflects the pillars, and a player walking across it, rather than the sky alone. Load `@builtin::systems.reflections.demo.polished_floor` with the `scene.load` tool. Players arrive facing the pillars from floor height, which is the grazing angle a polished dielectric reflects at most strongly. ## What it shows - **The feature on one entity.** `render_features` carries a `RenderFeatures` list with `@builtin::systems.reflections.ssr` at `intensity = 1`, `maxDistance = 60`, `quality = "high"`. Delete the entity and the floor mirrors the sky alone; put it back and the pillars stand in the floor. - **Only what the camera sees.** A pillar's reflection comes from the frame the camera drew, so turning away from a pillar takes its reflection with it, and the reflection fades toward the screen edge rather than cutting off. - **The environment underneath.** Where a ray finds nothing the floor keeps the sky reflection the environment gives it, at the same mip and Fresnel weight, so the seam between the two is a change of content and not of brightness. - **Roughness decides who reflects.** The floor sits at roughness 0.06, the pillars at 0.55 — above the feature's `roughnessCutoff` — so the floor is the one surface tracing reflections in the scene. ## Contents - `floor` — a 60 m slab of `PolishedStone`, a near-black dielectric at roughness 0.06, with a collider. - `pillar_1..3` — 1.6 × 5 × 1.6 m boxes in `PillarRed`, `PillarGreen`, `PillarBlue`, sunk into the slab. - `sun`, `ambient`, `sky` — a side-lit afternoon under a `ProceduralSky`. - `render_features` — the `RenderFeatures` entity this scene exists for. - `PlayerSetups` / `Spawns` — the humanoid player on an orbital-follow camera, spawning in front of the pillars. The scene's player intent is `"spawns"`. ## Entry point - `entrypoint.luau` — nothing to set up: the reflections run from the `RenderFeatures` entity the moment the scene loads.
▲ 0↑ born
▣
module · born here
❒asset
# ssr Screen-space reflections — polished floors reflect what stands on them, from the frame the engine has already drawn. ```lua local ssr = require("@builtin::systems.reflections.ssr") ssr.set({ intensity = 1, maxDistance = 40, quality = "high" }) ssr.get() --> the settings in force ssr.active() --> are the reflection passes running ssr.clear() --> off, settings kept for the next set() ``` `set` takes any subset — omitted fields keep their current value, so adjusting one knob does not mean restating the rest. An `intensity` of 0 turns reflections off and releases the passes rather than running them at zero strength. `roughnessCutoff` is the field that decides who gets reflections at all. A screen-space ray traces one direction, which describes a mirror; past the cutoff a real surface scatters over a lobe too wide for one ray to stand in for, and the pass leaves those surfaces alone. `maxDistance` and `quality` trade against each other. Quality sets how many steps the march spends, so a long distance at a low quality samples coarsely — reflections thin out at range rather than disappearing, because refinement adapts to the step size on its own. For reflections a scene should simply have, author the `ScreenSpaceReflections` component instead — it carries the same settings, persists with the scene, and edits live in the inspector. This module is for code that changes reflections at runtime: a quality slider, a cutscene, weather turning pavement wet. The full description of what each knob does and how the passes work is in the package readme: `guides { path = "systems/reflections" }`.
▲ 0↑ born
·
renderfeature · born here
❒asset
# ssr The render feature behind `ScreenSpaceReflections` and `@builtin::systems.reflections.ssr`. One compute pass runs at the `afterDepthPrepass` seam, once the visibility raster has laid the frame's depth and before anything shades: | order | pass | output | |---|---|---| | 60 | `ssr_trace` | reflected colour plus a confidence weight, per pixel, into `@scene.reflection` | That channel is what a material's environment term reads the same frame, at its own pixel: the traced reflection stands in for the probes and the sky to the degree the trace trusts it, and the material's own Fresnel, roughness and tint weigh it into the pixel the way they weigh any environment. Nothing here touches the lit picture. The trace marches against this frame's depth, which the visibility raster has just written, so the reflection stands where the camera is now. What it reads about the surfaces — the reflecting surface's normal and roughness, the hit's normal and its colour — comes from the previous frame: the G-buffer as that frame left it (`readsPrevious`), and `@scene.colorHistory`, the light it ended with, each reprojected through the previous view-projection `@frame.camera` carries in its rows `[5..9)`. A point the previous frame did not show reflects its environment alone. ## The march `ssr_trace` reflects the view ray about the surface normal and walks it through the depth buffer. Steps are uniform in **screen** space, not world space. A ray heading toward the horizon covers enormous world distance across a few pixels, so stepping in world units there means either missing every thin occluder near the camera or spending the whole budget on distance nothing can be seen at. The world parameter is recovered from the screen parameter through `1/w`, which is what interpolates linearly under perspective. At each step the ray's world position is compared against the world position reconstructed from the depth sample under it. Comparing in world space is what lets `thickness` mean a distance in world units rather than a depth-buffer epsilon whose meaning changes with range. The march looks only for the **crossing** — where the ray first passes behind the depth buffer. Thickness is tested after bisecting that crossing, not during the march: one march step covers roughly as much depth as a surface is thick, so a ray tested during the march can jump from in front of a surface to well behind it without ever landing inside it. Bisection runs for whichever is larger, the quality tier's budget or however many halvings bring one march step under `thickness`. A coarse march that stopped bisecting early would leave the crossing bracketed more loosely than a surface is thick, and every hit would then fail the thickness test — long-range reflections would vanish outright instead of thinning. A hit is rejected when the surface it landed on faces along the ray, because what is on screen there is that surface's far side. ## What the channel holds `ssr_out` is `@scene.reflection`: linear radiance in `.rgb`, and in `.a` the confidence — the edge fade, the range fade, how squarely the ray carried on into the scene, and `intensity`, multiplied — so `intensity` is how much of the trace reaches the picture and a miss is a zero the material reads as "the environment alone". The channel is cleared at the top of every scene render this feature does not run for, so such a frame reflects the environment alone too. The trace reads the light the previous frame ended with, so it runs on the lit channel alone (`debugChannel = 0`); a camera drawing a diagnostic pass is drawn lit first, unpresented, so the trace runs for where that camera stands, and the diagnostic reads the channel traced for it. The cutoff exists because a single ray describes a mirror: past it the lobe a rough surface gathers over is wider than one ray can stand in for, and the material keeps the prefiltered environment it already has.
▲ 0↑ born
·
shadermodule · born here
❒asset
# ssr_common The screen-space reflection passes' shared ground: the view-space reconstruction from depth, and the ray-to-screen projection the march steps along. Included with `#include "@builtin::systems.reflections.ssr_common"`.
▲ 0↑ born
·
computeshader · born here
❒asset
# ssr_trace Marches a reflection ray through the depth buffer and reports where it hit. ## Includes - `ssr_common` ## Bindings | name | kind | |------|------| | `scene_color` | `texture2d` | | `scene_depth` | `texture_depth` | | `scene_normal` | `texture2d` | | `scene_material` | `texture2d` (`sample: uint`) | | `frame_cam` | `buffer` | | `ssr_params` | `buffer` | | `ssr_out` | `storage2d (rgba16f)` |
▲ 0↑ born
·
assettype · born here
❒asset
# Package (asset type) A package is an asset — a `<Name>.package/` folder that bundles components, modules, scenes, presets, tools, and any other typed sub-assets under one identity, installed into a world as one unit. Packages are how authoring sets get distributed across worlds. A **library** is the addressable root scope content resolves under (`@builtin`, `@<name>`) — a whole world mounted into this one under a namespace, via `zeromind.installLib` / `world.installLibrary`. A package is an asset that lives inside such a root, and one library holds as many packages as its author put there. A package is installed by guid the way every asset is (`zeromind.installAsset`). ## When to use one - You're authoring a coherent feature suite (character controller stack, voxel engine, UI kit, animation library) and want to ship it as one install target. - You want a stable identity worlds can install and resolve sub-assets against (`@<lib>::pkg.sub_module`). - You want versioning + a per-package metadata layer that travels with the contents. If you're authoring inside a single world and don't need a separate install lifecycle, you don't need a package — drop your `.component` / `.module` / etc. directly in `/zero/source/`. Packages are for cross-world distribution. ## Where it lives - Source: `/zero/source/.../<Name>.package/` (or, for legacy packages, a folder containing `package.yaml` at its root). - Identity: `<Name>` (the `.package` suffix collapses in identity — mirrors `.module`). A file at `<Name>.package/<sub>/init.luau` resolves under the same identity it would at `<Name>/<sub>/init.luau`. - Folder shape (container-style — open child set): - `package.yaml`: metadata (name, version, description) and the packages it depends on (`dependencies`). **Required.** - `README.md` — instance documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** - Sub-asset folders: any combination of `.component/`, `.module/`, `.scene/`, `.preset/`, `.tool/`, `.toolbox/`, `.material/`, `.shader/`, etc. The validator permits unlisted children (`allow_unlisted: true`) by design — packages are open containers. ## How to create one ```luau asset.create("package", "<Name>") -- Creates: /zero/source/<Name>.package/ -- package.yaml (metadata scaffold) -- README.md (instance README template) -- `folder` places it in a subfolder of /source instead of the root: asset.create("package", "<Name>", { folder = "systems" }) ``` Then scaffold sub-assets inside it: ```luau local pkg = asset.resolve("<Name>.package") asset.create("component", "Spinner", { into = pkg }) asset.create("module", "util", { into = pkg }) ``` ## `package.yaml` ```yaml name: inkBlocks # optional; the folder stem when absent version: "1.0.3" description: "Ink and paper style blocks" dependencies: - { name: starterBlocks, version: "1.0.8" } - styleKit # a bare name states no version ``` `dependencies` lists the packages this package needs. Each entry's `name` is a reference the package records, the same kind of reference a `require` or an asset name in a data file makes: - **It resolves in the author's world.** Install the dependency here first (`zeromind.installAsset`); the entry then names the copy this world holds. A push refuses a package whose dependency resolves to nothing, naming it. - **An install brings it along.** Installing the package installs every dependency it lists, at the version this world holds when the package is pushed, under `/source/deps/<owner world>/`, deduplicated against what the installing world already holds. `zeromind.preview` lists each one before anything is written, and the install's `deps` lists what it brought. Uninstalling the package leaves a dependency that another installed package still uses. - **`version` is checked, not chosen.** A stated `version` has to equal the `version` the dependency's own `package.yaml` states in this world; `asset.validate` (and so the push) refuses the package naming both when they differ. To depend on another version, install that version here. ## How it operates 1. **Registration.** Writing `package.yaml` indexes the package with the package registry. 2. **Install.** A package is read before it is installed: `zm ls-tree @<guid>` in the shell lists what it contains, and `zm show @<guid>:<path>` and `zm grep <pattern> @<guid>` read it, so candidates are compared by what they do. The one that fits is installed by guid, the way every asset is: `zeromind_install` over the engine's MCP, or `tools.use("zeromind", "installAsset", <guid>)` inside the engine. The install writes the package's files (and the deps its `.refs` record, the packages its `package.yaml` lists under `dependencies` included) into this world's source, so its sub-assets resolve here. `tools.use("zeromind", "preview", <guid>)` reports what an install would write before it writes anything. 3. **Sub-asset resolution.** Because `.package` collapses in identity, sub-asset paths work whether the package folder is suffixed (`<Name>.package/<sub>`) or not (`<Name>/<sub>`). 4. **Updates.** `tools.use("zeromind", "updates")` lists installed content with upstream changes available; re-running the install pulls them. 5. **Metadata.** `package.yaml::name` overrides the folder stem if present (rare); `version` + `description` are surfaced in the package index. 6. **Hot reload.** Edits to sub-assets hot-reload as normal; edits to `package.yaml` reload package metadata. ## Discovery - `asset.list("package")` — every registered package. - `asset.inspect("<name>")` — sub-asset catalog, source, this type README. - `ls /zero/source/<Name>.package` lists the files the folder holds. - `library.list()` — the library assets registered in this world, including the ones packages under an installed library contribute. ## Authoring conventions - Name packages by their feature (`voxels`, `cameraRigs`, `dialogSystem`), not by their owner — the host library is the `@<lib>::` scope prefix. - Cluster cohesive sub-assets together. A "character controller" package should ship the controller component, its preset configs, its supporting modules, and a demo scene — not just the component. - Document the package's surface in `README.md` — what install installs, what each sub-asset does, how they relate. Agents reading `cat /zero/source/<Name>.package/README.md` get the full picture in one shot. - Version conservatively — breaking a package means breaking every world that installed it. Use semantic version bumps. ## Common pitfalls - **Missing `package.yaml`.** Without it the folder isn't recognized as a package even if it has a `.package/` suffix. - **A dependency named only by a string the code builds.** A package whose files reach another package only through a name assembled at runtime records no reference to it, so an install arrives without it. List that package under `dependencies` in `package.yaml`. - **Identity collapse.** Sub-asset paths skip the `.package` suffix. Don't include `.package` in `require` strings. ## Referencing inside a package: the `~` rule `~` means **this package**, resolved structurally from the referring file's own location. It is the only package-specific syntax. It is rename-safe (no package name is ever written) and library-agnostic (it never names the host library, so it survives `@builtin` being renamed or the package being mounted elsewhere). `~.tail` maps to `<pkg>.package/tail` from anywhere inside the package. | Referencing from | Use | Example | |---|---|---| | An asset/module **inside this package** | `~.tail` | `require("~.modules.myThing")`, `shader: "~.shaders.myThingBlend"` | | A **different package** under the same root | root-relative identity | `require("@root::systems.otherPkg.modules.x")` | | A **different library's** public API (a deliberate external dependency) | `@otherlib::…` | `require("@physics::modules.rigidbody")` | **Never write `@builtin::` (or any host-library name) to reference your own package** — that couples the package to the library it currently lives in and breaks if the library is renamed or the files move. Use `~.`. It works from Luau `require()` and from asset-reference strings in data files (a material's `mat.yaml` `shader:` field, a component's default asset ref); the engine resolves it against the referring file's own package. `asset.resolve` takes a `~.` name the same way, against the package of the file that calls it: `asset.resolve("~.materials.myThingStandard", "material")`. `~.` is the package-relative self-reference; `@root::` is the root-relative one (the library root, or the world source tree for non-library content). The `core/requiring-modules` guide covers the root model in full. A package that ships substantial functionality should include a `.guide` asset (a `<name>.guide/guide.md` folder anywhere under the package); the `guides` tool discovers it live, so collaborators find the system the way they find built-in guides. A package's test suite reaches the shared `Test` framework through its enclosing asset: `local Test = asset.containing(__FILE__).modules.shared` (`asset.containing` returns the deepest enclosing typed asset, the `.testSuite`, not the outer `.package`). ## Related types - Any of `.component`, `.module`, `.scene`, `.preset`, `.tool`, `.toolbox`, `.material`, `.shader`, `.style`, `.service`, `.importer` — packages bundle these. ## In the Inspector Package: the name, version and description package.yaml declares, how many things the package holds and what they are, and **Open** for package.yaml. A package.yaml that does not decode is listed under Problems with the decoder's error.
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
·
assettype · born here
❒asset
# Bundle (asset type) A bundle is a composable, spawnable entity assembly— a saved entity hierarchy plus its components, sub-assets (meshes, textures, animations, materials, sub-bundles), and any nested data, packaged into a single addressable identity. Bundles are the engine's "prefab" primitive. There are **two ways** to bring a bundle into a scene — and the difference matters enormously for scene saving and live editing. See "How to spawn" below before you write anything. If you want one snapshot of a whole world / level, use a `.scene`. ## Where it lives - Source: `/zero/source/.../<Name>.bundle/` - Identity: `<Name>` (the `.bundle` suffix strips for use in identities, but the on-disk folder keeps the suffix). - Folder shape (container type — open child set): - `entity_template` — JSON of the entity hierarchy + components. **Required.** - `.metadata`: optional. - Anything else: meshes / textures / materials / animations / sub-bundles. Validator allows arbitrary additional files (`allow_unlisted: true`). ## How to create one Two paths, depending on whether you have an in-scene hierarchy or are authoring from scratch: ```luau -- From an existing in-scene entity: composes the given entity tree into a bundle. local bundle = asset.create("bundle", "<Name>", { entity = entityRef }) -- Or scaffold an empty bundle, then update it from an entity: local bundleRef = asset.create("bundle", "<Name>") bundleRef:update(entityId) ``` ## How to spawn ### 1. `Asset` component — live-linked (the canonical way) **This is the primary way to put a bundle into a scene that survives saving as an asset reference.** Spawn an empty entity, attach the `Asset` component, point it at the bundle: ```luau local car = entity.spawn("car_1") car.component.add("Asset", { source = "<bundleIdentity>" }) ``` What you get: - The bundle's hierarchy spawns as **temporary** children under the owning entity. They're real, addressable runtime entities, but they are NOT serialized into `scene.json` on save, only the Asset component and its entity is saved. - **Live editing of the bundle is supported.** When you edit anything inside the `.bundle/` (entity_template, meshes, materials, sub-bundles, components), every entity in the world carrying that `Asset` component automatically tears down its spawned children and rebuilds them from the new template — same frame, no restart, no manual reload. The `onAssetReload` hook in `Asset.component/init.luau` drives this. - **Per-instance overrides survive across reloads.** Move a spawned child, rename it, add/remove components on it, edit a component's public state — the changes land in the Asset's `public.diff` (a sparse delta keyed by the child's `original_id`) at save time and are replayed by `bundleRef:instantiate()` on next load. The bundle stays a single source of truth; per-spot tweaks ride on top. - **Stable child ids across reloads.** `public.idMap` records the `original_id → runtimeId` assignment on first spawn; on every subsequent spawn the same mapping is reused so cross-entity references (e.g. `SkinnedModel.skeletonRoot = "ent_xxx"`) stay valid. ### 2. `bundleRef:instantiate` — baked, no live link Resolve the bundle and instantiate it onto an entity — this spawns the children **without** the live-link plumbing the `Asset` component provides: ```luau local car = entity.spawn("car_1") asset.resolve("<Name>", "bundle"):instantiate(car) ``` What you get — and the trade-offs: - The children spawn as **regular** (non-temporary) entities. On the next `layers.active:save()`, the entire spawned hierarchy is serialized into `scene.json` verbatim. - **No live link to the bundle.** Editing the `.bundle/` after the fact has no effect on already-spawned hierarchies. The bundle was effectively consumed at spawn time. - Scene-save / restore round-trip works the same as for any other hand-authored entity tree. Use this path when you want a one-shot expansion of the bundle into permanent scene state (e.g. you're populating a scene's static layout from a parametric bundle and don't want a live link). You can convert a live-linked instance into a baked one explicitly via `Asset:bakeIntoScene()` — this drops the `Asset` component and promotes the temporary children to permanent scene entities. ## How it operates 1. **Registration.** Writing into a `.bundle/` folder indexes the asset with the container-category restore plumbing. 2. **Inner refs.** Component fields that reference assets inside the bundle use the `__ref` shape (`{ "__ref": "<guid>", "name": "...", "type": "..." }`). The asset registry resolves them at spawn time. 3. **Identity.** The `.bundle` suffix is stripped, so a bundle resolves as `<Name>` (`asset.resolve("<Name>", "bundle")`); the type argument tells it apart from any `<Name>` of another type. 4. **Hot reload.** Saving `entity_template` (or any file in the bundle directory) fires the engine's per-asset content-changed hook, which calls `onAssetReload("source")` on every entity whose `Asset` component points at the bundle. Each carrier despawns its children and re-instantiates against the new template — typically next frame. ## Discovery - `asset.list("bundle")` — every registered bundle. - `asset.inspect("<name>")` — sub-asset catalog, source, this type README. - `ls /zero/source/<Name>.bundle` lists the files the folder holds. ## Parts and slots A bundle can be an object assembled from parts, and a part that fills one. A child carrying a `Slot` component is a named place another part fills (its `kind`, the `tags` a part must carry, a `default` part, and where the part attaches: that child's transform). A bundle whose root carries a `Part` component is a part, and its `fits` names the slot kinds it fits. An `Assembly` on the root holds what fills each slot by path (`wheel_fl`, `engine/turbo`, a part's own slots nesting under the slot it fills), and resolves, swaps and explains them at runtime: ```luau local car = entity.spawn("car") car.component.add("Asset", { source = "Buggy" }) -- a bundle with slots and an Assembly at its root local asm = car.component.get("Assembly") asm:configure({ wheel_fl = "SportWheel", engine = { part = "V8", turbo = "Turbo" } }) asm:fill("wheel_fl", "OffroadWheel") -- rebuilds that wheel alone print(asm:fits("wheel_fl", "V8")) -- false, and why ``` The configuration is saved with the scene and synced; the parts are runtime children rebuilt from it. `guides { path: "topics/assemblies" }` covers the whole family. ## Authoring conventions - Prefer `bundle:update()` or `asset.create("bundle", "<name>", { entity = ref })` over hand-authoring `entity_template`. The in-scene composer wires `__ref` shapes correctly and pulls in every transitively-needed asset. - Keep bundles focused — one prefab per bundle. If you have a "vehicle + driver + sound" assembly, split into three bundles and reference them from a scene rather than packing all three into one bundle. - Name child entities meaningfully — bundle interiors are inspectable, and stable IDs let other tooling (`entity_template` edits, animation hooks) find them. - Sub-bundles compose: a bundle's `entity_template` can `__ref` a child `.bundle/`. The asset registry resolves them transitively. ## Common pitfalls - **Confusing the two spawn paths.** `Asset` component = live link, reference-only scene save. `bundleRef:instantiate()` directly = baked, full hierarchy in `scene.json`. - **Copying JSON between bundles.** A copied `entity_template` does not bring the assets it references along with it; re-compose from a live scene rather than copy-pasting JSON. - **Live editing only fires for live-linked carriers.** Already-baked entities don't update when you edit the source bundle. ## Related types - `Asset.component` — the canonical mount point for a live-linked bundle. See its README for the `idMap` / `diff` machinery in detail. - `.scene` — owns a whole world's worth of entities; spawns bundles into itself via `Asset` components on per-spot entities. ## In the Inspector Selecting a bundle opens its **Contents** section, from the bundle's inspect detail: the template's node count, its root, the component types attached across it, a table of each renderable node's mesh and material, and a link to each of those meshes and materials.
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
·
animation · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
▦
texture · born here
❒asset
# Texture Managed texture asset. `data.ztex` is the encoded payload the renderer uploads; compression settings live in `settings.yaml` (`texRef:setSettings({ ... })` to change one).
▲ 0↑ born
backing path · assetTypes/shader.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 on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬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.