Skip to content

3D Loading and Supported Assets

Olivier Biot edited this page Jun 19, 2026 · 3 revisions

Part of Working in 3D.

3D geometry loads through the standard asset pipeline (me.loader), the same as images, audio, or Tiled maps. Two formats are supported:

Format Asset type Best for
Wavefront OBJ/MTL "obj" + "mtl" a single static model (prop, character billboard)
glTF 2.0 / GLB "gltf" / "glb" a whole authored scene — node hierarchy, multiple meshes, a camera (see Loading glTF / GLB scenes)

WebGL required. 3D meshes render only under the WebGL renderer (renderer: video.WEBGL). The Canvas2D renderer has no mesh path.

Loading an OBJ model

me.loader.preload([
  { name: "fox",    type: "obj",   src: "models/fox.obj" },
  { name: "fox",    type: "mtl",   src: "models/fox.mtl" },
  { name: "foxtex", type: "image", src: "models/fox.png" },
], () => {
  const mesh = new me.Mesh(400, 300, {
    model: "fox", material: "fox", texture: "foxtex",
    width: 200, height: 200,
  });
  me.game.world.addChild(mesh);
});

me.loader.getOBJ(name) / getMTL(name) return the raw parsed data if you need it directly. Multi-material OBJ files (multiple usemtl groups) are supported — each group's diffuse color is baked into a per-vertex color buffer, so multiple materials don't cost extra draw calls.

For importing a full scene (many meshes + hierarchy + camera) rather than a single model, see Loading glTF / GLB scenes.

Current support & limitations

The following applies to all 3D meshes regardless of source format.

Geometry

Supported Not yet
Positions, UVs (TEXCOORD_0), indexed triangles Multiple UV sets, tangents
Normals (NORMAL, or synthesized when absent) Sparse accessors, Draco compression
Node hierarchy + per-node TRS (glTF) → Matrix3d
Per-vertex colors (COLOR_0, float / normalized)
Uint32 index buffers (meshes > 65 535 vertices)
Multi-material submeshes (OBJ usemtl groups)

Materials & textures

Supported Not yet
One diffuse / baseColor texture per material Metallic-roughness, normal, emissive, occlusion (AO) maps
Diffuse color (Kd / baseColorFactor) as tint Full PBR shading
Opacity (d / baseColorFactor.a) Alpha cutoff / per-material blend modes
Per-vertex tint multiply (WebGL) Two-color ("dark") tint on Canvas (WebGL-only)
Texture wrap from the sampler (wrapS / wrapT, default REPEAT) Mirrored repeat
External textures & buffers (image / .bin files referenced by uri)

Animation

glTF node animation is supported: keyframed translation / rotation / scale, sampled and re-posed every frame over the node hierarchy (a parent transform carries its children). This drives rigid rigs — walking characters made of separate body parts, spinning pickups, opening doors, moving platforms. It plays through the same API as a 2D Sprite (see below).

Supported Not yet
glTF node animation (keyframed TRS channels, LERP / SLERP / STEP) Skeletal animation / skinning (vertex-deforming bones)
Hierarchical playback (an animated parent moves its children) Morph targets (blend shapes)
CUBICSPLINE channels (keyframe values; tangents approximated)
Transform animation you drive yourself (mesh.rotate() / translate(), tweens)

Rigid (modular) characters like Kenney's Blocky Characters animate this way — each limb is a separate mesh node rotated about its joint, no skinning. Rigged/skinned characters (a single mesh deformed by an armature) import at rest pose for now. For 2D skeletal animation, use the Spine plugin.

Playing an animation

When a glTF asset defines animation channels, it loads as a single GLTFModel — a container that keeps the node hierarchy intact and drives playback. Retrieve it from the world by the asset name, then use the Sprite-style API:

me.loader.preload(
  [{ name: "hero", type: "glb", src: "models/hero.glb" }],
  () => {
    me.level.load("hero", { scale: 200 });

    // the animated asset loads as one GLTFModel, named after the asset
    const hero = me.game.world.getChildByName("hero")[0];

    hero.getAnimationNames();          // ["idle", "walk", "sprint", ...]
    hero.setCurrentAnimation("walk");  // loop forever
    hero.play("walk");                 // ...or the shorthand
  },
);

The second argument matches Sprite.setCurrentAnimation — an options object, a clip name to chain to, or a completion callback:

hero.setCurrentAnimation("die", { loop: false });          // play once, hold the last pose
hero.setCurrentAnimation("jump", { next: "idle" });        // jump, then return to idle
hero.setCurrentAnimation("walk", { speed: 2 });            // twice as fast
hero.setCurrentAnimation("pickup", { onComplete: grab });  // callback each cycle

hero.animationspeed = 0.5;  // playback multiplier (1 = authored speed)
hero.pause();               // freeze on the current pose
hero.play();                // resume
hero.stop();                // stop and reset to the bind pose

The same API works on a 2D Sprite, so 2D and 3D animation read identically.

Lighting & shadows

Directional lighting is supported for 3D meshes via Light3d / LightingEnvironment (half-Lambert diffuse + an ambient floor); a glTF scene's authored KHR_lights_punctual directional lights are loaded automatically. See Lighting and Working in 3D → Meshes. With no lights in the environment, meshes render unlit (texture × tint), so existing scenes are unaffected.

Not yet: point/spot light shading (parsed, not shaded), shadows, normal/PBR maps. Light2d remains 2D-only.

Clone this wiki locally