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 .material per 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/.jpg files) 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.


← All docs Edit this page