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)

RequirementSpec
FormatglTF 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 setsUV0 (albedo/normal), UV1 (optional lightmap)
Vertex influencesMax 4 joints per vertex
Joint countMax 256 joints
Coordinate systemY-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.

MapChannel packingRecommended resolution
albedoRGB (sRGB)4096 × 4096
normalRGB (tangent-space, OpenGL)4096 × 4096
roughnessR (linear)2048 × 2048
sss_maskR (SSS strength, linear)2048 × 2048
thicknessR (backscatter thickness, linear)2048 × 2048
hair_flowRG (anisotropy direction, linear)2048 × 2048
eye_irisRGB (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:

skeleton-naming.json
{
    "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 mixamo or --rig-preset metahuman to 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

manifest.json
{
    "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

ErrorCauseFix
JOINT_LIMIT_EXCEEDEDMesh has > 256 jointsMerge bones or reduce rig complexity
INFLUENCE_COUNT_EXCEEDEDVertex has > 4 bone weightsEnable "limit weights to 4" in your DCC tool
MORPH_VERTEX_MISMATCHMorph target vertex count differs from base meshRe-export morph targets from the same base mesh
TEXTURE_DIMENSION_NOT_POTTexture width/height is not power-of-twoResize to nearest POT (e.g. 4096, 2048, 1024)
KTX2_ENCODE_FAILEDSource texture has unsupported colour profileConvert to sRGB (albedo) or linear (all other maps)
BUNDLE_SIZE_EXCEEDED.ohb > 100 MBReduce texture resolution or enable --texture-quality medium

Next Steps