The Shinra tool (src/shinra) produces binary engine assets and validates them before packaging. 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/.
| 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 | project scenes/*.json (kind: com.hikari.scene) | Schema-neutral binary value tree (HSC1; Shinra --scenes) |
.shinprefab | assets/prefabs/**/*.prefab.json (kind: com.hikari.prefab) | Schema-neutral binary value tree (HSC1; Shinra --prefabs) |
.shinparticle | .particle.json | Compiled GPU particle program (SPT2) |
.shinanimgraph | .animgraph.json | Skeletal animation state machine (AGR1 v5 — layers, foot IK, root motion, 1D/2D blend spaces) |
.shinspriteatlas | .spriteatlas.json | Sorted sprite rect/pivot/nine-slice metadata referencing a normal texture (SAT1) |
.shinfont | .ttf | Validated TrueType pass-through |
.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. 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.
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.jsonhasmesh,materials[](one slot), and a one-entryparts[]. - 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_trianglescontrols authoring/residency granularity,max_extentcontrols preferred bounds, andmin_trianglesprevents 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 v5 meshlets; runtime camera and shadow submission culls those meshlets and merges compatible work into pipeline buckets. Emitsmodels/<stem>_p<N>.shinmodel+models/<stem>_m<N>.material.json; no stem.shinmodel/.material.json..model.jsonsetsmeshtoparts[0].mesh, lists all materials, and recordsparts[](name,mesh,material). - Editable hierarchy placement: set
model.placementtoeditable_hierarchyin import settings. Static primitives are no longer merged by material; the generated.model.jsonembeds acom.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 usemergedplacement. - Scene placeable: actors set
"components": { "render": { "model": "asset://…/<stem>.model.json", … } }plus optional"materials": [...]slot overrides (com.hikari.scenedocument version 1). Runtime binds oneRenderComponentwith N primitives (one draw per slot). 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/ForwardLitwithtransmission,ior,thickness,attenuation_color, andattenuation_distance. Imported anisotropy (glTFKHR_materials_anisotropy; FBX Arnold / OSL / 3ds Max Physical / OpenPBR) maps toanisotropy_strength,anisotropy_rotation, and the anisotropy texture. Imported clearcoat (glTFKHR_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.transparentselects the forward path;params.transmission > 0further selects the refractive PSO (depth write, no blend).flags.alpha_modeisstraightorpremultipliedand defaults tostraight.flags.preserve_authored_normalskeeps custom foliage normals oriented as authored in raster and RT (default false; also available in exact-name model overrides).flags.double_sideddisables back-face cull;flags.alpha_cutoff(>0) enables Mask discard in the G-buffer (glTFalphaMode: MASK). - Graph-authored material: still a version-1
com.hikari.materialdocument and still cooks to SMA2. Its optional top-levelgraphobject haskind: com.hikari.material-graph, graph schemaversion: 1, stable-idnodes[], and pin-ordinallinks[]. Surface graphs pointmaterial.shaderat the deterministicmaterial_graph_<material-id>package (a reserved namespace — Shinra rejects a graph package outside it) andmaterial_graph_vertex/material_graph_fragment; skinned meshes use thematerial_graph_vertex_skinnedtwin. Volume graphs have no surface shader package: Shinra folds canonical medium properties and emits a bounded extinction program directly into SMA2. A surface document withoutgraphis 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 withoutgraphhas no such form:domain: volumewith only amaterial.volumeblock 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..16split 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. This replaced a linear RGB565, which quantised the channels against different grids: red and blue died below 0.26 while green survived, so a dim grey glow came 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_texturenode fills the material'snormalslot, and the surface'sNormalinput (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_normalinbrdf.akari, shared with the standardgbufferand 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.fresnelmeasures 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
textis its single string payload: an asset path on a sampler, Akari source on a Custom Expression. A sampler with notextlowers to its neutral constant with no fetch and no branch — an unfinished node costs nothing until it is finished — while an assigned one keeps theMapMaskguard, because assigned in the document is not the same as resolved at load. material.textures/material.paramsare load-bearing, not decoration. The ray-traced path samplesHikariGpuMaterial, 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 intoparams. 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, soAlbedo Texture × Colorwrites 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.
- 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
.shinmodel(SRM1 v5): 64-byte mesh descriptors with fullf32×3world AABB min/max, a cook-selected ray-tracing LOD bias, and per-LOD meshlet topology. Import bakes node TRS (glTFdecomposed()/ ufbxlocal_transform), generates missing normals, flips winding on negative-scale bakes, and preserves authored tangents when present. Version 5 is a hard cut: the on-disk index buffer is gone. 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. After meshes/materials the optional skeleton/animation trailer is:- 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
Foxships three vertices per triangle) would otherwise fill every meshlet to the 64-vertex cap at 21 triangles. - 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
buildMeshletsScanthen 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). Older cooks (v4 and below) are rejected. - The layout is hand-mirrored in three places with no shared schema — the Rust encoder, the Rust reader, and
src/hikari/src/assets/formats/model.zig.src/shinra/fixtures/shinmodel/srm1_v5_meshlets.shinmodelis the committed file all three are tested against; regenerate it withHIKARI_WRITE_SHINMODEL_FIXTURE=1 cargo test -p shinra goldenand update the Zig expectations inmodel_mesh.zigin the same change. - Skeleton (
joint_count > 0):joint_count ×(name_hashu32+ parent_indexi16+ bind_matrixf32×16IBM, column-major) = 70 B/joint; then mandatory rest-local TRSjoint_count ×(pos3 + rot4 xyzw + scale3) = 40 B/joint; then joint name strings in palette order (u16len > 0 + UTF-8, max 256 B).name_hashmust equal FNV-1a of the name. Runtime skin:skin[j] = global_pose[j] * ibm[j]. - Animations (
animation_count > 0): per clip a 16-byte header (name_hash, duration, ticks_per_second, channel_count, flags) +channel_count ×16-byte channel headers (joint_index, path T/R/S, interpolation, key_count, time_offset, value_offset relative to payload base after that clip’s channel headers) + tightly packedf32times/values. After all clip track payloads: clip name strings inanimations[]order (u16len + UTF-8). Explicitkey_count— no trailer length estimation. Flags: bit0 compressed (reserved — writers never set it; readers reject it), bit1 uniform_times.
- 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
.shinmaterial(SMA2): header byte 22 selects thesurfaceorvolumedomain; bytes 23–26 carry the volume-program word count.FLAG_DOUBLE_SIDED(16),FLAG_ALPHA_MASK(32), andFLAG_PRESERVE_AUTHORED_NORMALS(256) apply to surfaces; alpha cutoff is anf32at header bytes 18–21 when the mask flag is set. The fixed parameter block carries surface and canonical volume properties, followed by the string-offset table, an optional validated volume program (eight words per instruction, at most 24 instructions), then the string heap. Format version 4 carries the surface anisotropy/clearcoat parameters and texture slots alongsidephase_second,phase_blend, andmulti_scatterfor volumes. Both readers reject older versions.
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.
| 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 trailer |
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). Flag bit4 (ibl_split_sum) marks that path and a 108-byte trailer of 27×f32 L2 SH coeffs (RGB-interleaved) after the last mip payload.
.shintexture sampler bits (header)
64-byte SRT1 header field after array_size (formerly reserved 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 occupies SRM1 header bit 6; it does not change the v2 payload 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 (3.0): binary default → {name}.shinbundle with an embedded TOC: every entry payload starts on a 4 KiB boundary, then a fixed-record table (80 bytes per entry: FNV-1a 64 of the ASCII-lowercased key, offset, stored size, uncompressed size, key offset/length into a string blob, compression, asset type, SHA-256), then a 48-byte footer (SHNBNDL3). 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 pre-3.0 bundle (no footer) is refused at open with TocMissing; 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. Exception: cook --bundle-strategy first-level-folders (debug/specialist) still writes a stem map for partitioned art.
Art packs include cooked binaries only: .shintexture / .shinmodel / .shincollision / .shinmaterial / .shinmodeldoc / .shinaudio / .shinparticle / .shinanimgraph / .shinspriteatlas / .shinfont / .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 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:
{ "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.
{
"model": {
"collision": {
"mode": "convex_decomposition",
"max_hulls": 16,
"concavity": 0.002,
"min_volume": 0.001
}
}
}Binary: 48-byte SCC3 header + fixed 80-byte descriptors for each tightly packed part + 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.
"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.