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.