Load glTF models and images in parallel, recover from failures and map textures.
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
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
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 };}
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
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) };}
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.
| API / option | Meaning |
|---|---|
| center | Move the bounding-box centre to the origin. Default true. |
| fit | Scale so the largest side is this long. Default: source size. |
| shine | Gloss 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. |
| LoadedModel | Model 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
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" });}
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
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};}