Skip to content

Models

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

Both formats produce the same products and go through the same settings; where FBX differs is its own section below.

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.

Seven shapes need no import and no file. Create → Cube (or Sphere, Capsule, Cylinder, Cone, Plane, Quad) makes an entity whose MeshRenderer.mesh holds a builtin: ref, and the same seven appear at the top of the picker on any MeshRenderer.mesh field.

Ref Shape
builtin:cube 100-unit box, one flat normal per face
builtin:sphere 100-unit sphere, smooth normals
builtin:capsule 100 units tall — radius 25, 50 of cylinder between the caps, the shape CapsuleCollider3D describes
builtin:cylinder 100-unit upright cylinder, flat caps
builtin:cone 100-unit cone standing on its base
builtin:plane 100-unit square in XZ, facing up — a ground
builtin:quad 100-unit square in XY, facing the camera — a backdrop

They are built in code at load time, so they cost no bytes in a package and a scene that references one carries no dependency. All seven are the same size as a new ShapeRenderer, so one dropped into a scene is visible without being scaled first; give it the size you want through the Transform, the way an imported model gets one.

The five closed shapes are created opaque and back-face culled, which is what lets them hide each other instead of blending in submission order; plane and quad are flat, so they stay two-sided and are still there when seen from behind.

A primitive is 100 units deep as well as wide, which a camera placed for flat content is not far enough back for: leave it at the 2D default and the near half of every solid falls in front of the near plane. Put the scene camera a few hundred units away, as the 3D examples do.

Three doors, one import:

How What happens
Drag a .gltf/.glb/.fbx 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-model <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 MeshRenderer 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 MeshRenderer with its mesh, texture, tint and flags.

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

Models are authored in metres — an FBX authored in centimetres is converted on the way in. 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 source 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.

A baked lightmap is read through a second UV set, and it has to be one where no two surfaces land on the same texels. The set the art is wrapped in is not: two arms deliberately share one arm’s pixels, which is what keeps a texture small. Give them the same lightmap texels and one arm’s shadow falls on the other.

Turn on Generate Lightmap UVs in the model’s import settings and the import unwraps one: the surface is cut into nearly-flat charts, each is projected onto its own plane, and they are packed into the unit square at one world-to-texel ratio — so a wall is lit at the same resolution as the floor it meets. Vertices on a chart boundary are split, so the mesh comes back with more of them than the source had; nothing moves, and every shape, skin weight and vertex colour follows its vertex.

It is off by default because most models are never baked, and a split vertex costs memory whether or not anything reads the channel. Two models never get one:

  • A skinned mesh, because bones move it and a bake cannot follow.
  • A model that already carries a second UV set, because the layout is the author’s and the art may be painted against it. Blender’s Smart UV Project into a second UV map is exactly this, and it is usually better than what an importer can infer.

The import reports how much of the atlas the charts fill. A low number means the charts are many and small: raise the chart angle and they merge, at the cost of more stretch inside each one.

See Baked light for what reads the result.

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 MeshRenderer 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
alphaMode ≠ BLEND 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.

A model that carries shapes to blend towards — blend shapes, in most DCC tools — brings them with it: the .esmesh holds one delta per vertex per target beside the vertices, and the prefab gives the entity a MeshMorph whose weights say how far it is blended towards each. Entry i is target i, in the order the file lists them; the names the source carries are kept with the geometry, so a weight addresses a shape by position.

A weight of 0 leaves the mesh as authored and 1 is the target in full. Neither is a bound: glTF states none, and pushing past 1 or below 0 is a shape an author may want.

Limit Why
8 shapes blended at once A budget on what is LIVE, not on what a mesh carries — a face holds a handful of expressions open while its mesh has a hundred. The heaviest weights are taken; the rest read as zero, which is what an untouched weight already means.
Normals deform only where the geometry has them A mesh with no NORMAL channel is shaded off a constant normal, which no delta can bend.
Tangents are not carried The tangent frame is derived per pixel, from the surface as deformed.
An FBX’s in-between shapes A channel that passes through shapes on its way to the last one carries only the last; the import says which it dropped.

FBX is read by ufbx, built into the editor — no Autodesk FBX SDK, nothing to install. Binary and ASCII, and every version from the 5000-series up, go through the same reader.

What the format needs that glTF does not, the import settles before anything downstream sees it:

FBX says What arrives
Its own up axis and unit (centimetres, Z-up, …) Converted to the engine’s: +Y up, one unit is one metre.
Polygons of any size Triangles.
Geometry offset from its node with no node to hold it A child entity holding that offset, named GeometryTransform.
A mesh split across several materials One .esmesh per material run — the same split glTF calls a primitive.
Rotations as Euler angles around pivots, with pre- and post-rotation Baked into position/rotation/scale keyframes (resampled at 30 fps where the curves are not already linear).

Materials are read through ufbx’s own mapping, so Phong, Lambert, Maya’s Standard Surface, 3ds Max’s Physical and Blender’s Principled all arrive as the same parameters listed under Materials — a Phong material’s roughness is derived from its specular exponent, and it reports no metalness rather than a made-up zero-metal surface.

Two things about FBX materials are worth knowing before you author for it:

  • Metalness and roughness maps are only carried when they are one image, packed the way glTF packs them (roughness in green, metal in blue) — which is what the engine’s Model shader samples. Exporters normally write two separate images; when they do, the import says so and the constants are used. Pack them into one image, or set the map by hand on the .esmaterial.
  • An opacity map is not carried. Alpha comes from the base colour image’s own channel.

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
weights animation channels The shapes themselves are imported (see Morph targets); a clip that drives them is not carried yet.
A second UV set (TEXCOORD_1), KHR_texture_transform, an FBX uv 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.
Cameras and lights in an FBX A scene’s camera and lighting are the scene’s, not the model’s.
Separate FBX metalness/roughness maps, an FBX opacity map See FBX.
  • 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.