---
title: "modelImport"
description: "The modelImport namespace — the engine's Luau API reference for modelImport."
section: "API Reference"
slug: "api-modelimport"
canonical: "https://origozero.ai/docs/api-modelimport"
updated: "2026-09-05T23:13:46.789869878+00:00"
tags: ["api", "reference"]
---

# modelImport

The `modelImport` namespace — 25 functions.

## globals/modelImport/decompose {#globals-modelimport-decompose}

```lua
modelImport.decompose(bytes: buffer | string, format: string) -> string
```

Parse raw model bytes on a background thread. `format` is the real source
extension (`"fbx"`, `"obj"`, `"dae"`, `"gltf"`, `"glb"`, `"stl"`, `"ply"`,
`"3ds"`, …), forwarded to assimp as the format hint. Returns a promise
handle: `task.await` it, then read the data with `result(handle)`.

**Parameters**

- `bytes` `buffer | string` — Raw model file bytes (from `vfs.readAsync`).
- `format` `string` — The source file extension (lowercase, no dot).

**Returns** `string` — Promise handle for `task.await`.

```lua
local h = modelImport.decompose(bytes, "obj"); task.await(h)
```

## globals/modelImport/decomposeFiles {#globals-modelimport-decomposefiles}

```lua
modelImport.decomposeFiles(files: { ModelFile }, mainName: string) -> string
```

Parse a model plus its companion files on a background thread, so assimp
resolves the model's external references (a `.gltf`'s external `.bin` and
image files, an `.obj`'s `.mtl` colors/textures, MD5's `.md5anim`, …).
`files` is an array of `{ name = basename, bytes = <bytes> }` that MUST
include the model file itself; `mainName` is that file's basename. Returns a
promise handle: `task.await` it, then read the data with `result(handle)` —
the same shape `decompose` produces.

**Parameters**

- `files` `{ ModelFile }` — Array of `{ name, bytes }`: the model file plus its companions.
- `mainName` `string` — Basename of the model file to import (one of `files`' names).

**Returns** `string` — Promise handle for `task.await`.

```lua
local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h)
```

## globals/modelImport/extractAnimation {#globals-modelimport-extractanimation}

```lua
modelImport.extractAnimation(sourcePath: string, clipName: string) -> string
```

Read a model source file and extract one animation clip to its `.zanim`
payload, stashed for retrieval. The source extension decides the parser, so
this is format-agnostic. Returns a promise handle: `task.await` it, then
`extractAnimationResult(handle)` returns the bytes.

**Parameters**

- `sourcePath` `string` — VFS path to the source model file.
- `clipName` `string` — Clip name as returned by `result(handle).animations[i].name`.

**Returns** `string` — Promise handle for `task.await`.

## globals/modelImport/extractAnimationResult {#globals-modelimport-extractanimationresult}

```lua
modelImport.extractAnimationResult(handle: string) -> string?
```

After awaiting an `extractAnimation` handle, return the extracted
`.zanim` bytes (binary-safe), consuming them. The bytes to hand to
`asset.create("animation", name, { bytes })`. Returns nil on failure or if
already taken.

**Parameters**

- `handle` `string` — Promise handle from `extractAnimation`.

**Returns** `string?` — The clip's `.zanim` payload bytes, or nil.

## globals/modelImport/result {#globals-modelimport-result}

```lua
modelImport.result(handle: string) -> any?
```

Read the decomposed model after `decompose`'s handle has been awaited.
Every format decomposes into the same shape, so this is format-agnostic.
Runs on the main thread; consumes the stored result.

**Parameters**

- `handle` `string` — Promise handle from `decompose`.

**Returns** `any?` — `{ nodes, meshes, materials, textures, animations, hasSkeleton, skeletonRootNode?, skeleton? }`, or nil.

## globals/modelImport/retryHandle {#globals-modelimport-retryhandle}

```lua
modelImport.retryHandle(makeHandle: () -> any, retries: number?, yield: (() -> ())?) -> string?
```

Call `makeHandle` — which returns a promise-handle string, or a falsy
value on a transient failure (e.g. a source read that raced a pending
write during a parallel import) — up to `retries + 1` times, yielding via
`yield` between attempts so a pending write can land before the next try.
Returns the handle string once one is produced, or nil when every attempt
failed. Callers `task.await` the result only when it is non-nil, so a
transient miss never reaches `task.await` as a non-string.

**Parameters**

- `makeHandle` `() -> any` — Returns a promise-handle string, or a falsy value on failure.
- `retries` `number` _(optional)_ — Extra attempts after the first (default 3).
- `yield` `(() -> ())` _(optional)_ — Called between attempts (default `task.wait`).

**Returns** `string?` — The handle string, or nil when every attempt failed.

## globals/modelImport/rigFromMeshSkin {#globals-modelimport-rigfrommeshskin}

```lua
modelImport.rigFromMeshSkin(meshBytes: buffer | string) -> string?
```

Lift the skeleton out of a skinned `.mesh` (ZMSH) payload and return it
as a `.rig` JSON document: bones (hierarchy, rest pose, inverse-bind), the
auto-derived humanoid profile, and the humanoid classification. The source
rig a skinned mesh's clips retarget through. Returns nil when the bytes are
not a mesh or carry no skin.

**Parameters**

- `meshBytes` `buffer | string` — Raw ZMSH mesh bytes carrying a skin.

**Returns** `string?` — `.rig` JSON document, or nil.

## globals/modelImport/rigFromSkeleton {#globals-modelimport-rigfromskeleton}

```lua
modelImport.rigFromSkeleton(skeleton: AnimationSkeleton) -> string?
```

Build a `.rig` JSON document from the skeleton an animation-only file was
authored on — `result(handle).skeleton`, the bones its clips drive with
their local rest transforms. Forward kinematics over the locals resolves
globals + inverse-bind; the humanoid profile + classification are derived as
for `rigFromMeshSkin`. The source rig a standalone clip retargets through.
Returns nil on malformed input.

**Parameters**

- `skeleton` `AnimationSkeleton` — `{ names, parents, locals }` (a `decompose` result's `skeleton`).

**Returns** `string?` — `.rig` JSON document, or nil.

```lua
local rigJson = modelImport.rigFromSkeleton(data.skeleton)
```

## modules/modelImport/README {#modules-modelimport-readme}

```lua
require("@builtin/modules/api/engine/modelImport") -- modelImport (also available as global 'modelImport')
```

The model-import pipeline: decompose any assimp-supported 3D model (fbx, obj, dae, gltf, glb, stl, ply, 3ds, …) into meshes, materials, textures, animation clips and a node graph; extract a single clip to its `.zanim` payload; and derive a `.rig` from a skinned mesh or from an animation-only file's driven skeleton. Public Luau surface over the `__model` and `__rig` Internal FFI namespaces.

Usage: local modelImport = require("@builtin/modules/api/engine/modelImport")
Also available as global: modelImport

## modules/modelImport/decompose {#modules-modelimport-decompose}

```lua
decompose(bytes: buffer | string, format: string): string
```

Parse raw model bytes on a background thread. `format` is the real source
extension (`"fbx"`, `"obj"`, `"dae"`, `"gltf"`, `"glb"`, `"stl"`, `"ply"`,
`"3ds"`, …), forwarded to assimp as the format hint. Returns a promise
handle: `task.await` it, then read the data with `result(handle)`.

**Parameters**

- `bytes` `buffer | string` — Raw model file bytes (from `vfs.readAsync`).
- `format` `string` — The source file extension (lowercase, no dot).

```lua
local h = modelImport.decompose(bytes, "obj"); task.await(h)
```

## modules/modelImport/decomposeFiles {#modules-modelimport-decomposefiles}

```lua
decomposeFiles(files: { ModelFile }, mainName: string): string
```

Parse a model plus its companion files on a background thread, so assimp
resolves the model's external references (a `.gltf`'s external `.bin` and
image files, an `.obj`'s `.mtl` colors/textures, MD5's `.md5anim`, …).
`files` is an array of `{ name = basename, bytes = <bytes> }` that MUST
include the model file itself; `mainName` is that file's basename. Returns a
promise handle: `task.await` it, then read the data with `result(handle)` —
the same shape `decompose` produces.

**Parameters**

- `files` `{ ModelFile }` — Array of `{ name, bytes }`: the model file plus its companions.
- `mainName` `string` — Basename of the model file to import (one of `files`' names).

```lua
local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h)
```

## modules/modelImport/extractAnimation {#modules-modelimport-extractanimation}

```lua
extractAnimation(sourcePath: string, clipName: string): string
```

Read a model source file and extract one animation clip to its `.zanim`
payload, stashed for retrieval. The source extension decides the parser, so
this is format-agnostic. Returns a promise handle: `task.await` it, then
`extractAnimationResult(handle)` returns the bytes.

**Parameters**

- `sourcePath` `string` — VFS path to the source model file.
- `clipName` `string` — Clip name as returned by `result(handle).animations[i].name`.

## modules/modelImport/extractAnimationResult {#modules-modelimport-extractanimationresult}

```lua
extractAnimationResult(handle: string): string?
```

After awaiting an `extractAnimation` handle, return the extracted
`.zanim` bytes (binary-safe), consuming them. The bytes to hand to
`asset.create("animation", name, { bytes })`. Returns nil on failure or if
already taken.

**Parameters**

- `handle` `string` — Promise handle from `extractAnimation`.

## modules/modelImport/result {#modules-modelimport-result}

```lua
result(handle: string): any?
```

Read the decomposed model after `decompose`'s handle has been awaited.
Every format decomposes into the same shape, so this is format-agnostic.
Runs on the main thread; consumes the stored result.

**Parameters**

- `handle` `string` — Promise handle from `decompose`.

## modules/modelImport/retryHandle {#modules-modelimport-retryhandle}

```lua
retryHandle(makeHandle: () -> any, retries: number?, yield: (() -> ())?): string?
```

Call `makeHandle` — which returns a promise-handle string, or a falsy
value on a transient failure (e.g. a source read that raced a pending
write during a parallel import) — up to `retries + 1` times, yielding via
`yield` between attempts so a pending write can land before the next try.
Returns the handle string once one is produced, or nil when every attempt
failed. Callers `task.await` the result only when it is non-nil, so a
transient miss never reaches `task.await` as a non-string.

**Parameters**

- `makeHandle` `() -> any` — Returns a promise-handle string, or a falsy value on failure.
- `retries` `number?` _(optional)_ — Extra attempts after the first (default 3).
- `yield` `(() -> ())?` _(optional)_ — Called between attempts (default `task.wait`).

## modules/modelImport/rigFromMeshSkin {#modules-modelimport-rigfrommeshskin}

```lua
rigFromMeshSkin(meshBytes: buffer | string): string?
```

Lift the skeleton out of a skinned `.mesh` (ZMSH) payload and return it
as a `.rig` JSON document: bones (hierarchy, rest pose, inverse-bind), the
auto-derived humanoid profile, and the humanoid classification. The source
rig a skinned mesh's clips retarget through. Returns nil when the bytes are
not a mesh or carry no skin.

**Parameters**

- `meshBytes` `buffer | string` — Raw ZMSH mesh bytes carrying a skin.

## modules/modelImport/rigFromSkeleton {#modules-modelimport-rigfromskeleton}

```lua
rigFromSkeleton(skeleton: AnimationSkeleton): string?
```

Build a `.rig` JSON document from the skeleton an animation-only file was
authored on — `result(handle).skeleton`, the bones its clips drive with
their local rest transforms. Forward kinematics over the locals resolves
globals + inverse-bind; the humanoid profile + classification are derived as
for `rigFromMeshSkin`. The source rig a standalone clip retargets through.
Returns nil on malformed input.

**Parameters**

- `skeleton` `AnimationSkeleton` — `{ names, parents, locals }` (a `decompose` result's `skeleton`).

```lua
local rigJson = modelImport.rigFromSkeleton(data.skeleton)
```

## typed/builtin//modules/api/engine/modelImport/modelImport/decompose {#typed-builtin-modules-api-engine-modelimport-modelimport-decompose}

```lua
modelImport.decompose(bytes: buffer | string, format: string) -> string
```

Parse raw model bytes on a background thread. `format` is the real source
extension (`"fbx"`, `"obj"`, `"dae"`, `"gltf"`, `"glb"`, `"stl"`, `"ply"`,
`"3ds"`, …), forwarded to assimp as the format hint. Returns a promise
handle: `task.await` it, then read the data with `result(handle)`.

**Parameters**

- `bytes` `buffer | string` — Raw model file bytes (from `vfs.readAsync`).
- `format` `string` — The source file extension (lowercase, no dot).

**Returns** `string` — Promise handle for `task.await`.

```lua
local h = modelImport.decompose(bytes, "obj"); task.await(h)
```

## typed/builtin//modules/api/engine/modelImport/modelImport/decomposeFiles {#typed-builtin-modules-api-engine-modelimport-modelimport-decomposefiles}

```lua
modelImport.decomposeFiles(files: { ModelFile }, mainName: string) -> string
```

Parse a model plus its companion files on a background thread, so assimp
resolves the model's external references (a `.gltf`'s external `.bin` and
image files, an `.obj`'s `.mtl` colors/textures, MD5's `.md5anim`, …).
`files` is an array of `{ name = basename, bytes = <bytes> }` that MUST
include the model file itself; `mainName` is that file's basename. Returns a
promise handle: `task.await` it, then read the data with `result(handle)` —
the same shape `decompose` produces.

**Parameters**

- `files` `{ ModelFile }` — Array of `{ name, bytes }`: the model file plus its companions.
- `mainName` `string` — Basename of the model file to import (one of `files`' names).

**Returns** `string` — Promise handle for `task.await`.

```lua
local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h)
```

## typed/builtin//modules/api/engine/modelImport/modelImport/extractAnimation {#typed-builtin-modules-api-engine-modelimport-modelimport-extractanimation}

```lua
modelImport.extractAnimation(sourcePath: string, clipName: string) -> string
```

Read a model source file and extract one animation clip to its `.zanim`
payload, stashed for retrieval. The source extension decides the parser, so
this is format-agnostic. Returns a promise handle: `task.await` it, then
`extractAnimationResult(handle)` returns the bytes.

**Parameters**

- `sourcePath` `string` — VFS path to the source model file.
- `clipName` `string` — Clip name as returned by `result(handle).animations[i].name`.

**Returns** `string` — Promise handle for `task.await`.

## typed/builtin//modules/api/engine/modelImport/modelImport/extractAnimationResult {#typed-builtin-modules-api-engine-modelimport-modelimport-extractanimationresult}

```lua
modelImport.extractAnimationResult(handle: string) -> string?
```

After awaiting an `extractAnimation` handle, return the extracted
`.zanim` bytes (binary-safe), consuming them. The bytes to hand to
`asset.create("animation", name, { bytes })`. Returns nil on failure or if
already taken.

**Parameters**

- `handle` `string` — Promise handle from `extractAnimation`.

**Returns** `string?` — The clip's `.zanim` payload bytes, or nil.

## typed/builtin//modules/api/engine/modelImport/modelImport/result {#typed-builtin-modules-api-engine-modelimport-modelimport-result}

```lua
modelImport.result(handle: string) -> any?
```

Read the decomposed model after `decompose`'s handle has been awaited.
Every format decomposes into the same shape, so this is format-agnostic.
Runs on the main thread; consumes the stored result.

**Parameters**

- `handle` `string` — Promise handle from `decompose`.

**Returns** `any?` — `{ nodes, meshes, materials, textures, animations, hasSkeleton, skeletonRootNode?, skeleton? }`, or nil.

## typed/builtin//modules/api/engine/modelImport/modelImport/retryHandle {#typed-builtin-modules-api-engine-modelimport-modelimport-retryhandle}

```lua
modelImport.retryHandle(makeHandle: () -> any, retries: number?, yield: (() -> ())?) -> string?
```

Call `makeHandle` — which returns a promise-handle string, or a falsy
value on a transient failure (e.g. a source read that raced a pending
write during a parallel import) — up to `retries + 1` times, yielding via
`yield` between attempts so a pending write can land before the next try.
Returns the handle string once one is produced, or nil when every attempt
failed. Callers `task.await` the result only when it is non-nil, so a
transient miss never reaches `task.await` as a non-string.

**Parameters**

- `makeHandle` `() -> any` — Returns a promise-handle string, or a falsy value on failure.
- `retries` `number` _(optional)_ — Extra attempts after the first (default 3).
- `yield` `(() -> ())` _(optional)_ — Called between attempts (default `task.wait`).

**Returns** `string?` — The handle string, or nil when every attempt failed.

## typed/builtin//modules/api/engine/modelImport/modelImport/rigFromMeshSkin {#typed-builtin-modules-api-engine-modelimport-modelimport-rigfrommeshskin}

```lua
modelImport.rigFromMeshSkin(meshBytes: buffer | string) -> string?
```

Lift the skeleton out of a skinned `.mesh` (ZMSH) payload and return it
as a `.rig` JSON document: bones (hierarchy, rest pose, inverse-bind), the
auto-derived humanoid profile, and the humanoid classification. The source
rig a skinned mesh's clips retarget through. Returns nil when the bytes are
not a mesh or carry no skin.

**Parameters**

- `meshBytes` `buffer | string` — Raw ZMSH mesh bytes carrying a skin.

**Returns** `string?` — `.rig` JSON document, or nil.

## typed/builtin//modules/api/engine/modelImport/modelImport/rigFromSkeleton {#typed-builtin-modules-api-engine-modelimport-modelimport-rigfromskeleton}

```lua
modelImport.rigFromSkeleton(skeleton: AnimationSkeleton) -> string?
```

Build a `.rig` JSON document from the skeleton an animation-only file was
authored on — `result(handle).skeleton`, the bones its clips drive with
their local rest transforms. Forward kinematics over the locals resolves
globals + inverse-bind; the humanoid profile + classification are derived as
for `rigFromMeshSkin`. The source rig a standalone clip retargets through.
Returns nil on malformed input.

**Parameters**

- `skeleton` `AnimationSkeleton` — `{ names, parents, locals }` (a `decompose` result's `skeleton`).

**Returns** `string?` — `.rig` JSON document, or nil.

```lua
local rigJson = modelImport.rigFromSkeleton(data.skeleton)
```
