Assets and loading
Load models, textures and sounds by their source name, show real loading progress, and release what a scene no longer needs.
On this page
How assets reach the game
You put source models, images and sounds in assets/. At build time @threenative/assets
compiles them into public/ and writes a manifest that maps each source name to its output file.
| Path | Contents |
|---|---|
assets/ |
Your source files. Add or replace files here. |
public/assets.manifest.json |
Source name to compiled output. The loader reads it. |
public/bake.receipt.json |
Every file the build produced, with its producer and source. |
You load through ctx.assets with the source name. The loader looks up the manifest, so your code
keeps working when a rebuild changes an output filename. Without a manifest it tries the verbatim
path and then the source directory.
Load in a scene
Load what the first frame needs in the scene's async load method. ThreeNative awaits it before
enter.
import type { ICtx } from "@threenative/core";
import type { GLTF } from "three/addons/loaders/GLTFLoader.js";
// In your Scene subclass:
private model: GLTF | undefined;
async load(ctx: ICtx): Promise<void> {
this.model = await ctx.assets.model<GLTF>("models/robot.glb");
}
// In enter(): ctx.add(this.model!.scene);The type parameter only describes the result. It does not validate the file.
| Method | Returns |
|---|---|
ctx.assets.model<T>(path) |
The parsed model |
ctx.assets.texture(path, options?) |
A Texture. With options you get a configured copy, so the shared cached one stays untouched. |
ctx.assets.audio(path) |
An AudioBuffer |
The loader caches every result, so a second call for the same path returns the same object. If
several characters animate independently, create each one with SkeletalMesh3D. See
Animation.
Formats and compression
The build writes each distinct model image once, compresses model textures to KTX2/Basis and can simplify geometry. Check the compiled result from the gameplay camera before you keep it.
Android and iOS hosts have no WebAssembly, so they have no Basis transcoder or Meshopt decoder.
For those targets the build skips the texture and model passes and ships resized PNGs instead.
KTX2 files that are already compressed in assets/ still fail native preflight there.
A ready GLB loads directly. Converting .fbx, .blend, .obj or .dae needs Blender. If
conversion fails, run npx threenative doctor --text and install Blender with the command it
prints. Keep each asset's license and attribution with your project.
Loading progress
ctx.assets.progress reports what the loader is doing.
| Field | Meaning |
|---|---|
requested, settled |
Loads asked for, and loads that resolved or rejected |
requestedBytes, settledBytes |
The same, weighed by manifest sizes. Both stay 0 without a manifest. |
pending |
Source paths still loading, in request order |
Drive a loading bar from the byte counts. A file count treats a huge model and a tiny icon alike.
settled includes failures, so handle a failed required asset yourself and show the player what
to do.
ctx.startup covers the rest of the launch, such as shader compilation and a stable frame
window. ctx.startup.progress runs from 0 to 1 and never goes backwards. whenReady() resolves
when the world is safe to show.
Hold startup for your own assets
If the game streams a second tier of assets after launch, register a hold during scene entry. Startup then waits for your work too.
const criticalTier = ctx.startup.whenFrameworkReady().then(async () => {
// Load and attach the critical content. Handle failures in your UI.
});
ctx.startup.hold("critical-tier", criticalTier, 15_000);Start that work from whenFrameworkReady(). whenReady() waits for every hold, so work started
from it waits on itself until the hold's budget runs out. A hold settles when its promise
resolves or rejects, or after budgetMs (45 s by default). hold() throws if startup has
already resolved.
Custom loaders and cleanup
For loaders ThreeNative does not wrap, such as an HDR sky or a font, call
ctx.assets.resolve(path). It returns the URLs to try in order. Never hard-code a compiled
output name, because it changes on every rebuild.
Call ctx.assets.release(kind, path) or ctx.assets.clear() once nothing uses a cached asset.
Removing an object from the scene does not free its GPU resources. Dispose scene-specific
resources in exit, and keep anything another instance still shares.
If a load fails, check the name, its manifest entry and the network or decoder error. See Troubleshooting.