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.

ts
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.

ts
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.

Source

Edit this page on GitHub ↗