DEMIENGINEDOCUMENTATION Everything is free
3D gameplay · Game systems

glTF models, LOD, instancing, and procedural meshes.

Models & meshes

A model is 3D art brought into the game as an asset. This page explains how to import one, how MeshRenderer draws it (including level-of-detail variants), how to instance many copies, and how to build geometry in code.

What a model is

A model is a bundle of triangles, optional skeleton, and optional animation clips produced by a 3D art tool. DemiEngine reads that bundle as a Model3D asset and a MeshRenderer component draws it at an entity’s transform.

Models exist so characters, props, and stations can be authored once in a DCC tool and reused everywhere: same asset, many placed instances, one import pipeline.

Importing a model

Author renderable models in glTF or GLB. The asset importer also recognizes OBJ, IQM, and M3D, but the current 3D renderer loads Model3D geometry through its glTF/GLB loaders; convert other source formats to glTF/GLB for this workflow. The glTF loaders draw triangle primitives; line and point primitives are skipped.

demi asset import games/skyforge/source/drone.glb \
  --project games/skyforge --id asset://models/drone \
  --preset animated_character
  • The first path is the source file produced by your art tool.
  • --id gives the model a stable asset URI; the importer writes its *.asset.json manifest under the project's assets/ directory.
  • --preset animated_character enables animation import. The default static_prop preset does not import animations.
  • The importer writes settings.model_import. Runtime clip names come from animation names in the glTF/GLB; the optional settings.animations.clips list is manifest metadata and does not enable import or rename glTF clips.
  • Validate the project with demi validate games/skyforge to check its references.

An animated asset can also record clip metadata in its manifest:

{
  "settings": {
    "animations": {
      "clips": [
        { "name": "Idle", "skeleton": "Armature" },
        { "name": "Fly",  "skeleton": "Armature" }
      ]
    }
  }
}
  • clips records names for manifest validation. The glTF/GLB animation names supply the actual runtime clips; keep metadata names aligned with them.
  • skeleton records a skeleton name for validation. Listed clips in one asset must not name different skeletons. Animated LOD variants need matching runtime clip names and durations for the renderer to switch models.

MeshRenderer

{
  "MeshRenderer": {
    "model": "asset://models/drone",
    "medium_lod_model": "asset://models/drone_med",
    "medium_lod_distance": 25,
    "low_lod_model": "asset://models/drone_low",
    "low_lod_distance": 60
  }
}
FieldMeaning
modelThe full-detail Model3D asset URI to draw.
medium_lod_modelOptional mid-distance model. Used when the camera is far enough that the full mesh is wasted.
medium_lod_distanceCamera distance in world units where the medium model starts. Zero disables this threshold.
low_lod_modelOptional far-distance model.
low_lod_distanceCamera distance in world units where the low model starts. Zero disables this threshold.

Level of detail (LOD) keeps a deck full of crates cheap: near ones use model, distant ones use the reduced meshes. LOD variants are ordinary authored models — build them in your art tool with the same proportions and give them the same clip names and durations so animation keeps working when the swap happens. Animated LOD stays on the full-detail model during authored layers, tracked transition progress, or procedural bone overrides.

Skeletal playback is controlled by AnimationPlayer3D plus per-entity visual update settings:

{
  "AnimationPlayer3D": {
    "clip_name": "Fly",
    "loop": true,
    "visual_update_rate": 30,
    "visual_update_distance": 40
  }
}
  • clip_name names an imported glTF/GLB animation clip.
  • loop repeats the clip.
  • visual_update_rate is how many times per second distant poses refresh. Lower rates save work on background crowds.
  • visual_update_distance is the distance beyond which poses are held rather than refreshed every frame.

On Vulkan, supported rigs use GPU skinning automatically; unsupported rigs use CPU skinning. The GPU path supports up to 128 referenced skin/node matrices across multiple skins and rigid parts, with up to four influences per vertex. It requires the built-in material and valid authored normals; procedural bone segments, authored layers, and tracked transition progress use the CPU path. Those fallback cases do not imply visual layer or transition pose blending. OpenGL/OpenGLES also use CPU skinning. Set DEMI_GPU_SKINNING=0 before launch to force the CPU path while profiling.

Clip switching and blend rules live in State machines.

MeshInstances3D

A station corridor with two hundred identical pipes can use one entity with MeshRenderer and MeshInstances3D. The latter draws labeled, render-only copies of the entity's shared static mesh.

{
  "MeshRenderer": { "model": "asset://models/pipe" },
  "MeshInstances3D": {
    "transforms": {
      "pipe_a": { "position": [0, 0, 0] },
      "pipe_b": { "position": [2, 0, 0] },
      "pipe_c": { "position": [4, 0, 0], "rotation": [0, 0.4, 0], "scale": [1, 1, 1] }
    }
  }
}
  • MeshRenderer.model supplies the shared model; MeshInstances3D.transforms maps nonempty stable labels to local transforms.
  • Each transform accepts position, Euler rotation in radians, and scale. These are composed with the owning entity's transform. Omitted fields use identity values; no scale axis may be zero.

Use it for static repeated props. Copies have no individual entities, colliders, or scripts, and the component cannot share an entity with AnimationPlayer3D. Use separate entities when each copy needs animation or physics.

World text

WorldText3D places a text label in the world — docking bay numbers, cargo tags, callouts over objectives. It behaves like a mesh for placement and depth, and takes its string and style from authored fields.

Procedural meshes

Some geometry is not worth exporting from a tool: debug hulls, generated floors, voxel caves. The demi.mesh.procedural module builds a mesh at runtime and attaches it to an entity.

local Proc = require("demi.mesh.procedural")

local mesh = Proc.create(4)
mesh:add_quad(0, 1, 0,
    0, 0, 0, 0, 0,  1, 0, 0, 1, 0,
    1, 0, 1, 1, 1,  0, 0, 1, 0, 1)

local applied = Proc.apply(entity_id, mesh)
CallArgumentsReturns
createoptional vertex capacityA new empty mesh builder handle.
clearon the builderNothing. Empties accumulated geometry.
reserveon the builder, vertex countNothing. Preallocates space for large builds.
add_vertexposition, normal, UV componentsNothing. Appends one vertex.
add_quadone normal and four positions/UVsNothing. Appends a quad face.
add_voxel_blocksblocks, occupancy, tile mapping, atlas columns, occupancy strideNothing. Appends blocky voxel geometry.
add_voxel_heightfieldheight and block layers, rocky columns, tile mapping, atlas columns, chunk and section dimensions, fill depthNothing. Appends a heightfield surface.
applyentity id, builder, optional texture/material/render layerBoolean success. Uploads built geometry for the entity.

Build, then apply. A cleared builder can be refilled and reapplied without creating a new handle.

Voxel worlds

For sectioned editable block volumes, use demi.voxel.world. Scripts set or erase sections, then call build_section_mesh and Proc.apply for the sections they want to draw. This is an explicit section-meshing API, not automatic world streaming or terrain excavation.

Picking the right tool

  • Character or hero prop with animation: glTF Model3D + MeshRenderer + AnimationPlayer3D.
  • Hundreds of identical bolts: MeshInstances3D.
  • Generated floor or debug shape: demi.mesh.procedural.
  • Editable block volume: demi.voxel.world.
  • Label in space: WorldText3D.

Characters that walk the world use CharacterController3D. Cameras and lighting that make models readable are covered in Lights & camera. Breakable props build on the same model setup — see Destruction.