# Asset formats (Shinra pipeline)

The Shinra tool (`src/shinra`) produces binary engine assets and validates them before packaging. Every authored JSON document (materials, scenes, particles, configs, …) has a rendered JSON Schema under `src/hikari/schemas/`; see [Document schemas](/docs/systems/document-schemas.md) for the envelope, validation and versioning rules. Runtime readers are brand-neutral under `src/hikari/src/assets/formats/` and `src/hikari/src/assets/bundle/`; processors and validators stay in `src/shinra/src/`.

## One container, one definition per format

Every cooked file the engine reads shares one container, and every format is defined once, as a Zig type. Shinra is the only compiler; the runtime never parses an authoring document.

- **Container** (`src/hikari/sdk/src/cooked.zig`): a 32-byte header (`magic[4]`, container version, header size, **layout hash**, meta size, file size), the root record at offset 32, then the lists, strings and value trees it points to; blob payloads (vertex data, mips, audio, native programs) follow the meta region at their declared alignment. Little-endian, read in place with no alignment requirement on the buffer. A streaming reader loads the meta region and fetches blobs by range.
- **Layouts** (`cooked_layout` in each format file): extern structs built from scalars, fixed arrays, exhaustive enums, packed flag structs, `List(T)`, `Str`, `Value` (a schema-neutral HSC1 value tree), `Blob(align)`, and checked `Index` / `OptIndex` / `Range` references. `cooked.View` validates every offset, enum tag and reference once at load; after that, reads are plain field access.
- **Registry** (`src/hikari/src/assets/cooked_layouts.zig`): one row per format. Every asset kind is either a row or an exclusion with a reason (Kawa bytecode, shader libraries), checked at compile time. The rows render `src/hikari/schemas/cooked/<name>.layout.json`; a freshness test fails when a rendered descriptor is stale. The same file renders `shared_rules.json`, test vectors for the rules both sides implement in code (name hashes, the bundle key hash, partition cells), which Shinra replays.
- **Rust side**: Shinra's build script generates `cooked::<name>` from the descriptors — structs, enums, `encode` (which checks every reference first) and a validating `decode`. There is no hand-written reader or writer on either side. `shinra validate` and bundle sealing run `cooked::check` on every cooked file.
- **Cook cache**: a cooked output must carry the signature and layout hash expected by its extension, with a complete header and matching file size. Stale, truncated, or old-format outputs are recooked, so changing a format needs no cache-version bump.
- **Golden fixtures**: `src/shinra/src/cooked/fixtures.rs` lists authored documents whose Shinra output the engine's Zig tests embed, so both sides are tested on the same bytes.

Changing a format: edit the Zig type, then run `zig build generate-cooked-layouts -Dgame-src=<abs>` from `src/hikari`. Next, rebuild Shinra and refresh fixtures with `cargo test regenerate_cooked_fixtures -- --ignored`.

| Extension | Source inputs | Runtime purpose |
|---|---|---|
| `.shintexture` | PNG, JPEG, EXR, HDR, DDS (BC1–BC5 / ATI2), TGA; also extracted from glTF/GLB/FBX/OBJ/USDZ | Texture data (albedo / normal / cubemap) and platform-compressed or HDR mip chain |
| `.shinmodel` | glTF, GLB, FBX, OBJ, USDZ | Model/mesh data (node TRS baked into vertices) |
| `.shincollision` | glTF/GLB/FBX/OBJ/USDZ when `model.collision` meta is set | Cooked mesh, convex hull, or decomposed compound (`SCC3`) |
| `.model.json` | auto-emitted beside a source model (or hand-authored) | Authoring placeable: `mesh` + `materials[]` + `parts[]` (editor / loose) |
| `.shinmodeldoc` | cooked from `.model.json` | Packaged placeable parts list (`SMD1`; scenes still reference `*.model.json`) |
| `.shinaudio` | WAV, OGG | Audio payload and playback metadata |
| `.material.json` | authored JSON, or auto-emitted beside a source model | Authoring material template (editor / loose) |
| `.shinmaterial` | cooked from `.material.json` | Packaged surface/volume material template (`SMA2`; scenes still reference `*.material.json`) |
| `.shinscene` | any `assets/**/*.json` with `kind: com.hikari.scene` | The document as a schema-neutral value tree in the container (`SCN1`), cooked beside its source path; a partitioned scene also cooks `<stem>/cells/cell_<x>_<y>_<z>.shinscene` per occupied cell |
| `.shinprefab` | any `assets/**/*.prefab.json` (or `kind: com.hikari.prefab`) | The document as a schema-neutral value tree in the container (`PFB1`), cooked beside its source path |
| `.shinparticle` | `.particle.json` | Compiled GPU particle program (`SPT2`) |
| `.shinbrain` | `.brain.json` | Shared AI states, fact conditions, transitions and duration parameters (`HBR2`); see [Brain assets](/docs/guides/brain-assets.md) |
| `.shinanimgraph` | `.animgraph.json` | Skeletal animation state machine (`AGR1` — layers, foot IK, root motion, 1D/2D blend spaces) |
| `.shinspriteatlas` | `.spriteatlas.json` | Sorted sprite rect/pivot/nine-slice metadata referencing a normal texture (`SAT1`) |
| `.shinui` | `.ui.json` | Data-driven UI document: node tree, bindings, slots, theme and fonts (`UID1`; see [UI documents](/docs/systems/ui-documents.md)) |
| `.shinfont` | `.ttf` | Validated TrueType face in the container (`SFN1`) |
| `.shinloc` | `<domain>/<locale>.localization.json` | Cooked localization strings; see [Localization](/docs/guides/localization.md) |
| `.kawabc` | `.kawa` | Compiled Kawa bytecode |
| `.shinthumb` | sidecar of a cooked texture/audio/… | Editor thumbnail bitmap (`<cooked>.shinthumb`) |
| `.shinpeaks` | sidecar of `.shinaudio` | Editor waveform envelope (`<cooked>.shinpeaks`) |

**Model import (multi-output):** one source model cooks to derived `models/<stem>_img<N>.shintexture` for **unique** used images (byte-identical maps intern onto one slot), companion materials, and `models/<stem>.model.json`. Sidecar files already referenced by the model are not cooked again as standalone textures. Generated JSON carries `"generator": "shinra.model"`; hand-authored files at the same path without that marker are left untouched. Editor model drop reads `.model.json` (no stem renaming). glTF/GLB, FBX, OBJ, and USDZ share every stage after import — the front-ends in `shinra/src/import/` all produce the same `ImportDocument`.

**OBJ specifics** (`shinra/src/import/obj/`): Wavefront OBJ + MTL, no extra crate. Faces fan-triangulate and weld on `(position, uv, normal)` indices. UV V is flipped into top-left space. The file is already treated as right-handed Y-up metres (no FBX-style axis/unit conversion). Missing `vn` is left empty so the cook generates area-weighted normals. Classic Phong maps onto the glTF-shaped IR: `Kd`/`map_Kd` → base colour, `Ns` → GGX roughness `√(2/(Ns+2))`, metallic 0. `illum` 4/6 (or a black `Kd` with a non-white `Tf`) becomes **transmission** on the forward-lit path; `d`/`Tr` without that glass flag becomes **alpha** (blend). Alpha in the `map_Kd` of an otherwise opaque material is a **cutout** (Mask, cutoff 0.5) — the same rule as FBX Phong; two-sidedness has no MTL encoding and is set per material in the sidecar's `material_overrides`. Unset glass `Ni` (1.0) is raised to 1.5. Grayscale `bump`/`map_bump` is Sobel-converted to an OpenGL tangent-space normal (`-bm` scales the slope); already-blue maps pass through. Missing textures warn and drop to factors. `.mtl` and `map_*` files are cook-cache dependencies; sidecar images are model-owned and not cooked twice. Lines are parsed borrowed and the freshness scan streams `mtllib`, so gigabyte OBJs are not duplicated in memory.

**USDZ specifics** (`shinra/src/import/usd/`, backed by the native-Rust `openusd` crate — no C++ OpenUSD): a `.usdz` is a 64-byte-aligned uncompressed ZIP whose first USD layer (USDA or USDC) is the stage root; textures stay inside the archive and are decoded with the same PNG/JPEG/DDS/TGA path as the other importers. `UsdGeomMesh` n-gons fan-triangulate (control cage only — Catmull-Clark is not subdivided). UV V is flipped into top-left space. `metersPerUnit` (UsdGeom fallback 0.01) and `upAxis` (fallback Y) land on a root helper node, never on vertex data, matching the FBX axis/unit fold. `UsdPreviewSurface` maps onto the glTF-shaped IR: `diffuseColor` → base colour, `opacity` / `opacityThreshold` → Blend / Mask, `metallic` + `roughness` pack into one metallic-roughness image when both are textures of equal size (G = roughness, B = metallic, R = occlusion when present). Separate maps of mismatched size drop to factors with a warning, like FBX. Missing textures warn and drop to factors. Material bindings walk `material:binding:preview` then `material:binding` (including `MaterialBindingAPI` when applied) and `GeomSubset` `materialBind` families. **UsdSkel** (`import/usd/skel.rs`): the first `Skeleton` with bound, weighted meshes becomes the model's one skin. Joints become nodes under the Skeleton prim's node (rest from `restTransforms`, else derived from `bindTransforms`), so the cook's palette closure and the units/up-axis helper wrap them like a glTF rig. Bound meshes (`SkelBindingAPI` applied, `skel:jointIndices` / `skel:jointWeights`, `elementSize` influences, `skel:joints` subsets, `constant` interpolation = rigid, `geomBindTransform`) get their strongest four influences per point, renormalised; their bind points and the inverse bind matrices both fold `geomBindTransform × Skeleton local-to-world × helper` so glTF skinning (`v × IBM × G(t)`, mesh node ignored) reproduces UsdSkel skinning exactly. Every distinct `skel:animationSource` (inherited, or bound directly on the meshes as Sketchfab exports do) becomes a clip: joint-local T / R / S sampled at every authored time code, `timeCodesPerSecond` → seconds, animation joint order remapped onto the skeleton, unauthored paths left unkeyed. Joint scale keys are kept — the runtime sampler applies them, so FBX-derived `×100 / ×0.01` chains render correctly. Blend shapes and a second skeleton are not imported (extra skeletons' meshes import static, with a warning).

**FBX specifics** (`shinra/src/import/fbx/`, backed by the `ufbx` C library): axes and units convert to the engine convention at load (right-handed Y-up, metres) and land on node transforms, never on vertex data. FBX geometric transforms and non-standard inherit modes become helper nodes, since the IR carries only T/R/S. UV V is flipped into top-left space. Legacy Lambert/Phong `TransparentColor × TransparencyFactor` becomes ordinary **alpha** because FBX does not distinguish coverage from physical glass. Exact-name `model.material_overrides.<name>` sidecar authoring can either request `transmission_from_alpha` or explicitly replace PBR factors (`base_color`, `metallic`, `roughness`, `emissive`, `emissive_strength`, `transmission`, `ior`, `thickness`, `attenuation_color`, `attenuation_distance`, and `double_sided`). Invalid values and stale material names fail the cook, so this is durable source-format repair rather than a material-name heuristic. Real PBR shaders (Arnold / OpenPBR / 3ds Max Physical / glTF) preserve their explicit transmission, and map specular anisotropy plus clearcoat into the same companion slots as glTF `KHR_materials_anisotropy` / `KHR_materials_clearcoat`. Arnold and OSL store anisotropy rotation as a 0–1 factor over 180°; 3ds Max Physical stores degrees — both become radians at import. Greyscale anisotropy amount maps pack to the glTF layout (RG = +X, B = strength); RGB maps already in that layout pass through. DirectX clearcoat normals flip green with the base normal. Lambert/Phong has no coat or anisotropy lobe, so those slots stay empty. Animation is baked to 30 Hz linear T/R/S keys per anim stack, trimmed to start at t=0. Skin deformers with an identical joint palette *and* identical inverse binds are interned into one skin; anything else stays separate and meets the cook's one-skin-per-stem rule. Missing or undecodable textures warn and drop to material factors rather than failing the cook; separate metalness/roughness maps are dropped too, because the IR slot is a *packed* glTF metallic-roughness texture. Game-industry extras (filename and material-name tokens, never a scene-name branch): DDS BC1–BC5 (including ATI2 normals) decode to RGBA8; a Phong `SpecularColor` map named `*_Specular` provides packed roughness/metalness (G/B), while explicit `*_ORM` / `*_ARM` names additionally provide occlusion (R), all with metallic/roughness factors 1. DirectX normal maps flip green into the OpenGL IR; an otherwise opaque albedo with translucent texels becomes Mask (cutoff 0.5) so foliage cutouts depth-write; material-name tokens `DoubleSided` / `TwoSided` and `Masked` fill flags Phong does not carry; byte-identical images intern onto one cooked texture. Model-referenced sidecar files are not cooked a second time as standalone textures.

- **Single-part** (one baked primitive): `models/<stem>.shinmodel` + `models/<stem>.material.json`; `.model.json` has `mesh`, `materials[]` (one slot), and a one-entry `parts[]`.
- **Multi-part** (more than one material slot after merge): primitives that share a material index are merged into one slot by default. Large static environments can enable `model.spatial_clusters`: Shinra first merges compatible geometry, then recursively splits expensive ranges along their longest spatial axis. `target_triangles` controls authoring/residency granularity, `max_extent` controls preferred bounds, and `min_triangles` prevents sparse geometry from becoming tiny parts merely because it spans a large area. Defaults are 32,768 target / 8,192 minimum triangles, aligned with rigid LOD generation so ordinary clusters retain an LOD chain. Every emitted part is then converted to SRM1 meshlets; runtime camera and shadow submission culls those meshlets and merges compatible work into pipeline buckets. Emits `models/<stem>_p<N>.shinmodel` + `models/<stem>_m<N>.material.json`; no stem `.shinmodel` / `.material.json`. `.model.json` sets `mesh` to `parts[0].mesh`, lists all materials, and records `parts[]` (`name`, `mesh`, `material`).
- **Editable hierarchy placement:** set `model.placement` to `editable_hierarchy` in import settings. Static primitives are no longer merged by material; the generated `.model.json` embeds a `com.hikari.prefab`-shaped placement template. Dropping the model in the editor expands that template as ordinary scene actors in one undo step, with source-node grouping and one independently selectable render actor per primitive. Geometry remains world-baked so previews and initial placement agree; the group actors therefore carry identity transforms rather than reconstructed DCC pivots. The expansion deliberately has no prefab link or runtime template dependency. Skeletal models warn and use `merged` placement.
- **Scene placeable:** actors set `"components": { "render": { "model": "asset://…/<stem>.model.json", … } }` plus optional `"materials": [...]` slot overrides (`com.hikari.scene` document version 1). Runtime binds one `RenderComponent` with N primitives (one per slot); the renderer groups compatible meshlet work into pipeline buckets. Transparent slot materials disable shadow casts for that section only.
- Companion materials map imported PBR onto engine params. Opaque slots use `_engine/gbuffer` / `DeferredGeometry`; transparent / transmissive slots use `_engine/forward_lit` / `ForwardLit` with `transmission`, `ior`, `thickness`, `attenuation_color`, and `attenuation_distance`. Imported anisotropy (glTF `KHR_materials_anisotropy`; FBX Arnold / OSL / 3ds Max Physical / OpenPBR) maps to `anisotropy_strength`, `anisotropy_rotation`, and the anisotropy texture. Imported clearcoat (glTF `KHR_materials_clearcoat`; the same FBX PBR shaders plus glTF-in-FBX) maps its factor, roughness, and three texture slots without flattening either lobe into base roughness. `flags.transparent` selects the forward path; `params.transmission > 0` further selects the refractive PSO (depth write, no blend). `flags.alpha_mode` is `straight` or `premultiplied` and defaults to `straight`. `flags.preserve_authored_normals` keeps custom foliage normals oriented as authored in raster and RT (default false; also available in exact-name model overrides). `flags.double_sided` disables back-face cull; `flags.alpha_cutoff` (>0) enables Mask discard in the G-buffer (glTF `alphaMode: MASK`).
- **Graph-authored material:** still a version-1 `com.hikari.material` document and still cooks to SMA2. Its optional top-level `graph` object has `kind: com.hikari.material-graph`, graph schema `version: 1`, stable-id `nodes[]`, and pin-ordinal `links[]`. Surface graphs point `material.shader` at the deterministic `material_graph_<material-id>` package (a reserved namespace — Shinra rejects a graph package outside it) and `material_graph_vertex` / `material_graph_fragment`; skinned meshes use the `material_graph_vertex_skinned` twin. Volume graphs have no surface shader package: Shinra folds canonical medium properties and emits a bounded extinction program directly into SMA2. A **surface** document without `graph` is a hand-authored Akari material and is never inferred into a graph — there is a hand-authored form for it to stay in. A **volume** document without `graph` has no such form: `domain: volume` with only a `material.volume` block is the constant medium, and opening it in the editor synthesizes the equivalent graph (one constant per Volume Output input) so it can be edited at all. Cooking either shape produces the same SMA2; the synthesized graph only reaches disk if the author saves.
  - Emissive textures are not cooked opaque, so their **alpha survives** and carries no lighting meaning — which makes it the natural place to bake a per-pixel coordinate for a graph to animate against. The example bakes distance along each circuit trace there, and `fract(alpha − time × speed)` marches one bright head down the path. Panning the UVs instead would slide the whole pattern sideways, which is a different effect and usually the wrong one.
  - **Emissive is HDR, and its precision is relative.** The G-buffer packs it into 16 bits over `0..16` split across two alpha channels (`emissive_pack.akari`): one 4-bit exponent, stepping by half a binade, shared by three 4-bit mantissas. Because every channel is quantised against that one shared scale, a neutral colour stays neutral at any brightness and a 0.1 emissive resolves as finely as a 10.0 one — worst case 5.7% of the colour's peak, ~2.6% typical, and every power-of-two multiple of the range is exact. A linear RGB565 packing would quantise the channels against *different* grids, so a dim grey glow would come back pure green. Anything below ~0.003 still packs to black, and the editor warns when a folded emissive lands there.
  - **Normals and Fresnel.** A `normal_texture` node fills the material's `normal` slot, and the surface's `Normal` input (appended last, so older documents keep their pin ordinals) takes a tangent-space vector. The tangent frame is derived from screen-space derivatives of world position and UV — `hikari_tangent_to_world_normal` in `brdf.akari`, shared with the standard `gbuffer` and forward paths — so no mesh needs a tangent attribute and nothing interpolates one. A graph with no Normal wired emits no frame, no derivatives and no call at all. `fresnel` measures against the *shaded* normal, so a rim follows the mapped detail; wiring one into the Normal chain is refused by name rather than producing a shader that references a value before it exists.
  - A node's optional `text` is its single string payload: an asset path on a sampler, Akari source on a Custom Expression. A sampler with no `text` lowers to its neutral constant with **no fetch and no branch** — an unfinished node costs nothing until it is finished — while an assigned one keeps the `MapMask` guard, because assigned in the document is not the same as resolved at load.
  - **`material.textures` / `material.params` are load-bearing, not decoration.** The ray-traced path samples `HikariGpuMaterial`, never the lowered graph shader, so the editor writes each sampler node into *its own* bindless slot (`albedo`, `metallic_roughness`, `occlusion`, `emissive`, `anisotropy`, `clearcoat`, `clearcoat_roughness`, `clearcoat_normal` — one node per slot, a second is rejected) and constant-folds each surface input into `params`. The Surface Output exposes anisotropy direction/strength/rotation plus clearcoat factor/roughness/normal; older pin ordinals remain stable because these inputs are appended. A sampler standing in its own channel folds to the identity multiplier, so `Albedo Texture × Color` writes that colour and RT reproduces the graph exactly; a channel reaching Time or UV falls back to the same constant the generated shader uses. The node vocabulary itself is one table, `src/hikari/src/editor/material_graph/nodes.schema.json`, that the Zig editor renders at comptime and Shinra checks in a test — drift between the two is a build failure, not a material that silently stops producing a shader.
- **`.shinmodel` (SRM1):** mesh records with full `f32×3` world AABB min/max, a cook-selected ray-tracing LOD bias, per-LOD meshlet topology, and the interleaved vertex blob; joints, clips and channels share the same file. Import bakes node TRS (glTF `decomposed()` / ufbx `local_transform`), generates missing normals, flips winding on negative-scale bakes, and preserves authored tangents when present. There is no on-disk index buffer. Each LOD stores 64-byte meshlet descriptors (sphere@16 / cone@32), a global-vertex remap, and packed local micro-triangles (64 verts / 124 tris). The engine loader materializes a u32 index stream from that topology for BLAS and physics. Vertices are expanded into the packed 32-byte engine row (`hikari/sdk/src/vertex_layout.zig`: f32×3 position, oct snorm16×2 normal, unorm8×4 color, f32×2 uv, one reserved word; skinned rows add joints/weights for 40 B). Alpha-masked meshes that pass Shinra's card-topology classifier receive bias 2; ordinary cutouts emit zero and use Hikari's conservative masked fallback. Mesh processing, skeletons and animations follow these rules:
  - **Every primitive is welded at bake.** Byte-identical vertices (all present attributes equal bit for bit) collapse into one and the primitive is re-indexed, before LOD, cards, and meshlets. Lossless: hard edges and UV seams stay split. Unindexed glTF/OBJ content (the Khronos `Fox` ships three vertices per triangle) would otherwise fill every meshlet to the 64-vertex cap at 21 triangles.
  - **LOD collapses preserve UV and hard-normal seams.** Corners at the same position remain distinct when their UVs or normals differ; an edge collapse must pair every surface wedge with a unique wedge along that edge. This protects frame openings and recessed panels even when adjacent faces share UVs or have no UVs. A triangle budget may remain unmet rather than cross a hard seam. Whole-component thinning is restricted to alpha-cut materials, including when no edge collapse is possible.
  - **Meshlets are the on-disk topology.** Rigid triangles are Morton-sorted by centroid *before* meshlet construction so clusters start from a spatially coherent stream; skinned meshes run meshoptimizer's vertex-cache ordering instead (they draw as one submesh, so spatial order buys nothing). A linear `buildMeshletsScan` then packs that stream into 64-vert / 124-tri clusters — greedy adjacency/KD-tree clustering is ~14× slower on a 32k-triangle part and only ~10% denser after the same pre-pass. The cooker **permutes the vertex buffer into meshlet first-use order**: the engine's vertex shader is nothing but a remap pull, so remap locality is the dominant raster memory cost and the on-disk remap is therefore near-sequential. The cook log reports meshlet fill (average triangles/vertices, cone validity, and the count of meshlets under 32 triangles that their submesh does not explain — a small submesh or a submesh's last meshlet is never counted).
  - The records are the `model` layout; what no layout can describe — the interleaved vertex blob's attribute order and the cooked triangle order — is pinned by `src/shinra/fixtures/shinmodel/srm1_v5_meshlets.shinmodel`, which both the Rust decoder and `model_mesh.zig` are tested against. Regenerate it with `cargo test regenerate_shinmodel_fixture -- --ignored` and update the Zig expectations in the same change.
  - **Skeleton:** joint records in palette order — inverse bind matrix (`f32×16`, column-major), rest-local TRS (rotation xyzw), name, `name_hash` (FNV-1a of the name) and parent. Runtime skin: `skin[j] = global_pose[j] * ibm[j]`.
  - **Animations:** clip records (name, duration, ticks per second, flags) own a range of channel records (joint, path T/R/S, interpolation), each naming its times and values as ranges of one shared `f32` key list. The runtime checks that times are sorted and finite and that the value count fits the path.
- **`.shinmaterial` (SMA2):** the `material` layout: domain (`surface` / `volume`), binding layout, flags (transparent, premultiplied alpha, double sided, receives decals, traced refraction, preserve authored normals, async lag), alpha cutoff, surface parameters and texture slots (including anisotropy and clearcoat), the folded volume medium (`phase_second`, `phase_blend`, `multi_scatter`, …), and an optional volume program (at most 24 typed instructions, checked by the runtime's register rule at load).

Output mirrors input folders and includes `assets.shinmanifest.json`. Unsupported types and dotfiles are skipped.

## Metadata sidecars

Each `<filename>.shinmeta.json` may contain optional `texture`, `audio`, and/or `model` sections. Model metadata includes `placement` (`merged`, the default, or static-only `editable_hierarchy`), optional workload-aware `spatial_clusters`, exact-name `material_overrides`, and LOD, optimization, CPU-access, and collision controls.

**`texture` section:** `type` (`albedo` / `normal` / `cubemap` / `metallic_roughness` / `occlusion` / `emissive`), mipmaps, sRGB, premultiplied alpha, compression quality, cubemap `face_size`, stop/max/min-edge controls, **sampler**: `address_u` / `address_v` (`repeat` \| `clamp` \| `mirror`, default `repeat`), `filter` (`linear` \| `point`, default `linear`).

Texture type drives preprocessing. Colour maps stay BC7 / ASTC 6×6; normals use a 2-channel-friendly format so XY is not sharing bits with an unused B and A.

The BC5 encoder takes tightly packed RG8 with a two-byte pixel stride. Shinra extracts XY from padded RGBA8 before encoding; passing RGBA8 directly would alternate real XY with blue/alpha values.

| Type | Defaults | Processing |
|---|---|---|
| `albedo` | sRGB on, mips on | sRGB-aware mips, BC7/ASTC |
| `normal` | linear, mips on | re-normalized mips; **BC5** (D3D12/Vulkan) / **ASTC 4×4** (Metal). Shaders reconstruct Z from XY. |
| `metallic_roughness` | linear, mips on | BC7/ASTC; glTF G=roughness, B=metallic |
| `occlusion` | linear, mips on | BC7/ASTC; sample R |
| `emissive` | sRGB on, mips on | sRGB-aware mips, BC7/ASTC |
| `cubemap` | linear, mips on | equirectangular → 6 faces; HDR sources (EXR/HDR) store RGBA16F; LDR uses platform compression; GGX-prefiltered specular mips + L2 irradiance SH |

Alpha-tested model textures preserve source alpha-test coverage across generated mip levels. Their colour is filtered with alpha weights, so transparent atlas padding cannot darken surviving foliage or railings as distance selects lower mips.

Cubemap layout: `width=height=face`, `array_size=6`, `cube_map` flag, faces packed `+X,-X,+Y,-Y,+Z,-Z` per mip. Mip 0 is the sharp sky; mips 1..N are GGX-convolved at roughness `i/(N-1)`. The `ibl_split_sum` flag marks that path; the texture layout's `irradiance_sh` field carries the 27×f32 L2 SH coefficients (RGB-interleaved).

### `.shintexture` sampler bits (header)

64-byte SRT1 header field after `array_size` (`u16`):

| Bits | Field | Encoding |
|------|--------|----------|
| 0–1 | `address_u` | `0` = repeat, `1` = clamp, `2` = mirror |
| 2–3 | `address_v` | same |
| 4 | `filter` | `0` = linear, `1` = point |
| 5–15 | reserved | must be 0 |

Zero (missing shinmeta / old assets) = wrap/wrap/linear — bit-identical to historical content. Runtime `Texture.sampler` carries the decoded `SamplerDesc`. Each material draw uses **one effective sampler** (albedo if set, else first bound map). That desc indexes the geometry **sampler catalog** (`gpu/sampler_catalog.zig`): D3D12 static samplers in space 1, Metal argument-buffer sampler table. AF applies to linear catalog entries.

| Section | Important settings |
|---|---|
| `audio` | volume normalization, sample rate, compression quality, looping, streaming hint, output format |
| `model` | `allow_cpu_access` (default `false`), LOD generation, mesh optimization, compression quality, **collision** (`mode`: `none` \| `mesh` \| `convex` \| `convex_decomposition`, optional `node` name filter) |

Static render geometry is GPU-resident after upload and its decoded CPU payload is released by default. Set `model.allow_cpu_access: true` only for systems that need to inspect render vertices at runtime. Physics does not require it: collision uses the separately cooked `.shincollision` companion. The flag is `ModelFlags.allow_cpu_access` in the `SRM1` layout.

Audio formats are `Pcm16Le`, `PcmF32Le`, and `OpusOgg` (wire magic **`SHN1`**). Shinra's `--target`, not runtime asset code, chooses texture compression.

## Debug output and bundles

`--emit-raw texture,audio` writes inspectable raw sidecars; `--emit-json-assets` writes base64 JSON mirrors with version, byte length, SHA-256, and data. JSON mirrors are omitted from release manifests.

**Bundle wire shape (4.0):** binary default → `{name}.shinbundle` with an **embedded TOC**: every entry payload starts on a 4 KiB boundary, then the table — a cooked `BTOC` file of its own (`bundle_toc` layout: FNV-1a 64 of the ASCII-lowercased key, offset, stored size, uncompressed size, SHA-256, key, compression, asset type per entry, sorted by hash) — then a 24-byte footer (`toc_offset`, `toc_size`, `SHNBNDL4`). The footer is the only hand-mirrored piece; a Shinra-written fixture bundle pins it for the Zig reader. The runtime maps the file, validates the table in place and binary-searches it — no parse, no per-key allocation; boot reads only the footer and table of each packed pack to build its path → pack index. `{name}.shinbundle.json` is still written beside it for humans and `shinra validate` (which checks the two agree), but nothing at runtime reads it; `{name}.shinbundle.statcache.json` is Shinra's rebuild cache. Writer `src/shinra/src/bundle/toc.rs`, reader `src/hikari/src/assets/bundle/toc.zig`. `--bundle-format json` → self-contained `{name}.shinbundle.data.json` (entries sorted by relative path: asset type, base64 payload, stored/uncompressed size, per-entry compression, hash of uncompressed data). `--compression zstd` is the pack default; each TOC entry records its actual compression. `.shintexture` entries stay uncompressed so mip ranges are directly addressable (their image blocks are already GPU-compressed), while other entries use the requested default. A bundle sealed with an older footer is refused at open with `TocMissing`, and one whose table has another layout with `InvalidToc`; re-seal.

**Catalog maps:** product seal writes **`product.shinbundle.map.json`** v3 (pack → delivery, deps, labels, loose root; `paths` only for loose packs — a packed pack's membership is its TOC). **`shinra pack` and cook `--bundle` do not emit per-stem `.shinbundle.map.json`** — membership is in each pack's TOC.

Art packs include cooked binaries only: `.shintexture` / `.shinmodel` / `.shincollision` / `.shinmaterial` / `.shinmodeldoc` / `.shinaudio` / `.shinparticle` / `.shinanimgraph` / `.shinspriteatlas` / `.shinui` / `.shinfont` / `.shinloc` / `.kawabc` / shaders. Scenes (`.shinscene`) and prefabs (`.shinprefab`) are domain-staged, not art-pack members. Authoring `.model.json` / `.material.json` / `.prefab.json` / `.particle.json` / `.animgraph.json` / `.spriteatlas.json` / `.ui.json` stay out of the pack; runtime remaps those URIs to the binary companions. Other `.json` sidecars (for example `.shinmeta.json`) stay out of the pack. Editor-only `.shinthumb` / `.shinpeaks` are not product pack members.

**`.shinspriteatlas` (SAT1):** a 32-byte header, hash/name-sorted fixed 40-byte records, and a length-prefixed UTF-8 heap. Each record stores a source rect (`u16`), normalized pivot, and left/top/right/bottom borders. Runtime validates once and performs allocation-free binary search by FNV-1a 64-bit hash with name comparison for collisions. Pixel data is intentionally absent; the header names an ordinary texture asset so compression, mips, hot reload, and GPU residency are shared with non-UI consumers.

### `.shincollision` (SCC3)

Offline physics geometry, companion of the cooked model (`models/foo.shinmodel` + `models/foo.shincollision`). Opt in via `<model>.shinmeta.json`:

```json
{ "model": { "collision": { "mode": "mesh" } } }
```

| `mode` | Output |
|--------|--------|
| `none` (default) | No file |
| `mesh` | Triangle soup from baked scene meshes |
| `convex` | Closed convex triangle surface plus exact unit-density mass moments |
| `convex_decomposition` | Concave closed surface decomposed offline into a bounded convex compound |

Optional `node`: only include baked pieces whose node/mesh name contains that substring (e.g. `"Collision"`).

Convex input must be closed, manifold, non-degenerate and convex. Shinra normalizes whole-mesh winding, validates every face, and cooks volume, center of mass and the full inertia tensor. Invalid content fails during cooking rather than producing an approximate dynamic body.

Decomposition uses V-HACD at a fixed internal resolution, then validates and bounds the result. `max_hulls` defaults to 16 (range 1–64), `concavity` to 0.002 (lower is tighter and slower), and `min_volume` to 0.001. Tiny parts merge into the nearest part instead of leaving holes. These controls affect only the offline cook.

```json
{
  "model": {
    "collision": {
      "mode": "convex_decomposition",
      "max_hulls": 16,
      "concavity": 0.002,
      "min_volume": 0.001
    }
  }
}
```

Binary: the `SCC3` layout — part records (vertex/index ranges plus mass properties), then flat `f32×3` vertices and `u32` triangle indices. Every hull descriptor stores unit-density volume, center of mass, and the full inertia tensor. Runtime caches the geometry once and expands a compound into ordinary Tenkai hull colliders, so decomposition adds no work to the simulation hot path.

```json
{
  "physics": { "collision": "asset://./models/foo" }
}
```

`--validate` checks processed formats and present bundles, including payload sizes, offsets, hashes, and map consistency. `--validate no-build` checks an existing output tree.


## AI Brain programs (`HBR2`)

Shinra validates `.brain.json` against `com.hikari.brain.v1.schema.json` and resolves named references into fixed tables: the `HBR2` layout in `sdk/src/ai/brain.zig` (states with their task and custom-task arguments, transitions with required/excluded fact masks, parameters, facts). Runtime decodes binary programs only. There are at most 64 states, 128 transitions, 16 parameters, 32 facts and 32 target slots; decode rejects invalid references, names, masks, nonfinite or negative durations, and nonpositive radii. The Rust cooker and Zig runtime share the golden fixtures in `src/hikari/sdk/src/ai/fixtures/`.

Worlds own decoded programs and expose callback-scoped borrows. Typed Brain references in custom component payloads participate in scene pack discovery and residency requirements. See [Brain authoring and runtime ownership](/docs/guides/brain-assets.md).
