Skip to content

Models and animation

Lesson 7.6

Your own entity types can look like anything: a glTF model with bones, animations and morph targets (shape keys). This lesson gives a type a model, plays animations in layers, reacts to events in a clip and turns a head towards the player.

The model file

Export glTF 2.0 (.glb) and put it in your mod’s assets: assets/my_mod/models/entity/creature.glb is the path "my_mod:models/entity/creature.glb". 1 unit is 1 block, the model faces +Z, every glTF animation is a clip named after it, and textures are PNG. A creature_clips.json next to it sets loop flags and events (named moments in a clip):

{ "clips": { "attack": { "loop": false, "events": { "strike": 0.3 } } } }

Give a type its model

In CL_EV_LIFECYCLE_REGISTER, after the type itself:

static void on_register(void* payload, void* user) {
    (void)payload;
    (void)user;
    cl_entity_type_desc d = CL_ENTITY_TYPE_DESC_INIT;
    d.id = CL_STR("my_mod:creature");
    d.vanilla_fallback = CL_STR("minecraft:zombie");
    d.width = 0.8f;
    d.height = 1.2f;
    cl_entity_type_register(&d, NULL);

    cl_model_opts mo = CL_MODEL_OPTS_INIT;
    mo.default_clip = CL_STR("idle");         // looped until you play something else
    cl_model model;
    if (cl_render_entity_model(CL_STR("my_mod:creature"), CL_STR("my_mod:models/entity/creature.glb"), &mo, &model) != CL_OK)
        CL_LOGF(CL_LOG_ERROR, "creature model: %d", (int)cl_last_error());
}

cl_model_opts also has scale, yaw_offset (180 for a model facing -Z), offset, shadow_radius and flags (CL_MODEL_NO_SHADOW, CL_MODEL_NEAREST for pixel art, CL_MODEL_DOUBLE_SIDED, CL_MODEL_NO_DEATH_TILT). cl_model_get_info and cl_model_clip_length read the model.

Play clips

static void start(cl_entity c) {
    cl_entity_anim_play(c, CL_STR("walk"), NULL);             // layer 0, defaults

    cl_anim_play_opts o = CL_ANIM_PLAY_OPTS_INIT;
    o.layer = 1;
    o.mask = CL_STR("neck");                                  // only the neck and its children
    o.fade = 0.3f;
    cl_entity_anim_play(c, CL_STR("look_around"), &o);

    cl_anim_play_opts b = CL_ANIM_PLAY_OPTS_INIT;
    b.layer = 2;
    b.additive = true;                                        // added on top of layers 0 and 1
    b.weight = 0.5f;
    cl_entity_anim_play(c, CL_STR("breathe"), &b);
}

cl_anim_play_opts: loop (-1 the clip’s own, 0 once, 1 loop), fade, speed, layer (0…7), weight, additive, mask, start, root_motion (the root bone’s walk moves the entity), hold. On the server, the state goes to every Celestial client that sees the entity. cl_entity_anim_stop(c, -1, 0.2f) fades every layer out; cl_entity_anim_layer changes a layer’s weight or mask without restarting it; cl_entity_set_morph(c, CL_STR("inhale"), 0.8f) sets a morph target.

Events

cl_str is a pointer and a length, so a small helper compares names:

#include <string.h>

static bool is(cl_str s, const char* name) { return s.len == strlen(name) && memcmp(s.ptr, name, s.len) == 0; }
static void on_anim_event(void* payload, void* user) {
    (void)user;
    cl_ev_anim_event* e = CL_EVENT(cl_ev_anim_event, payload);
    if (e->client) return;                                  // fires on clients too (sounds, particles)
    if (is(e->name, "strike")) CL_LOGF(CL_LOG_INFO, "the claw lands at %.2f s", e->time);
}

static cl_result subscribe_events(void) {
    cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
    o.filter = cl_ident_intern(CL_STR("my_mod:creature"));
    return cl_event_subscribe(CL_EV_ENTITY_ANIM_EVENT, &o, on_anim_event, NULL, NULL);
}

Look-at and bones

static void follow(cl_entity c, cl_entity player) {
    cl_look_at_opts o = CL_LOOK_AT_OPTS_INIT;
    o.bones = CL_STR("neck,head");      // parent first; they share the turn
    o.target_entity = player;           // its eyes; or o.target = a position
    o.max_yaw = 60.0f;
    o.max_pitch = 30.0f;
    cl_entity_anim_look_at(c, &o);

    cl_bone_transform jaw;
    if (cl_entity_bone_transform(c, CL_STR("jaw"), &jaw) == CL_OK)
        CL_LOGF(CL_LOG_INFO, "jaw at %.1f %.1f %.1f", jaw.pos.x, jaw.pos.y, jaw.pos.z);
}

cl_bone_transform has pos, the unit vectors forward, up, right, and scale.

Items and blocks with models

The same glTF files work for items and blocks (API 1.15):

static void on_register(void* payload, void* user) {
    (void)payload, (void)user;
    cl_item_model_opts io = CL_ITEM_MODEL_OPTS_INIT;
    io.default_clip = CL_STR("glow");
    io.display = CL_STR("{\"gui\": {\"rotation\": [20, 30, 0], \"scale\": [0.9, 0.9, 0.9]}}");
    cl_model staff;
    cl_render_item_model(CL_STR("my_mod:crystal_staff"), CL_STR("my_mod:models/item/staff.glb"), &io, &staff);

    cl_block_model_opts bo = CL_BLOCK_MODEL_OPTS_INIT;
    cl_model statue;
    if (cl_render_block_model(CL_STR("my_mod:statue"), CL_STR("my_mod:models/block/statue.glb"), &bo, &statue) == CL_OK)
        cl_model_set_lod_distances(statue, CL_STR("12,32"), 0);  /* statue_lod1 / statue_lod2 from 12 and 32 blocks */
}

io.fit (true by default) makes an item model the size of a block item; display is the "display" JSON of vanilla item models. A block entity such as minecraft:chest draws the model instead of its vanilla look. Guide: Item and Block Models.

Try it

When a creature’s "attack" clip fires "strike" on the server, log the position of its "jaw" bone.

Show a solution
static void on_strike(void* payload, void* user) {
    (void)user;
    cl_ev_anim_event* e = CL_EVENT(cl_ev_anim_event, payload);
    if (e->client || !is(e->name, "strike")) return;
    cl_bone_transform jaw;
    if (cl_entity_bone_transform(e->entity, CL_STR("jaw"), &jaw) == CL_OK)
        CL_LOGF(CL_LOG_INFO, "strike at %.2f %.2f %.2f", jaw.pos.x, jaw.pos.y, jaw.pos.z);
}

Subscribe it to CL_EV_ENTITY_ANIM_EVENT with the type filter, like above.

Recap: cl_render_entity_model gives a type a glTF model, cl_entity_anim_play plays clips on layers (masks, additive, fades), CL_EV_ENTITY_ANIM_EVENT fires on named moments, cl_entity_anim_look_at turns bones toward a target and cl_entity_bone_transform finds a bone in the world. Guide: Entity Models and Animation.