---
title: "texture"
description: "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."
section: "Types"
slug: "types-texture"
canonical: "https://origozero.ai/docs/types-texture"
updated: "2026-09-05T16:41:47.493245824+00:00"
tags: ["asset-type", "reference"]
---

# 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`) — the conversion step
decides its shape. Two shapes are produced today:

### Plain image container

```
<name>.texture/
  <name>.png          the primary image (or .jpg / .webp / .hdr / …)
```

Produced by `asset.create("texture", name, { bytes, ext })` and the
svg→texture importer. The renderer decodes the image and generates mips on
upload.

### Managed (compressed) container

Produced by the **texture importer** when a loose raster image lands in
`/source/`:

```
<name>.texture/
  source.<ext>   the original raster, MOVED in (never lost)
  meta.yaml      compression settings — see below
  data.tex       canonical pre-encoded + pre-mipped payload (ZTEX magic)
  README.md
```

`data.tex` is the **preferred primary**: when present the renderer resolves
and uploads it verbatim — no decode, no re-mip. When absent the primary
falls through to the raster image, so plain containers keep working.

That fallthrough reaches every `*.png` in the folder, the generated
`preview.png` thumbnail included, so a container that lost its encoded payload
loads the thumbnail at the thumbnail's size. `asset.primaryFile(ref).path`
names the file the resolution landed on, and `asset.diagnose(ref)` reports
whether it can be used and why not — see the asset-system guide.

#### `meta.yaml` settings

Editing any of these re-runs compression against the sibling `source.<ext>`
and rewrites `data.tex`:

| key | values | meaning |
|-----|--------|---------|
| `format` | `srgb` (default) / `rgba8` / `bc7` / `bc7_srgb` / `rgba16` / `rgba32f` | output format. `srgb` for albedo/emissive/base-color; `rgba8` (linear) for normal/RM/AO/height; `bc7` / `bc7_srgb` for block compression — sixteen bytes per 4x4 texels, so the texture costs a quarter of RGBA8 on disk **and in VRAM**, at a small loss; `rgba16` / `rgba32f` for data rasters 8 bits per channel quantizes visibly. |
| `generateMipmaps` | `true` (default) / `false` | bake the full mip chain into `data.tex` at import time. |
| `srgb` | `true` / `false` | optional override applied when `format` is omitted or non-srgb. |
| `maxDimension` | integer, `0` = no cap | downsample so neither axis exceeds this. |

The `data.tex` payload format lives in `crates/zero_texture/src/blob.rs`
(`ZTEX` magic + 32-byte header + mip levels); the CPU encoder is exposed to
Luau as `__texture.*` and consumed by the texture importer and this assetType.

#### Block compression

`format = "bc7"` (or `"bc7_srgb"`) compresses every mip level to BC7 blocks —
sixteen bytes per 4x4 texels. The blocks are what reaches the GPU, so the
texture is resident at one byte per texel instead of four; an adapter without
BC7 support uploads the payload decoded to RGBA8 instead, so the texture is
correct everywhere and compressed where the hardware allows it. Compression is
lossy and runs on the CPU at import time, which makes it the choice for colour
and detail maps rather than for a mask whose exact codes matter.

Each 4x4 block is encoded under whichever BC7 mode reconstructs it closest:
one endpoint pair across the whole block, or two endpoint pairs over one of
the format's 64 fixed splits of the block's texels, which is what carries a
block whose colours vary along two directions at once. Searching those splits
is what the import spends its time on — roughly a second per megatexel on one
core, so a 4096x4096 import is a wait rather than an instant.
