Log inGet started
❒
bundle · drop-in viewer
asset⌬ bundlebundleprimary: entity_template·originates fromworld 07158574-5…

ChronographWatch

No description provided.

by◐lumi·posted 2mo ago
What it does

No description provided yet.

Interface

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

This asset doesn't declare a schema or structured payload.
⌬ Metadata
components["Model"]humanoidfalseimport{"at":951.2999277,"importer":"5210d113-558a-4637-ac09-94e9c57b41cd","iteration":1,"source":{"guid":"3f343606-7c10-4ff5-8e6d-789ae4cff1b6","path":"ChronographWatch.glb"}}

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
·
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
·
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.** - `preview.png` — a rendered still of the assembled hierarchy, the bundle's persisted visual description. **Required.** Rendered when the bundle is staged (`zm add`) with its preview missing or stale, and by `generatePreviews`; it syncs, publishes, feeds image-based search, and gives browsers a thumbnail. - 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>",<entityProxy/id>) -- 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 preservation.** The `.bundle` suffix stays in the identity for container types — `<Name>.bundle` resolves under that exact string, distinguishing it from any `<Name>.scene` at the same stem. 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. ## Authoring conventions - Prefer `bundle:update()` or `asset.create("bundle","<name>",entity)` 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`. - **Inner-ref guids drift.** If you copy an `entity_template` from one bundle into another, the `__ref` guids may not resolve — re-compose from a live scene rather than copy-pasting JSON. - **The `.bundle` suffix is part of the identity.** This is the container-category convention. Don't strip it in reference strings. - **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
·
assettype · born here
❒asset
# `animation` asset type A single animation clip, as a composite asset. The folder `<name>.animation/` carries: - **`data.zanim`** — the engine-native binary clip payload (bincode `AnimAsset`): bone-named tracks, duration, channels. This is the asset's *primary*; consumers reference the `.animation` asset's guid, never this inner file. Parallel to how `.mesh` carries `data.zmsh`. - **`source_rig.json`** (optional) — a reference to the `.rig` the clip was authored on. A clip whose source rig qualifies as humanoid retargets onto any humanoid rig, through the roles each rig's `qualify()` reads from its structure. Clips are produced by the model importers (`model`, `model_animation`), which decode embedded animation tracks and call `asset.create("animation", name, { bytes })`. Bare `.anim` files are the legacy shape; the `.animation` composite supersedes them (existing `.anim` guids are inherited by their migrated `.animation` asset so references keep resolving). ## In the Inspector Selecting an animation opens its **Clip** section, decoded from `data.zanim` once per payload version. It reports the clip's name, duration in seconds, channel and bone counts, and, when the clip is tagged, whether it is humanoid. It links the rig named in `source_rig.json` and lists the clip's bones. An animation with no `data.zanim` says so.
▲ 0↑ born
◇
component · born here
❒asset
# Model Loads and displays a 3D mesh on an entity. Disabling the component hides the mesh. Supports procedural meshes (`cube`, `sphere`, ...), model files, and remote URLs. Tint and outline are shader effects applied to the mesh. **Material lives on this component.** There is no standalone `Material` component — a material only renders where there is a mesh to render it on, so the `material` field is part of Model (and SkinnedModel). Set it at add time or assign the field later; properties are registry-wide. Public fields: `model`, `material` (`AssetRef<material>`), `tintR/G/B`, `tintBlend`, `outlineR/G/B`, `outlineIntensity`. Methods: `:setTint(color, blend?)` (color: `{r, g, b}` array or `{r=, g=, b=}` map), `:clearTint()`, `:setOutline(color, intensity?)`, `:clearOutline()`, `:setMaterialProperty(prop, value)`, `:getMaterialProperty(prop)`, `:getMaterialPropertyNames()`. ```luau entity(id).component.add("Model", { model = "cube", material = "gold" }) entity(id).component.get("Model").material = "checkerboard" -- swap material entity(id).component.get("Model"):setTint({1, 0, 0}, 0.5) entity(id).component.get("Model"):setMaterialProperty("roughness", 0.2) ```
▲ 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.** - `preview.png` — the material's rendered shader-ball still, its persisted visual description. **Required.** Rendered when the material is staged (`zm add`) with its preview missing or stale, and by `generatePreviews`; it syncs, publishes, feeds image-based search, and gives browsers a thumbnail. - `README.md` / `.metadata` — optional prose + tags, indexed when an author writes real ones. - `textures/` — optional folder for embedded textures. Validator allows the folder; its interior is unrestricted. - `shaders/` — optional folder for a co-located custom shader. Same rule. ## How to create one ```luau asset.create("material", "<Name>") -- Creates: /zero/source/<Name>.material/ -- mat.yaml (shader binding + property scaffold) -- preview.png (rendered when the material is staged with `zm add`; the -- picture lands as a change the next `zm add` stages) -- `folder` places it in a subfolder of /source instead of the root: asset.create("material", "<Name>", { folder = "materials" }) ``` Pass the shader and initial values through `asset.create`, then tune with `setProperty` and keep what you tuned with `saveDefinition`: ```luau local mat = asset.create("material", "<Name>", { shader = "@builtin::shaders.pbr" }) mat:setProperty("base_color", { 0.8, 0.6, 0.1, 1 }) mat:setProperty("roughness", 0.3) mat:saveDefinition() -- writes the tuned values into mat.yaml ``` This mints the `.material` asset and returns a handle you tune with `setProperty`. A set is a runtime change: it reaches the GPU material and every entity wearing it, and `saveDefinition` is the call that writes it into `mat.yaml`, so a property driven every frame costs no file write and leaves the authored file as its author wrote it. A purely runtime `asset.create` lives only for the session. For an ephemeral, runtime-only GPU material that is not an asset at all, use `renderer.material.create` instead (see `core/resource-model`). ## How it operates 1. **Property reflection.** The bound shader's uniform struct fields become the material's configurable properties — no manual property declarations needed. `naga` introspects the shader at load. 2. **Property values.** `mat.yaml` groups values by type, each block keyed by the shader's field names: `properties:` holds scalars and colors/vectors, `ints:` holds integer-typed fields, `bools:` holds boolean-typed fields, and `textures:` holds texture slots. Omitted fields take the shader's default. A scalar is a bare number (`roughness: 0.5`); a color or vector is an array (`base_color: [1, 1, 1, 1]`). `shader:` is the shader's identity string. This is the shape every material the engine writes uses. A `saveDefinition` writes the changed values into the file where they already stand and leaves the rest of it — its comments, the order of its keys and blocks, the spelling of its literals — untouched. The older `floats:` / `colors:` blocks (with `{ r, g, b, a }` map colors) still load for materials hand-authored that way. 3. **Render state.** `render:` holds how the mesh is drawn rather than what the shader reads: `blend` (the equation the fragment composites with, also spelled `type`), `cull`, `depth_write`, `depth_compare` and `topology`. The `.shader` type README's "The render block" section lists every value each key takes, and `asset.create`'s `render` argument takes the same keys and values. 4. **Application.** An entity wears a material by referencing it from its `Model` component (`material = "@.../<Name>"`). Materials affect every entity that references them; runtime `asset.resolve(ref):setProperty(...)` changes propagate to all of them immediately, and last until the next mode change unless `saveDefinition` writes them into `mat.yaml`. 5. **Texture binding.** Texture slots accept `color:r,g,b,a` for solid colors, `default:white` / `default:black` / `default:normal` for the engine's own fallback texels, `@builtin::textures.<name>` for builtins, `path/to/image.png` for file textures, or a render-target handle for camera renders. The GPU texture cache reads the `color:` and `default:` forms from the string itself, so a slot written in one names no asset and carries nothing with the material when it travels; every other form names a texture asset the slot depends on. 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. 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.** Update every `Model` component and `asset.create` callsite that references it. ## Related types - `.shader` — defines what properties exist; required before a material can bind to it. - `.preset` — captures a component configuration; conceptually similar to a material, but for components instead of shaders.
▲ 0↑ born
·
assettype · born here
❒asset
# mesh A mesh is renderable geometry — vertices and triangle indices, and, for a skinned mesh, the per-vertex skinning data that binds it to a skeleton. A component that draws geometry references a mesh, and the renderer binds the mesh's GPU resource for that entity. > New to how a mesh exists as an asset, as CPU-side vertex data, and as a live > GPU resource (and how `renderer.mesh.*` moves between them)? Read > `core/resource-model` first. Geometry is stored in `data.zmsh`; the GPU resource is keyed by the asset's guid. ## Create `asset.create("mesh", name, opts)` writes the geometry and registers the mesh. `opts` is raw geometry — flat arrays; `positions` and `indices` are required, `normals` and `uvs` optional: ```luau asset.create("mesh", "tri", { positions = { 0,0,0, 1,0,0, 0,1,0 }, -- [x,y,z, ...] (required) indices = { 0, 1, 2 }, -- [i0,i1,i2, ...] (required, 0-based) normals = { 0,0,1, 0,0,1, 0,0,1 }, -- [x,y,z, ...] (optional) uvs = { 0,0, 1,0, 0,1 }, -- [u,v, ...] (optional) }) ``` The full authorable geometry, all flat arrays unless noted: | Field | Shape | Notes | |---|---|---| | `positions` | `[x,y,z, ...]` | required | | `indices` | `[i0,i1,i2, ...]` | required; 0-based, counter-clockwise for the front face (see Conventions) | | `normals` | `[x,y,z, ...]` | optional; the surface's outward direction | | `uvs` | `[u,v, ...]` | texture UVs, optional; `(0,0)` is the image's top-left | | `colors` | `[r,g,b,a, ...]` | optional | | `tangents` | `[x,y,z,w, ...]` | optional (`w` = handedness) | | `uvs1` | `[u,v, ...]` | the lightmap UV set, optional | | `unwrapUvs` | boolean | generate `uvs1` when it is absent | | `skinning` | `{ joints, weights }` | per-vertex skin binding (four each per vertex) | | `skins` | array of skin tables | the skeleton (see `renderer.mesh.decode` for the shape) | | `morphTargets` | array of `{ name?, positions, normals? }` | the shapes the mesh blends towards, each a per-vertex displacement | `encode` / `decode` round-trip every one of these, so a **skinned** mesh can be built in engine, not only imported. Pass `unwrapUvs = true` to generate a lightmap UV set at creation time. ## Conventions Geometry is read against these whether it arrived through `asset.create`, `renderer.mesh.create`, or an importer. | Convention | How the engine reads the geometry | |---|---| | Index base | `indices` count vertices from **0** — index `0` is the first `positions` triple. The Luau array carrying them is 1-based like any other, so `indices[1]` is the first triangle's first index, and vertex `v` has its x at `positions[v * 3 + 1]`. | | Triangle winding | A triangle's **front** face is the one whose three vertices turn **counter-clockwise** as the viewer sees them. Put another way, `cross(v1 - v0, v2 - v0)` points out of the front face. | | Back faces | A material culls its back faces by default, so a triangle turned away from the camera draws nothing where it stands and whatever is behind it shows through. | | Normals | `normals` give the surface its outward direction and shade the face. Which side of a triangle draws comes from the index order alone, so a normal pointing away from the side the winding draws leaves that face standing and shades it as if the light were behind it. | | UVs | `(0, 0)` samples the first texel of an image — its top-left corner — and `v` grows downward through it. | | Axes | Model space carries the world's basis: `+X` right, `+Y` up, and `-Z` the direction an entity's `transform.forward` points. | Winding is what the author states: the inward faces of a room and a sky shell seen from inside are geometry meant to be wound the other way, and `asset.create` writes the index order it is handed. Two levers turn a face around: ```luau indices = { 0, 2, 1 } -- the same three vertices, reversed asset.create("material", "two_sided", { -- or keep the order and draw both sides shader = "@builtin::shaders.pbr", render = { cull = "none" }, }) ``` `cull = "front"` draws the far side by itself, which is what an inverted-hull outline is built on, and `"back"` is the default. `.shader` documents the whole `render` block a material's `mat.yaml` carries. A mesh states which way its own triangles face, so the question is answerable off the geometry — every triangle whose cross product agrees with the normal it carries is wound to draw the side that normal points at: ```luau local cpu = meshRef:load() local g = cpu:geometry() local function facesOutward(t: number): boolean -- t counts triangles from 0 local i = { g.indices[t * 3 + 1], g.indices[t * 3 + 2], g.indices[t * 3 + 3] } local function p(k: number, c: number): number return g.positions[i[k] * 3 + c] end local ax, ay, az = p(2,1) - p(1,1), p(2,2) - p(1,2), p(2,3) - p(1,3) local bx, by, bz = p(3,1) - p(1,1), p(3,2) - p(1,2), p(3,3) - p(1,3) local cx, cy, cz = ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx local n = i[1] * 3 return cx * g.normals[n+1] + cy * g.normals[n+2] + cz * g.normals[n+3] > 0 end local inward = 0 for t = 0, #g.indices // 3 - 1 do if not facesOutward(t) then inward += 1 end end cpu:unload() ``` ## Built-in primitive meshes The engine ships blockout geometry every world references by bare name (`{ model = "cube" }`). Each one is centred on its local origin, so an entity's `scale` reads directly as the size the mesh covers in the world: | Mesh | Extent (x, y, z) | Local AABB | |---|---|---| | `cube` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` | | `sphere` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` | | `cylinder` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` | | `cone` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` | | `octahedron` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` | | `rounded_box` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` | | `capsule` | 0.5 x 1 x 0.5 | `-0.25, -0.5, -0.25` .. `0.25, 0.5, 0.25` | | `torus` | 1 x 0.3 x 1 | `-0.5, -0.15, -0.5` .. `0.5, 0.15, 0.5` | | `plane` | 1 x 0 x 1 | `-0.5, 0, -0.5` .. `0.5, 0, 0.5` | | `ground_quad` | 1 x 0.1 x 1 | `-0.5, -0.05, -0.5` .. `0.5, 0.05, 0.5` | | `ground_hex` | 1.732 x 0.1 x 2 | `-0.866, -0.05, -1` .. `0.866, 0.05, 1` | | `shader_ball` | 0.933 x 1 x 0.933 | `-0.467, -0.5, -0.467` .. `0.467, 0.5, 0.467` | `plane` is a flat quad in the XZ plane, so it has no vertical extent at all and scaling it on Y moves nothing. `meshRef:load():getBounds()` reads the same numbers back off any mesh, built-in or authored. ## Put a mesh on an entity A component draws a mesh through a `resource("mesh")` field, which accepts the mesh **by name, by ref, or as a `MeshHandle`** — the component resolves whatever you give it. The builtin `Model` and `SkinnedModel` both expose a `model` field: ```luau entity.find("box").component.add("Model", { model = "tri" }) -- by name entity.find("box").component.add("Model", { model = asset.ref("tri") }) -- by ref entity.find("box").component.add("Model", { model = meshHandle }) -- a MeshHandle (below) ``` ## Runtime mesh — no asset Build geometry straight onto the GPU and render it without creating an asset. The `resource("mesh")` field takes the returned `MeshHandle` directly: ```luau local h = renderer.mesh.create({ positions = {0,0,0, 1,0,0, 0,1,0}, indices = {0,1,2} }) entity.find("box").component.add("Model", { model = h }) ``` Its geometry reads back off the handle. `renderer.mesh.geometry(mesh)` returns the same flat-array `MeshGeometry` `create` takes, carrying every stream the mesh has — so a runtime mesh can be inspected, edited, and rebuilt without ever becoming an asset: ```luau local geom = renderer.mesh.geometry(h) -- { positions, indices, tangents?, ... } geom.positions[1] = 0.5 local h2 = renderer.mesh.create(geom) ``` An optional stream is present only when the mesh carries one, so `geom.tangents` answers whether the mesh has a tangent basis and `geom.uvs1` whether it has a second UV set. ## Read geometry — `meshRef:load()` `:load()` decodes `data.zmsh` into the CPU store and returns a CPU handle carrying `vertexCount` / `indexCount`: | Method | Returns | |---|---| | `cpu:getVertices()` | array of `{ pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }`, one per vertex | | `cpu:getTriangles()` | array of `{ v1, v2, v3 }`, each a full vertex (the shape above) | | `cpu:getBounds()` | `{ min = {x,y,z}, max = {x,y,z} }` — the local-space AABB | | `cpu:geometry()` | the complete `MeshGeometry` — flat arrays, every stream the mesh carries | | `cpu:encode()` | the `data.zmsh` bytes | | `cpu:unload()` | drop this guid's CPU copy | ```luau local cpu = meshRef:load() local v = cpu:getVertices()[1] -- { pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} } local aabb = cpu:getBounds() cpu:unload() ``` By default the CPU copy is transient (decode → read → `unload`). Set `keepCpu` to keep it resident — see Settings. ## Write geometry back — `meshRef.primary` The inverse of reading: encode a `MeshGeometry` and write it to the asset's primary content file. The assetType picks the write up and re-uploads the live GPU mesh, so the change shows without a reload: ```luau local geom = renderer.mesh.geometry(meshRef) geom.tangents = computeTangents(geom) -- add a stream it did not carry vfs.write(meshRef.primary, renderer.mesh.encode(geom), { overwrite = true }) ``` `meshRef.primary` is the asset's `data.zmsh` path. This is the route for a stream with no setting of its own; `keepCpu` + `meshRef:setVertices` moves positions in place, and `lightmapUvs` regenerates the second UV set (both below). ## GPU handle — `meshRef:handle()` `:handle()` uploads the geometry (Disk → CPU → GPU) and returns a `MeshHandle`, cached on the asset so every consumer shares ONE GPU entry. A component does this for you when you pass it the mesh; call it directly only for the runtime/low-level path. ## Settings — `meshRef:settings()` / `meshRef:setSettings(patch)` A mesh's settings are declared in `settings.luau` and live in `settings.yaml` (or, for content published before it existed, a legacy `.metadata` block). `:settings()` returns them with every declared default filled in. `:setSettings(patch)` writes any subset — sibling settings are preserved, and a patch that fails the schema is refused, naming the field and why. | Setting | Type | Default | Meaning | |---|---|---|---| | `keepCpu` | boolean | `false` | Keep the CPU geometry copy resident — for an in-use mesh — so geometry can be read repeatedly without re-loading. | | `lightmapUvs` | string | `"keep"` | The mesh's second (lightmap) UV set. `"keep"` leaves the stored geometry's set as-is; `"generate"` unwraps a fresh non-overlapping set; `"none"` strips it. See below. | | `clusterLod` | boolean | `true` | How deep the cluster set is built. Every static mesh draws through clusters, from the frame it materialises (its `lod0` set) until its bake lands. `true` (default) builds the LOD hierarchy; `false` builds one level, drawn at full detail. | ```luau local s = meshRef:settings() -- { keepCpu = false } meshRef:setSettings({ keepCpu = true }) ``` `keepCpu` is an active lifecycle control, and only ever holds geometry for a mesh that is **in use** (GPU-resident): - Setting it **on** while the mesh is in use loads the CPU copy immediately. - Setting it **on** for a mesh that is **not** in use loads nothing — the copy is retained the next time the mesh is materialised, so an unused mesh is never pinned in memory. - Setting it **off** drops a resident CPU copy (the renderer keeps drawing from the GPU). `clusterLod` is the same kind of lifecycle control, for the depth of the cluster set rather than the CPU copy: - Setting it **off** on a mesh in use rebakes it as one level of clusters (`single`), drawn at full detail. - Setting it **on** on a mesh in use rebakes the LOD hierarchy (`dag`). - A mesh that is not in use bakes at the depth the setting asks for when it next materialises. Acting on either setting for a mesh in use can read the mesh's stored geometry, and where the geometry is fetched on first read that read spans frames. `meshRef:settingsPending()` answers whether one is still in flight; once it answers `false`, every settings change made so far has reached the mesh. ### Second UV set (`lightmapUvs`) A mesh can carry a **second**, independent UV set (`uv1`) alongside its texture UVs (`uv`) — a distinct per-vertex channel, non-overlapping where the texture UVs tile and reuse charts. `lightmapUvs` controls whether the stored geometry keeps one, generates one, or drops it: | Value | Effect | |---|---| | `"keep"` | Leave the stored geometry's `uv1` as-is (the default). | | `"generate"` | Unwrap a fresh non-overlapping `uv1` (near-planar charts packed into `[0,1]` with gutters). | | `"none"` | Strip the `uv1` set. | ```luau meshRef:setSettings({ lightmapUvs = "generate" }) ``` Changing the setting reprocesses the asset's geometry in **edit** mode; a change in play mode never rewrites the source. The reprocess round-trips the geometry through the codec, so positions, normals, tangents, skinning, and the skeleton are preserved — only the `uv1` set changes. A runtime mesh carries `uv1` the same way: pass `uvs1` (or `unwrapUvs = true`) to `renderer.mesh.create`. ## Read vertices — `meshRef:getVertices()` With `keepCpu` on for an in-use mesh, read its vertices straight from the resident CPU copy: ```luau meshRef:setSettings({ keepCpu = true }) -- on an in-use mesh local verts = meshRef:getVertices() -- { { pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }, ... } ``` If the CPU copy isn't resident — `keepCpu` is off, or the mesh isn't in use — it errors loudly, naming the reason and the remedy, rather than silently loading a throwaway copy: ``` mesh:getVertices: cannot read vertices — keepCpu is off, so no CPU copy is kept. Turn on CPU retention for an in-use mesh first: meshRef:setSettings({ keepCpu = true }). ``` ## Morph targets — `meshRef:morphTargets()` A mesh can carry morph targets: shapes it blends towards, each a per-vertex displacement from the base geometry. Every target carries the name it was authored under, and an imported model keeps the one its source file gave its blend shapes, so content addresses a shape as the thing it means rather than as the ordinal it happened to import at. ```luau for i, name in meshRef:morphTargets() do print(i, name) end -- 1 Eyes_Blink ``` An entity blends them with `ecs.MorphWeights`, weight `i` driving target `i`. `renderer.mesh.morphWeights(mesh, { Eyes_Blink = 1 })` builds that ordered array from weights named by shape — shapes left out sit at 0, and an unknown name errors listing the ones the mesh carries. ## Discovery | Call | Shows | |---|---| | `asset.list("mesh")` | every registered mesh | | `asset.inspect("<name>")` | a text report about the mesh (metadata, source, status) | | `meshRef:settings()` | the resolved settings | | `meshRef:settingsPending()` | whether a settings change is still reading the stored geometry | | `meshRef:morphTargets()` | the shapes it blends towards, named, in target order | For the on-disk shape of a mesh asset, run `asset.inspect("<name>")` or read the type's `type.yaml` — those are the live source of truth. ## Related - `renderer.mesh.*` — the GPU/CPU resource layer (`create` a runtime GPU mesh, `encode`/`decode` the `ZMSH` payload, `readback` GPU → CPU). `:load`/`:handle` wrap it; see `man renderer`. - `.material` + `.shader` — what shades the geometry. - `.bundle` — container assets that carry `.mesh` interiors. ## In the Inspector Selecting a mesh opens its **Geometry** section, after the asset's identity and before its Settings. It reports what `data.zmsh` holds, decoded once per payload version: vertices, triangles, the bounds and their size, the vertex streams beyond positions (normals, uvs, uvs1, colors, tangents, skinning), and, when the mesh has them, its skin joints and morph targets. While the renderer holds the mesh's cluster hierarchy, the section also reports its cluster and level counts. A payload is one index stream, so the section names no submeshes. A mesh folder with no `data.zmsh` says so.
▲ 0↑ born
·
assettype · born here
❒asset
# texture An **image asset** — the raw bytes of a `png` / `jpg` / `jpeg` / `hdr` / `ktx2` / `webp` / `bmp` / `tga` file. Loose image files under a `textures/` directory classify as `texture`. > New to how a texture exists as an asset, as CPU-side pixels, and as a live GPU > texture (and how `renderer.texture.*` and render targets move between them)? > Read `core/resource-model` first. `texture` is a first-class assetType (not a bare plural-dir category) so that: - a texture has a stable asset **identity** and an explicit `asset_type` link, and - a material that references a texture records a real `.refs` edge to it — which is what lets the CPU-residency system cascade from a material to the textures it actually uses. ## CPU residency vs GPU residency A texture can be resident two fundamentally different ways, and they must not be conflated: - **CPU residency** (this assetType): the raw image **bytes in memory** — readable, hashable, decodable. You can operate on the bytes. - **GPU residency**: those bytes uploaded to the device as a `wgpu::Texture` the renderer binds/samples. Byte-level reads are **not possible** on the GPU variant — that is simply how the device works. GPU residency is managed separately (`TextureRefCounts` → `drop_texture`) and is **not** part of this assetType. Same texture identity, two separate residency states, two separate operation sets, two separate reference counts. See `docs/specs/runtime-asset-copying.md`. ## Internal layout A `.texture/` is permissive (`allow_unlisted: true`). `asset.create("texture", name, ...)` and the **texture importer** (which creates through it when a loose raster image lands in `/source/`) produce this shape: ``` <name>.texture/ source.<ext> the retained source raster — see below source.ztex the full-resolution texels a cook dropped some of — see below settings.yaml the texture's settings — see below data.ztex canonical pre-encoded + pre-mipped payload (ZTEX magic) .metadata what data.ztex holds and was encoded from ``` `asset.create` keeps a copy of the image bytes it was given as `source.<ext>`; the importer moves the uploaded image in under its own name instead (`foo.png` stays `foo.png`). Either way it is the texture's retained source: rewriting it re-encodes `data.ztex` from the new pixels, and a rewrite carrying the bytes `data.ztex` was already encoded from changes nothing. `data.ztex` is the **preferred primary**: when present the renderer resolves and uploads it verbatim — no re-mip. When absent the primary falls through to a raster image in the folder, so a container holding only an image file reads that image. (`data.tex` is the pre-rename legacy payload name — still resolved for existing assets; new content writes `data.ztex`.) That fallthrough takes the lowest-named raster image in the folder and never the generated `preview.png` thumbnail, which is rendered from the payload. A container that lost its encoded payload and holds no other image has no primary: `asset.diagnose(ref)` reports `noPrimaryFile`, `asset.validate(ref)` reports `texture.no_payload`, and `renderer.texture.loadCpu` refuses it. `asset.primaryFile(ref).path` names the file the resolution landed on; the asset-system guide covers `diagnose`. #### `settings.yaml` settings A texture's settings are declared in `settings.luau` and live in its `settings.yaml`. `asset.create` stamps them when the texture is made, and `texRef:setSettings({ ... })` (or `asset.setSettings(ref, patch)`) changes them. Editing an encode-relevant one re-runs the encode against the retained source and rewrites `data.ztex`. A texture whose settings were stored under an earlier schema reads with the same meaning, and its first local edit rewrites them into `settings.yaml` under the fields below. ```lua local tex = asset.create("texture", "hero_albedo", { bytes = png, ext = "png", settings = { maxDimension = 1024 }, }) tex:setSettings({ uastcRdo = "medium" }) ``` | key | values | default | meaning | |-----|--------|---------|---------| | `textureType` | `color` / `normalMap` / `mask` / `sprite` | `color`, or guessed from the name | what the texture holds, recorded in the payload. `normalMap`: tangent-space normals — when `generateMipmaps` builds the chain, every level of it, the base included, is renormalised to unit length; with `generateMipmaps = false` the one stored level is the source as given, and the chain the GPU builds from it at upload is a plain box filter. `mask`: linear data (roughness, metallic, occlusion, height, masks). `sprite`: a 2D image with transparency. A new texture's type is guessed from its name — `_normal` / `_nrm` / `_n` / … → `normalMap`, `_rough` / `_metal` / `_ao` / `_height` / `_mask` / … → `mask` — unless it is given. | | `colorSpace` | `srgb` / `linear` | follows `textureType`: `srgb` for `color` and `sprite`, `linear` for `normalMap` and `mask` | how the stored values are read when sampled. `srgb` for albedo/emissive/base-color, UI and sprites; `linear` for normal/RM/AO/height/mask data. | | `compression` | `uastc` / `rgba8` / `rgba16` / `rgba32f` | a new texture: `uastc`, a `sprite` `rgba8` | what the texels are stored as — see "What each compression stores" below. | | `effort` | `fastest` / `fast` / `thorough` | `fastest` | how long a `uastc` encode searches for each block — see "Block compression" below. | | `uastcRdo` | `none` / `low` / `medium` / `high` | `none` | how far a `uastc` encode moves from the source to make a payload that compresses smaller on disk and over the network — see "Block compression" below. | | `generateMipmaps` | `true` / `false` | `true` | bake the full mip chain into `data.ztex` at encode time. | | `maxDimension` | integer, `1` to `8192` | a new texture: `4096` | the longest side the encoded texture may have; a larger source is downsampled to fit, and a `uastc` texture's longest side is the largest power of two within it. A value past 8192 is refused. | | `filter` | `linear` / `nearest` | `linear` | sampler filtering recorded in the payload. A material slot and a UI image widget sample the texture with it. A post-process effect's texture property, a render pass's `@texture` input and a compute shader's `texture_2d` binding sample through a sampler of their own (the effect's `postprocess.setSampler`, the pass's, the compute shader's named `sampler` binding), which this setting does not change. | | `wrapU` | `repeat` / `clamp` / `mirror` | follows `textureType`: `clamp` for `sprite`, else `repeat` | how a material samples the texture past its left and right edges: tiled, the edge texel held, or mirrored. Recorded in the payload. | | `wrapV` | `repeat` / `clamp` / `mirror` | follows `textureType`: `clamp` for `sprite`, else `repeat` | the same, past its top and bottom edges. | | `anisotropy` | `world` / `1` / `2` / `4` / `8` / `16` | `world` | samples taken along a surface seen at a grazing angle. `world` follows the `render.anisotropy` world setting (`renderer.setAnisotropy`); a number fixes it for this texture, clamped to what the device supports. A `nearest` texture samples with 1. A write may give the number itself (`anisotropy = 8`); it is stored as the string. Recorded in the payload. | | `alpha` | `input` / `premultiplied` / `none` | `input` | what the encode does to alpha. `input`: kept as the source has it (straight alpha). `premultiplied`: colour multiplied by alpha — decoded to linear light, multiplied and re-encoded for an `srgb` texture, multiplied as stored for a `linear` one — the source a material with the `premultiplied` blend mode (`src + dst * (1 - src.a)`) composites. `none`: every texel opaque, colour kept. | | `alphaIsTransparency` | `true` / `false` | follows `textureType`: `true` for `sprite`, else `false` | fill every fully transparent texel with the average colour of its non-transparent neighbours, spreading outward, so bilinear filtering and mip averaging never pull black into the edge of a transparent area. Alpha stays 0. Runs before `alpha` and before the mips. | | `keepCpu` | `true` / `false` | `false` | keep the decoded pixels in the CPU store after the GPU upload — see below. | The type's colour space, `alphaIsTransparency`, `wrapU` and `wrapV` are stamped when the texture is created, and a later write that changes `textureType` moves them with it — unless the same write names them. A texture whose settings predate `textureType` reads as `color`, with `alpha = input` and no transparency fill, which is what its payload holds. A cook that drops texels — a `maxDimension` smaller than the source, or a `uastc` cook resampling a side to a power of two — keeps the full-resolution input as `source.ztex` (one RGBA8 level) when the container holds no source image, so raising `maxDimension` later re-encodes from every texel. A cook that drops nothing keeps nothing extra. A texture made from raw pixels whose cook dropped nothing keeps its payload as its only source, so a settings change re-encodes from that payload read back with straight alpha: what an earlier encode discarded stays discarded, after `alpha = none` its alpha is opaque, and a `uastc` payload re-encoded is the blocks' texels, not the pixels first given. A new texture's settings start from the texture type's own defaults — `uastc` with a `maxDimension` of 4096; `rgba8` for a `sprite`; a 16-bit PNG or `.hdr` source left to the schema's `rgba8` unless a layer names a compression — and a world's Preset Manager rows for `texture` override them (a world that ships at 2048, say), then an importer's settings, then the `settings` the create names. `filter`, `wrapU`, `wrapV` and `anisotropy` say how the texture is sampled, not what its texels are. A write that changes only those rewrites the payload's header (`renderer.texture.withSampler`): every level keeps its bytes, and no preview is rendered, because no pixel changed. Every material slot samples through its own texture's state, so two textures on one material can wrap and filter differently. UI image widgets honour `filter` and draw each image over its own bounds, so the wraps apply to materials. A post-process effect, a render pass `@texture` input and a compute shader sample through samplers of their own, so none of the four settings reaches them. A texture whose payload predates wrap and anisotropy reads `repeat` / `repeat` / `world`, which is how it has always been sampled. `keepCpu` is a lifecycle setting, not an encode input — it never re-runs the encode. It acts on an already-materialised texture right away instead: `true` loads the decoded pixels into the CPU store when the texture is GPU-resident, `false` drops a resident CPU copy. The `data.ztex` payload format is `zero_texture::blob` (`ZTEX` magic + 32-byte header + mip levels); the CPU encoder is exposed to Luau as `renderer.texture.*` and consumed by the texture importer and this assetType. #### What each compression stores | `compression` | In `data.ztex` | On the GPU | Limits | |---|---|---|---| | `uastc` | UASTC 4x4 blocks, whole mip chain, one byte per texel, each level compressed for storage. | Transcoded at upload to a block format the adapter samples: `Bc7RgbaUnorm[Srgb]` where it samples BC textures (desktop native, desktop WebGPU), ASTC 4x4 where it samples ASTC and not BC (phones, a browser on one), ETC2 RGBA8 where it samples only ETC2, each one byte per texel; RGBA8 at four bytes per texel on an adapter that samples none of them, and for a texture with a side under four texels. | each side is resampled to the nearest power of two (halfway rounding up), nearest-neighbour for a `nearest` texture. | | `rgba8` | From an image with `generateMipmaps = false`, a `maxDimension` the image fits within, `alpha = input` and `alphaIsTransparency = false`: the source PNG / JPEG / WebP itself, decoded at load. Otherwise raw 8-bit RGBA with the encoder's box-filtered mip chain. | `Rgba8Unorm` / `Rgba8UnormSrgb`, four bytes per texel. | none | | `rgba16` | Raw 16-bit unsigned normalized, one level. An image is decoded at its own depth: a 16-bit PNG keeps every bit, an 8-bit channel `v` is stored as `v * 257`. The frames of an animated image are decoded at 8 bits. | `Rgba16Unorm` where the adapter grants 16-bit normalized textures (native); half float (`Rgba16Float`, about 11 significant bits) where it does not (every browser). | one level, no `maxDimension`, `linear` only. | | `rgba32f` | Raw 32-bit float, one level. An image channel is stored as its value over the channel's maximum. | `Rgba32Float`. Where the GPU filters 32-bit float, every consumer samples it. Where it does not (every browser): a material slot draws the shader's declared default; a UI image draws nothing; a render pass naming it as an `@texture` input is skipped; a compute shader that declares no sampler binds it and reads it with `textureLoad`, and one that declares a sampler refuses the dispatch — each logging once, naming the texture and its format. CPU reads (`:load()`, `renderer.texture.decode`) return every bit on every device. | one level, no `maxDimension`, `linear` only. | A high-precision compression turns `generateMipmaps` off, `maxDimension` to 8192 and the colour space to `linear` when the same write does not name them, and refuses a write that names a contradicting value. A payload of BC7 blocks, written before `uastc` existed, stays as it is: `Bc7RgbaUnorm[Srgb]` where the adapter samples BC textures, re-encoded as it loads into ASTC 4x4 or ETC2 where the adapter samples those instead, and decoded to RGBA8 where it samples none. Its settings read `compression = uastc`, what an encode of it now asks for, while `texRef:info()` reports the `bc7` payload it holds. #### Block compression `compression = "uastc"` stores every mip level as UASTC blocks — sixteen bytes per 4x4 texels — a format no GPU samples directly but every one can be handed: at upload each level is transcoded to a block format the adapter samples, BC7 on a desktop GPU and ASTC 4x4 on a phone's (ETC2 on one that samples ETC2 alone), so one payload is resident at one byte per texel on each, and RGBA8 on an adapter that samples none; `renderer.textureMemory().blockFormat` names the format this adapter holds it in. Only the levels a device keeps are transcoded: under a texture-size cap (`renderer.setMaxTextureSize`) a texture is uploaded from the first level of its chain within the cap. Compression is lossy, which makes it the choice for colour, normal and detail maps; `rgba8` holds a texture exactly, which is what pixel art and a mask whose exact codes matter need. `effort` is how long the encode searches for each block. `fastest` tries one candidate for an opaque block, `fast` takes about 6 times as long and `thorough` about 17 times, each landing closer to the source texels. What the GPU holds is the same size at every effort, and so is the time a load takes. `uastcRdo` trades accuracy for a payload that compresses smaller: `none` encodes each block as close to the source as the encoder finds, and `low`, `medium` and `high` let a block move further from the source when a nearby block's bytes would let the payload compress better. What the GPU holds is the same size either way; what moves is the size of `data.ztex` on disk and over the network, and the encode time, which a pass more than doubles. The encode runs on the CPU when the texture is created or a setting that decides its texels changes. A call from a task that can yield cooks off the frame and the task waits for it, so the world keeps running: a 1024x1024 texture with its mip chain takes about a second on a desktop CPU. A texture whose settings predate `compression` (stored before it was a setting) is healed by its first local edit: a `nearest`-filtered one becomes a `sprite` in `rgba8`, an `rgba16` or `rgba32f` one keeps its format, and every other one becomes `uastc` and is re-encoded; an older "no cap" becomes a `maxDimension` of 8192. Loading one changes nothing, and an edit arriving from another session or a texture installed from a library is never healed. A texture whose settings say `rgba8` is never compressed. ## Reading an asset from its ref `texRef:settings()` reads the container's settings with every schema default filled in and `texRef:setSettings({ maxDimension = 1024 })` patches them; `texRef:load()` decodes the payload's pixels into the CPU store for per-pixel access, `texRef:handle()` puts the texture on the GPU under the asset's own guid, and `texRef:preview()` renders it as a swatch. `texRef:info()` reads the compiled `data.ztex` payload's header: the format it was compiled to — also as its `compression` and `colorSpace` — the `textureType`, `alpha` and `alphaIsTransparency` it was encoded under, its width and height, its mip and array-layer counts, the sampler recorded in it (`filter`, `wrapU`, `wrapV`, `anisotropy`), and, for an animated sequence, each layer's display time and the length of one pass: ```lua local info = texRef:info() print(info.width, info.height, info.format, info.mipCount) ``` It answers off that payload's own header, so a compiled texture reports how big it is for a header read where `load()` decodes its pixels and `handle()` uploads them. A container whose primary is a source image answers `(nil, err)`, and `load()` reads its size. The `compression` and `colorSpace` it reports are what the payload holds; the settings table holds what the encode was asked for. ## In the Inspector Selecting a texture opens its **Texture** section right below the asset's identity, then its Settings. The section draws the encoded texture through the texture's own sampler, so a `filter = "nearest"` texture reads crisp at every zoom. The zoom row offers **Fit** and each integer zoom (1×, 2×, 4×, 8×) whose drawn size fits 512 px; a 128 px texture offers up to 4×, and one wider than 512 px offers Fit alone. Below the view, the section lists every field of the `data.ztex` header (`texRef:info()`) — size, format, mip levels, filter, layers, and a sequence's frames and duration — and the GPU memory the encoded mip chain takes across its layers. The filter and the other encode settings change in the Settings section; the Texture section reports the re-encoded result.
▲ 0↑ born
backing path · models/Sample/ChronographWatch.bundle

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.