Models, textures and asset loading

Load glTF models and images in parallel, recover from failures and map textures.

Load assets in parallel

Pass a URL string (or a bundler import) on the web, and require(...) or a URI in React Native; add glb and gltf to Metro's assetExts and install expo-asset for bundled Android release assets. Loaders start the engine themselves and cache by source and options, so a second call returns the same promise.

On iOS and Android, loadTexture, loadModel, loadFont and audio.load decode on native background threads, so Promise.all loads a level in parallel while the loading screen animates, and meshes register with the renderer inside Rust. The web's single-threaded core decodes on the main thread, so load before play, never in onUpdate.

Load a level (React)

React · browser

Load a level (React)
import { loadFont, loadModel, loadTexture } from "@nayan-ui/engine";
export async function loadLevel() {
const [tree, bark, font] = await Promise.all([
loadModel("/models/tree.glb", { center: false, fit: 3 }),
loadTexture("/textures/bark.jpg"),
loadFont("/fonts/Inter-Bold.ttf", { chars: "0123456789" }),
]);
return { tree, bark, font };
}

Load a level (React Native)

React Native · iOS and Android

Load a level (React Native)
import { loadFont, loadModel, loadTexture } from "@nayan-ui/engine";
export async function loadLevel() {
const [tree, bark, font] = await Promise.all([
loadModel(require("./assets/tree.glb"), { center: false, fit: 3 }),
loadTexture(require("./assets/bark.jpg")),
loadFont(require("./assets/Inter-Bold.ttf"), { chars: "0123456789" }),
]);
return { tree, bark, font };
}

Errors and retry

A failed load rejects with an Error naming the loader and source (loadTexture: couldn't load …), with the original error as cause. Failures aren't cached, so calling the loader again retries; a file the core rejects discards its saved bytes, while network and start-up failures keep them.

Offer a Retry action rather than retrying from onUpdate, and ignore results that arrive after unmount.

useTexture.ts

React and React Native · shared engine API

useTexture.ts
import { useEffect, useState } from "react";
import { loadTexture, type AssetSource, type Texture } from "@nayan-ui/engine";
export function useTexture(source: AssetSource) {
const [attempt, setAttempt] = useState(0);
const [texture, setTexture] = useState<Texture | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let active = true;
setError(null);
loadTexture(source).then(
(value) => active && setTexture(value),
(cause: Error) => active && setError(cause),
);
return () => { active = false; };
}, [source, attempt]);
return { texture, error, retry: () => setAttempt((n) => n + 1) };
}

Models and glTF limits

loadModel reads glTF 2.0: a .glb, or a .gltf with embedded buffers; external buffer files are rejected, so export a self-contained GLB. It keeps meshes, node transforms, vertex and material colors, one base-color texture per model, skins and clips, and decodes EXT_meshopt_compression, but it isn't a general importer for every material extension.

Every entity using a model draws in one instanced call, and colliders sized from it fit its bounding box. Ship third-party models with their licences.

Models and glTF limits reference
API / optionMeaning
centerMove the bounding-box centre to the origin. Default true.
fitScale so the largest side is this long. Default: source size.
shineGloss 0..1; about 0.8 reads as paint or glass. Default 0.
Model{ id, size }: a shared mesh handle with its fitted size, not an entity.
LoadedModelModel plus animations (ModelClip[]) and jointCount.
ModelClip{ name, duration }, duration in seconds.

Stand a centred model on the ground

React and React Native · shared engine API

Stand a centred model on the ground
import type { Model, World } from "@nayan-ui/engine";
// A centred model's bottom is half its height below its position.
export function placeProp(world: World, model: Model, x: number, z: number) {
return world.spawn({ mesh: model, position: [x, model.size[1] / 2, z], physics: "fixed" });
}

Textures, tint and atlas regions

loadTexture reads a PNG or JPEG up to 4096 × 4096, keeps its alpha and returns { id, width, height }; it maps through the mesh's UVs and the entity's color tints it. textureRegion shows [u0, v0, u1, v1] of the image (0..1, v from the top) for sprite sheets and card atlases.

Pass mesh again when set changes texture. Each mesh–texture pair takes a mesh id, so reuse a few pairs rather than making one per entity.

Show tiles from a 4 × 2 atlas

React and React Native · shared engine API

Show tiles from a 4 × 2 atlas
import type { Model, Texture, World } from "@nayan-ui/engine";
const COLUMNS = 4, ROWS = 2;
const tile = (i: number) => {
const x = i % COLUMNS, y = Math.floor(i / COLUMNS);
return [x / COLUMNS, y / ROWS, (x + 1) / COLUMNS, (y + 1) / ROWS] as const;
};
export function addCard(world: World, card: Model, atlas: Texture) {
const entity = world.spawn({ mesh: card, texture: atlas, textureRegion: tile(0), color: "white" });
return {
show: (index: number) => world.set(entity, { textureRegion: tile(index) }),
swap: (other: Texture) => world.set(entity, { mesh: card, texture: other }), // texture changes need mesh
};
}