Skip to content

Models

The engine draws meshes, and it loads exactly one mesh format: .esmesh. A .gltf or .glb is a source, not an asset — bringing one into a project runs an import that writes the assets a scene can actually reference.

That import is the subject of this page. For the component that draws the result see the component reference; for materials in general see Materials.

Three doors, one import:

How What happens
Drag a .gltf/.glb onto the Content Browser The source is copied in (with the files it points at) and imported.
Put one in the project folder The editor notices and imports it — a git checkout, a copy in Finder, a re-export from Blender.
estella import-gltf <file> [outDir] --project <dir> The same import, on the command line. --scale <n> sizes it.

Products land beside the source, not in whatever folder the browser is showing: re-importing has to overwrite the same files.

For robot.gltf with two primitives and one image:

Product What it is
robot_0_0.esmesh, robot_1_0.esmesh One per primitive — geometry, named <stem>_<mesh>_<primitive> (just <stem> for a single one).
robot_0.png Images the file carries inline. An image already on disk is referenced, never copied.
robot_m0.esmaterial Only when the source’s material says something a Mesh2D cannot — see Materials.
robot_Walk.estimeline One per animation in the source, named <stem>_<animation> — see Animations.
robot.esprefab How the pieces go together: the node hierarchy, each Mesh2D with its mesh, texture, tint and flags.

Drag the prefab into a scene. The individual .esmesh files are assets too — a Mesh2D.mesh field takes one directly — but the prefab is the model as authored.

A glTF is authored in metres. A world unit is a design pixel. A 1.7 m character therefore arrives 1.7 units tall — a couple of pixels — unless you say otherwise.

The import does not guess a factor. It tells you when a model came in small, and the number that fixes it is Scale in the source’s Import Settings (select the .gltf in the Content Browser). Saving it re-imports the model.

Scale lands on the prefab root’s Transform, never baked into the geometry — the .esmesh stays faithful to the source, and the number stays visible and editable.

An import runs again when:

  • the source file changes (re-exported over the top of the old one);
  • Reimport is chosen on it in the Content Browser;
  • an import setting is saved.

Asset uuids survive, so scenes and prefabs that reference the products keep working.

A glTF material is split by what can carry it.

On the Mesh2D component — base colour, because that is what the engine’s mesh path already is (texture(uv) × vertexColor × tint):

glTF Component field
baseColorTexture texture
baseColorFactor color
alphaModeBLEND opaque
doubleSided: false cullBackfaces
a NORMAL attribute lit: true

In an .esmaterial — shading, which is per-material constants and samplers a component has no room for. Written only when the source uses one of them, on the built-in Model shader:

glTF Material parameter
normalTexture u_normalMap
emissiveFactor / emissiveTexture u_emissive / u_emissiveMap
occlusionTexture (+ strength) u_occlusionMap / u_occlusionStrength
alphaMode: MASK + alphaCutoff u_alphaCutoff
metallicFactor / roughnessFactor / metallicRoughnessTexture u_metallic / u_roughness / u_metallicRoughnessMap

Metal and roughness are written even when they match the engine’s own defaults: glTF defaults both factors to 1 (a fully rough metal) and the shader defaults metal to 0, so a product that left them out would describe a surface the source did not.

The material also restates the draw’s blending, depth and culling, because a material replaces them — a model told to occlude itself keeps doing so through its material.

Each animation in the source becomes an .estimeline beside the meshes, and the prefab’s root carries a TimelinePlayer pointing at the first one with playing off — what to play is the scene’s decision. Set playing, or call the timeline API, to run it.

A clip drives the nodes it was authored against, addressing each by its path of names under the prefab root. Two sibling nodes sharing a name are given distinct ones during the import so a track cannot drive the wrong one; that name is what the prefab shows.

LINEAR, STEP and CUBICSPLINE all carry across, tangents included.

A rigged model comes in rigged: the mesh carries its joint indices, weights and bind pose, and the prefab gives the entity a MeshSkin naming the joint entities in the order those matrices are in. Moving a joint — by hand, or by the animation above — deforms the mesh.

A skinned mesh’s own transform is ignored, which glTF requires: its joints are already placed in the world, so moving the mesh entity does nothing. Move the rig’s root, or the joints.

Limit Why
64 joints per mesh The pose is a uniform block, which is 4KB at that size — inside what a WebGL2 block is guaranteed. A mesh wanting more draws undeformed.
Joint count must match the bind pose A mesh whose MeshSkin names a different number of joints draws undeformed rather than with vertices resolved against the wrong matrices.

EXT_meshopt_compression and KHR_draco_mesh_compression are decoded during import, so a compressed export produces the same .esmesh as an uncompressed one. Nothing about the runtime changes: compression is a property of the source file, not of the product.

The import never drops anything in silence — whatever it cannot carry appears as a note when it runs.

Not imported Why
Morph targets, weights animation channels Blending between shapes needs the targets themselves, which are not carried.
A second UV set (TEXCOORD_1), KHR_texture_transform UVs are used as authored.
Non-triangle primitives (mode ≠ 4) The renderer draws triangle lists.
normalTexture.scale The map is applied at full strength.
  • Point lights measure distance in the XY plane (the 2D lighting model); directional lights are exact, which is what a shaded model usually wants.
  • A material that writes its own vertex stage is not redirected onto mesh geometry — it would ignore the per-object transform. Fragment-only materials work on both.
  • Geometry with no NORMAL attribute still takes light, off the constant normal a 2D surface has — flat, but not black.