Make your own MetaHuman

A MetaHuman is a photoreal rigged character — and metahuman-to-glb is an open-source (MIT) pipeline that exports one from Unreal Engine 5.7 as a single web-ready .glb with all 51 ARKit face blendshapes. This tutorial loads one of its sample characters, Bruce, straight from the project's GitHub Pages URL, then makes him yours: no hair, a new skin tone and outfit, a smile posed from the blendshapes, selector-friendly classes on every part, and a still image from the Render tab. Open the editor, open the Shell tab, and paste each block in order.

A bald man with a slight smile in a glossy blue jacket, shown from the chest up against a grey background.
The result of this tutorial, captured with the Render tab's Take Snapshot — Bruce, bald, in a blue leather jacket, smiling.
Where the character comes from. creategamecharacters/metahuman-to-glb is a five-stage pipeline (Unreal assemble → Sequencer ARKit bake → Blender shape keys → Draco GLB → three.js viewer). Its live demo hosts the finished characters, so you can import one with a plain fetch — no download, no account. To bring in your own MetaHuman, run that pipeline on it and fetch (or drag in) the resulting GLB exactly the same way. The sample files are about 80 MB, so the first load takes a moment.

1 Import Bruce straight from the URL

The host sends Access-Control-Allow-Origin: *, so the browser can fetch the GLB directly. Wrapping the bytes in a File and handing it to the editor's own loader is the same path as File → Import or dragging a file onto the page — Draco and every other glTF extension are already handled:

const url = 'https://creategamecharacters.github.io/metahuman-to-glb/5.7/characters/bruce/bruce.glb';
const blob = await (await fetch(url)).blob();
editor.loader.loadFiles([new File([blob], 'bruce.glb', { type: 'model/gltf-binary' })]);

A progress banner shows reading, then parsing, and then the Import glTF dialog appears. Leave the defaults and click OK — geometry compression is for later, and see the note in step 7 before you turn it on for a rigged character.

Use fetch, not fetchAPI, for binary files. fetchAPI returns parsed JSON or text, which is right for APIs and wrong for a 80 MB model. Plain fetch(...).blob() keeps the bytes intact.

2 See what you imported

A MetaHuman GLB is a skinned body plus a pile of separate meshes. List them before touching anything — the names are how you'll address each part:

const rig = [];
scene.traverse(o => { if (o.isMesh) rig.push(o.name + (o.isSkinnedMesh ? '  (skinned)' : '  (hair card)')); });
rig;

You'll see the skinned SKM_Bruce_FaceMesh_1…8 (skin, teeth, eyes, lashes), SKM_Bruce_BodyMesh001, the outfit pieces Bruce_Outfits_1/2/3 (jacket, pants, shoes), and flat card meshes for Hair_, Beard_, Mustache_ and Eyebrows_. The face also carries the 51 ARKit blendshapes:

const face = scene.getObjectByName('SKM_Bruce_FaceMesh_1');
Object.keys(face.morphTargetDictionary).length;   // 51
Object.keys(face.morphTargetDictionary).slice(0, 5);
// ['eyeBlinkLeft', 'eyeLookDownLeft', 'eyeLookInLeft', 'eyeLookOutLeft', 'eyeLookUpLeft']

3 Take the hair off

Hair, beard and moustache are separate card meshes, so removing them is just hiding three objects. The eyebrows are also cards, and stay:

['Hair_S_Casual_CardsMesh_Group0_LOD0',
 'Beard_M_MuttonChops_CardMesh_Group0_LOD0',
 'Mustache_S_Horseshoe_CardMesh_Group0_LOD0'
].forEach(n => { scene.getObjectByName(n).visible = false; });
Not every character has the same part names. Ada, for example, has Eyebrows_M_Thin… and no beard, and her outfit meshes are named differently. If you'd rather not type names, the repo's presets/rigParts.js classifies the parts by mesh and material name: const p = findParts(scene) gives you p.hair, p.face, p.shirt, p.shoes… for any MetaHuman export.

4 Change the skin and the clothes

Skin keeps its texture and is only tinted (the colour multiplies the map), so pores and detail survive. Clothes drop their texture for a flat colour. Both go through a small helper that edits a clone of the material and swaps it in — changing a material that has already been drawn can leave the WebGPU renderer with a stale uniform buffer, and a fresh material avoids that:

const restyle = (mesh, fn) => {
  const edit = old => { const m = old.clone(); fn(m); old.dispose(); return m; };
  mesh.material = Array.isArray(mesh.material) ? mesh.material.map(edit) : edit(mesh.material);
};

// Skin: tint only, keep the texture
for (const n of ['SKM_Bruce_FaceMesh_1', 'SKM_Bruce_BodyMesh001']) {
  restyle(scene.getObjectByName(n), m => m.color.set(0xb98a6c));
}

// Clothes: flat colour, no texture
restyle(scene.getObjectByName('Bruce_Outfits_1'), m => { m.map = null; m.color.set(0x1b3a6b); m.roughness = 0.6; });  // jacket
restyle(scene.getObjectByName('Bruce_Outfits_2'), m => { m.map = null; m.color.set(0x202428); m.roughness = 0.7; });  // pants
restyle(scene.getObjectByName('Bruce_Outfits_3'), m => { m.map = null; m.color.set(0xf2f2f2); m.roughness = 0.5; });  // shoes

Change the hex values to change the person. Skin is the one to tune by eye: lighter tints read warmer under the default environment than they look as swatches.

5 Pose the face with the ARKit blendshapes

Each of the 51 shapes is a morph target with an influence from 0 to 1. The same shape names exist on the face, the eyebrow cards, and (here) the beard and moustache cards, so one helper sets a shape everywhere it exists:

const express = (shape, value) => scene.traverse(o => {
  const i = o.morphTargetDictionary?.[shape];
  if (i !== undefined) o.morphTargetInfluences[i] = value;
});

express('mouthSmileLeft', 0.8);
express('mouthSmileRight', 0.8);
express('cheekSquintLeft', 0.4);
express('cheekSquintRight', 0.4);
express('browInnerUp', 0.3);

Other useful ones: jawOpen, eyeBlinkLeft/eyeBlinkRight, mouthPucker, browDownLeft, eyeLookUpLeft. Because they're ordinary morph target influences, the Universal Timeline can keyframe them like any other number — a blink or a talking mouth is just a short clip.

6 Tag the parts with classes

The auto-derived classes on an imported rig are mostly material-name noise (.mifaceskinbakedlod0skmbrucefacemesh). Add a few meaningful ones with tagClass — it's a command, so it's undoable and travels with the scene — and the parts become addressable by selector:

tagClass(scene.getObjectByName('SKM_Bruce_FaceMesh_1'), 'head');
tagClass(scene.getObjectByName('SKM_Bruce_FaceMesh_1'), 'skin');
tagClass(scene.getObjectByName('SKM_Bruce_BodyMesh001'), 'skin');
tagClass(scene.getObjectByName('Bruce_Outfits_1'), 'jacket');
tagClass(scene.getObjectByName('Bruce_Outfits_2'), 'trousers');
tagClass(scene.getObjectByName('Bruce_Outfits_3'), 'shoes');

$S('.skin').count();   // 2 — head and body, both retinted together by one selector

7 Frame the shot and take a snapshot

Move the viewport camera to a head-and-shoulders view and project nothing by guesswork — set the position and look-at target, and tell the orbit controls the same target so the next mouse drag pivots around his face:

const cam = editor.camera;
cam.position.set(0.35, 1.68, 1.15);
cam.lookAt(0, 1.52, 0);
editor.controls.center.set(0, 1.52, 0);
editor.signals.cameraChanged.dispatch(cam);

Now open the Render tab. The Snapshot section at the top renders the scene once, offscreen, with no grid, gizmos or skeleton lines: pick Current view, a size (1280 × 720 for the picture above), PNG, and press Take Snapshot. It downloads the file and leaves a preview with Download again and Copy to clipboard buttons under it. Tick Transparent for a cut-out with no background.

Don't compress a rigged character on import. The Import glTF dialog's geometry options include Simplify mesh. Strata skips that step for skinned meshes and ones with morph targets (it would otherwise strip the skin weights and blendshapes), so on a MetaHuman it does nothing useful — and quantizing normals only helps non-skinned meshes. Leave compression off here.

8 The same edits from the Customize tab, or as a spec

Steps 3–6 are a customizer: they change how an existing rigged character looks and what its face is doing, and nothing else. The Customize tab runs exactly those edits from a panel (skin tint, hair and beard toggles, clothing recolor, expression sliders, a bone-attached prop), and the same edits can be written as a list of ops that replays deterministically — on this Bruce, on Ada, or on any other rigged GLB:

{ "customize": [ { "ops": [
  { "op": "hide",       "target": "part:hair",    "optional": true },
  { "op": "hide",       "target": "part:beard",   "optional": true },
  { "op": "tint",       "target": "part:skin",    "color": "#b98a6c" },
  { "op": "recolor",    "target": "part:shirt",   "color": "#1b3a6b", "roughness": 0.6 },
  { "op": "recolor",    "target": "part:bottom",  "color": "#202428", "roughness": 0.7 },
  { "op": "recolor",    "target": "part:shoes",   "color": "#f2f2f2", "roughness": 0.5 },
  { "op": "blendshape", "shapes": { "mouthSmileLeft": 0.8, "mouthSmileRight": 0.8, "cheekSquintLeft": 0.4, "cheekSquintRight": 0.4, "browInnerUp": 0.3 } },
  { "op": "tag",        "target": "part:skin",    "classes": ["skin"] }
] } ] }

Paste it into the Customize tab's Spec box and press Apply spec, or hand it to runCustomizeStage(scene, spec) from customize/customizeOps.js. "optional": true skips an op when the character has no such part (Ada has no beard), so one spec serves both.

Why it can only do these things. Every op here leaves the vertex set, the skin weights and the skeleton alone, which is what keeps the body skeleton and the 51 face morph targets working: the customized character still bakes and animates exactly as before (the tab shows a fingerprint of the rig and refuses to finish a run that changed it). Geometry edits, bone edits, swapping the body mesh, and adding a jacket or dress are therefore not customize ops — the spec validator rejects them. A rigid accessory such as sunglasses is allowed because it is parented to a bone; a new deformable garment must be skinned to the body first (the tab's Skin selected garment to body rig step), otherwise it z-fights and stays put when the character moves. Recoloring the clothing the character already has is the cheap, safe wardrobe.

9 Save it

Everything above is ordinary scene state — geometry, materials, classes, blendshape influences — so a normal commit captures it. Set your repository and token in the Git tab, then use Commit Scene. Expect a big commit for a MetaHuman: the face alone carries 51 full copies of its vertices per mesh, so the scene is stored as roughly a hundred megabytes of binary asset files plus a small scene.json that references them.

// Sanity-check what you're about to commit
$S('.skin').count();                                   // 2
scene.getObjectByName('Hair_S_Casual_CardsMesh_Group0_LOD0').visible;   // false
editor.history.undos.length;                           // commands issued this session

Credits and licences

People

Assets

Tools