Loading Custom Characters
OpenHuman uses the
.ohb(OpenHuman Bundle) format - a self-contained ZIP archive that packages your glTF mesh, KTX2 textures, skeleton, morph targets, and animation clips into a single file optimised for fast streaming and GPU upload.
This guide covers how to prepare your source assets, build an .ohb bundle using the CLI, and load it into the engine at runtime.
Overview
The ohb-builder CLI (included in @openhuman/tools) validates your inputs, compresses geometry with Draco, transcodes textures to KTX2/Basis Universal, bakes morph target deltas into a GPU texture atlas, and outputs a single .ohb bundle ready for deployment.
Source Asset Requirements
Mesh (glTF 2.0 / .glb)
| Requirement | Spec |
|---|---|
| Format | glTF 2.0 (.glb binary preferred) |
| Triangles (LOD 0) | 50 000 – 150 000 tris |
| Triangles (LOD 1) | ~80 000 tris |
| Triangles (LOD 2 / mobile) | ~30 000 tris |
| UV sets | UV0 (albedo/normal), UV1 (optional lightmap) |
| Vertex influences | Max 4 joints per vertex |
| Joint count | Max 256 joints |
| Coordinate system | Y-up, Z-forward (glTF default) |
Meshes with more than 4 bone influences per vertex will have excess weights stripped and renormalised automatically. For best results, enforce the limit in your DCC tool (Blender, Maya, etc.) before export.
Textures
All textures should be supplied as lossless PNG or 16-bit EXR. The builder handles compression.
| Map | Channel packing | Recommended resolution |
|---|---|---|
albedo | RGB (sRGB) | 4096 × 4096 |
normal | RGB (tangent-space, OpenGL) | 4096 × 4096 |
roughness | R (linear) | 2048 × 2048 |
sss_mask | R (SSS strength, linear) | 2048 × 2048 |
thickness | R (backscatter thickness, linear) | 2048 × 2048 |
hair_flow | RG (anisotropy direction, linear) | 2048 × 2048 |
eye_iris | RGB (sRGB, high detail) | 1024 × 1024 |
Texture memory budget per character is 256 MB on GPU. At 4K the albedo alone is ~48 MB (BC7 compressed). Reduce non-critical maps to 2048 if you are close to the budget.
Skeleton & Rig
Export the skeleton as part of your .glb or as a standalone skeleton.json. The rig must follow a humanoid joint naming convention compatible with OpenHuman's internal bone map:
{
"hips": "Hips",
"spine": "Spine",
"chest": "Chest",
"neck": "Neck",
"head": "Head",
"leftArm": "LeftArm",
"rightArm": "RightArm",
"leftHand": "LeftHand",
"rightHand": "RightHand",
"leftLeg": "LeftLeg",
"rightLeg": "RightLeg",
"leftFoot": "LeftFoot",
"rightFoot": "RightFoot"
}The builder accepts both Mixamo-style and Unreal MetaHuman-style bone names and remaps them automatically. Pass
--rig-preset mixamoor--rig-preset metahumanto the CLI if your rig uses those conventions.
Morph Targets (Facial Blendshapes)
For full facial animation support, your mesh must include the following 52 FACS blendshapes as glTF morph targets:
browDownLeft browDownRight browInnerUp
browOuterUpLeft browOuterUpRight
cheekPuff cheekSquintLeft cheekSquintRight
eyeBlinkLeft eyeBlinkRight
eyeLookDownLeft eyeLookDownRight eyeLookInLeft eyeLookInRight
eyeLookOutLeft eyeLookOutRight eyeLookUpLeft eyeLookUpRight
eyeSquintLeft eyeSquintRight eyeWideLeft eyeWideRight
jawForward jawLeft jawOpen jawRight
mouthClose mouthDimpleLeft mouthDimpleRight
mouthFrownLeft mouthFrownRight
mouthFunnel mouthLeft mouthLowerDownLeft mouthLowerDownRight
mouthPressLeft mouthPressRight mouthPucker mouthRight
mouthRollLower mouthRollUpper mouthShrugLower mouthShrugUpper
mouthSmileLeft mouthSmileRight
mouthStretchLeft mouthStretchRight
mouthUpperUpLeft mouthUpperUpRight
noseSneerLeft noseSneerRight
tongueOut
Meshes without morph targets can still be loaded - facial animation will be disabled and a warning emitted at build time. Partial FACS sets are supported; missing targets are skipped silently at runtime.
Animation Clips
Supply animation clips as separate .glb files, each containing one animation track. Supported interpolation modes: LINEAR, STEP, CUBICSPLINE (Hermite).
animations/
├── idle.glb
├── talk_neutral.glb
├── talk_happy.glb
├── gesture_wave.glb
└── blink.glb
Building a Bundle with the CLI
manifest.json schema
{
"version": "1.0",
"name": "MyCharacter",
"features": {
"morphTargets": true,
"lod": true,
"sss": true,
"hairFlow": true
},
"lods": {
"lod0": { "file": "mesh/lod0.glb", "triangles": 150242 },
"lod1": { "file": "mesh/lod1.glb", "triangles": 79810 },
"lod2": { "file": "mesh/lod2.glb", "triangles": 29994 }
},
"morphTargets": {
"file": "mesh/morphs.bin",
"count": 52,
"vertexCount": 150242,
"format": "RGB16F",
"names": ["eyeBlinkLeft", "eyeBlinkRight", "jawOpen", "..."]
},
"animations": {
"index": "animations/index.json",
"default": "idle"
}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
JOINT_LIMIT_EXCEEDED | Mesh has > 256 joints | Merge bones or reduce rig complexity |
INFLUENCE_COUNT_EXCEEDED | Vertex has > 4 bone weights | Enable "limit weights to 4" in your DCC tool |
MORPH_VERTEX_MISMATCH | Morph target vertex count differs from base mesh | Re-export morph targets from the same base mesh |
TEXTURE_DIMENSION_NOT_POT | Texture width/height is not power-of-two | Resize to nearest POT (e.g. 4096, 2048, 1024) |
KTX2_ENCODE_FAILED | Source texture has unsupported colour profile | Convert to sRGB (albedo) or linear (all other maps) |
BUNDLE_SIZE_EXCEEDED | .ohb > 100 MB | Reduce texture resolution or enable --texture-quality medium |
Next Steps
- Animation Graph - configure idle → talk → gesture state machine transitions
- Facial Blendshapes - drive the 52 FACS morph targets via the SDK
- Streaming Animation - connect a WebSocket lip-sync or mocap stream
- Embed API Reference - embed characters via the
<open-human>web component