Assets
Turian uses an import pipeline: the files you drop into a project's assets/
folder are sources. On import each source is assigned a stable GUID (stored
in a sidecar <file>.meta) and cooked into a runtime artifact under
.cache/assets/. The shipped game reads those artifacts from a packaged .oap
and never touches the loose assets/ folder. References between assets (a
material's textures, a MeshRenderer's mesh) are stored as GUIDs, so they
survive renames and moves.
Importing models (glTF / GLB / OBJ)
Dropping a .gltf, .glb, or .obj into assets/ imports its geometry. For
glTF/GLB, Turian also reads the file's materials and textures and
generates engine assets from them — a single model is a source that produces
many assets:
- one
.materialper glTF material, mapping the glTF metallic-roughness model onto the built-in PBR shader (base color, metallic, roughness, emissive + strength, normal scale, occlusion, alpha mode/cutoff, double-sided → cull); - one texture asset per image it references.
These generated assets are recorded in the model's .meta and keep their GUIDs
across reimports, so scene references stay valid. Unsupported material extensions
are warned about, never fatal.
Geometry is cooked to a canonical binary mesh at import time, so the runtime loads it with one fast, format-agnostic path — OBJ/glTF/cgltf parsing happens only in the editor, never in the shipped game.
Editing generated sub-assets
Select an imported model in the asset browser; the inspector lists its generated materials/textures under Generated Assets. Click a material to open it and tweak (e.g. its base colour). Edits persist across reimports — the importer only (re)generates missing materials. A full Reimport clears the cache to regenerate everything from source.
Node hierarchy and per-mesh geometry (glTF/GLB)
A glTF/GLB file's scene graph — named nodes, parent/child transforms, and which mesh each node uses — is imported too, not just materials and textures. Alongside the generated materials, Turian writes:
- one mesh sub-asset per glTF mesh, with its own submeshes and name, kept separate from the model's own cooked artifact (which stays the whole file flattened into one mesh, unchanged, for a simple single-object drop);
- one Hierarchy sub-asset — a Prefab with one GameObject per glTF node, named and positioned to match the source, each mesh-bearing node wired to its mesh and generated materials.
Find it in Generated Assets next to the model's materials, and drag it into the Scene Viewport — or right-click it and choose Instantiate into Scene — to bring in the whole named hierarchy at once, exactly like instantiating any other prefab.
Only glTF/GLB carry node hierarchy today; OBJ still imports as a single flat mesh with no submeshes. Skinning/rigging isn't imported either — a rigged file's joint nodes come in as ordinary (non-animating) GameObjects.
Import settings
Selecting a texture or model shows an Import Settings panel:
- Image — texture type (default / normal map / sprite / UI / HDR), color space, mipmaps, compression, filter, wrap, max size, and (DDS only) a green-channel flip for DirectX-convention normal maps.
- Model — import materials, import animations, scale factor.
- Font — default size.
Settings are stored in the .meta; Apply re-cooks the asset.
External vs embedded textures
- External images (a glTF that points at sibling
.png/.jpgfiles) become ordinary texture assets with their own.meta— so you can select and swap them like any other asset. - Embedded images (GLB binary chunks, or base64 data URIs) are extracted into cache-only texture assets during import.
Either way the generated material binds each map by GUID. Common formats (PNG, JPEG, …) are decoded to RGBA8.
Texture formats
| Format | Path |
|---|---|
| PNG / JPEG / BMP / TGA / WebP | Decoded to RGBA8 (stb_image). |
KTX2 (.ktx2) |
GPU block formats uploaded directly — BCn passthrough, Zstandard inflated, and Basis Universal (ETC1S / UASTC) transcoded to BC7. Mip levels are preserved. |
DDS (.dds) |
Legacy FourCC (DXT1/DXT3/DXT5/ATI1/ATI2) and DX10-extended headers, block-copied through untouched — BC1/BC3/BC4/BC5/BC7, no re-encode. Mip levels are preserved; cubemaps/arrays are not supported. |
KTX2 support lives in a standalone, engine-independent
ktx2 module (with a vendored Basis
Universal transcoder), slotted in behind loadTexture so materials and
importers stay format-agnostic. DDS is parsed and decoded by
engine/assets/DdsLoader.zig, which reuses the ktx2 module's Format/Level
types so both containers produce the identical GPU-ready shape.
Since legacy DDS FourCC has no sRGB bit, the importer's Color Space
setting is baked in at import time: a DDS tagged sRGB is rewritten to carry
a DX10 extended header with the matching sRGB DXGI format (upgrading the
container only when needed); a normal map with Flip Green Channel enabled
has its BC5 green channel inverted losslessly, block by block, to match the
engine's Y convention.
PNG/JPEG/etc. sources are always decoded to linear RGBA8 by stb_image, so a
Color Space of sRGB is cooked in the same way: a tiny envelope is
prefixed to the source bytes at import time, which the loader strips before
upgrading the decoded texture's GPU format. Linear needs no cooking, since
it already matches the decoder's default.
Normal maps, metallic-roughness maps, and occlusion maps referenced by a
glTF/FBX material default their sibling image's Color Space to Linear
(and, for the normal map, Texture Type to Normal Map) the first time
that image is imported — subsequent reimports never overwrite a setting
you've since changed by hand.
PBR materials
A .material is a small JSON asset that references a shader by GUID and stores
values for the parameters that shader exposes. The built-in PBR
(Metallic-Roughness) shader exposes:
| Parameter | Kind | Notes |
|---|---|---|
base_color |
color | albedo tint (RGBA) |
metallic, roughness |
scalar | 0–1 |
emissive, emissive_strength |
color / scalar | |
normal_scale, occlusion_strength |
scalar | |
alpha_cutoff |
scalar | for masked transparency |
albedo_map, metallic_roughness_map, normal_map, emissive_map, occlusion_map |
texture | bound by GUID |
Because a generated material is a normal asset, you can edit it in the inspector
or swap a mesh to a different material without touching the model. When you
set a MeshRenderer's mesh to a model and its material is still empty, Turian
fills it with the model's primary generated material automatically.
You can also create materials from scratch via the asset browser, including built-in presets (Default, Metal, Plastic, Emissive, Glass).
Multi-material meshes
A single mesh can carry many submeshes, each tagged with a material slot.
A MeshRenderer holds one material per slot — its Material list in the
inspector shows one row per slot ("Material 0", "Material 1", …) — and every
submesh draws with the material bound to its slot. Slots, not submeshes, are the
unit: a model with thousands of submeshes sharing a handful of materials only
needs that handful of slots. This lets whole scenes flattened into one mesh
(hundreds of materials) render fully, each surface with its own PBR material.
When you set a MeshRenderer to a model, Turian auto-fills every slot with the
material generated for it; you can then swap any individual slot.
Font assets
Dropping a .ttf or .otf into assets/ registers it as a Font asset —
GUID, .meta, asset browser tile, and an Import Settings panel (default
size) like any other asset.
Selecting a font shows a live Preview: Studio registers the font's real
bytes with dvui (once per session, keyed by GUID) and renders actual sample
text at a few sizes, next to a comparison line set in the dvui theme's
default body font — so you can judge a typeface before using it anywhere.
examples/basic-project/assets/fonts/ ships three SIL OFL–licensed fonts
(Lora, Bebas Neue, Inconsolata — see ATTRIBUTION.md there) chosen for
visual contrast (serif, condensed display, monospace) as a working demo.
A .uidoc text node can reference a Font asset directly: set style.font to
the font's GUID (and optionally style.font_size, default 24) and it wins
over style.font_style's theme-name lookup. The font's bytes are resolved
through the same GUID-keyed source callback the renderer already uses for
images, then registered with dvui once per session — shared by the Studio
viewport (edit-mode overlay, Play mode, and the .uidoc editor's own
preview) and the shipped game alike, so no #if EDITOR-style branching is
needed anywhere. examples/basic-project/assets/ui.uidoc's title uses
Bebas Neue this way as a working example.
A Theme asset (issue #104's epic) that sets a font for a whole document at once, instead of per-node, is tracked as later, non-blocking work.
Example
The 3d-model-materials
example shows three objects side by side: an OBJ cube with a built-in Metal
preset, the Khronos WaterBottle as glTF (external .png maps), and the
same bottle as GLB (embedded maps extracted on import). It is structured to
scale up to large scenes such as Sponza or Bistro — drop the model into
assets/models/, open the project, and add it to a scene.
See also Component Reference for the MeshRenderer
component and Project Settings for the boot
scene.