Particles and decals
Status: Implemented (API 1.13). Mod particle types from sprite sheets, spawning from the server (a vanilla fallback for vanilla clients) and the client, and decals projected on blocks. Emitters on entities and their bones (API 1.20). Particle types with their own tick code (providers) are Planned. Guide: Particles and Decals.
Function tables: render, level.
Particle types
cl_result cl_render_particle_type(cl_str id, const cl_particle_desc* desc); // MAIN, both sides, at setup
cl_particle_desc (CL_PARTICLE_DESC_INIT): texture (a PNG of the mod, frames frames in columns columns), frame_ticks (0: spread over the life), lifetime, lifetime_jitter, size, size_end (half size in blocks; < 0 no change), gravity, friction, collide, color, color_end (ARGB; 0 no change), fade, emissive, additive, facing (CL_PARTICLE_FACE_CAMERA, CL_PARTICLE_VERTICAL, CL_PARTICLE_FLAT, CL_PARTICLE_VELOCITY), fallback (the vanilla particle vanilla clients get, "" none). Registering an id again replaces the type. At most 4096 types.
Spawning
cl_result cl_level_particle(cl_level level, cl_str type, const cl_particle_spawn* s); // TICK, both sides
type: a mod type ("my_mod:spark") or a vanilla one ("minecraft:flame", "dust{color:[1,0,0],scale:1}"; a bare name the mod did not register is vanilla). cl_particle_spawn (CL_PARTICLE_SPAWN_INIT): pos, vel (blocks per tick), count (0…4096), spread (gaussian offsets per axis), speed (random extra velocity), color (tint over the type’s, 0 none), scale, force (beyond 32 blocks, up to 512, and with Particles: Minimal), player (server: one player only).
- Server: Celestial clients with CLight get the mod particle through the
clight:fxpayload, vanilla clients the type’s fallback as a vanillaClientboundLevelParticlesPacket(withcount0 andvelas its direction when there is no spread); a vanilla type goes to everyone as the vanilla packet. - Client: draws only on this client.
- The particle setting applies like vanilla (Decreased drops a third, Minimal shows none) unless
force.
Mod particles have their own small engine: at most 16384 at a time, the oldest go first, one instanced batch per type next to vanilla particles; shader packs draw them with gbuffers_particles.
Decals
cl_result cl_level_decal(cl_level level, const cl_decal_desc* desc); // TICK, both sides
cl_result cl_level_clear_decals(cl_level level, cl_vec3 pos, double radius); // the caller's decals; radius <= 0: all
cl_decal_desc (CL_DECAL_DESC_INIT): texture (a PNG of the mod), pos (a point on the surface), normal (snapped to the nearest block face; 0: up), size (edge length in blocks), rotation (degrees around the normal), lifetime (ticks, fading over the last fifth; <= 0 until cleared or the level changes), color (ARGB), emissive, player (server: one player only; else every Celestial client with CLight within 64 blocks).
The decal is clipped once, when it is created, to every full (sturdy) face under it that no other block covers. At most 1024 decals (the oldest go first). Vanilla clients see nothing.
| C | C++ | Lua |
|---|---|---|
cl_render_particle_type(id, &d) |
clight::render::particle_type(id, &d) |
cl.render.particle_type(id, { texture =, frames =, ... }) |
cl_level_particle(level, type, &s) |
clight::level::particle(level, type, &s) |
level:particle(type, pos, vel, count, spread) or level:particle(type, { ... }) |
cl_level_decal(level, &d) |
clight::level::decal(level, &d) |
level:decal(texture, pos, normal, size, rotation, lifetime) or level:decal{ ... } |
cl_level_clear_decals(level, pos, r) |
clight::level::clear_decals(level, pos, r) |
level:clear_decals(pos, r) |
cl.has("render.particles"), cl.has("render.decals").
Emitters (API 1.20)
cl_result cl_render_emitter_add(cl_entity e, cl_str type, cl_str bone, const cl_particle_spawn* s, int32_t interval_ticks, cl_emitter* out);
cl_result cl_render_emitter_remove(cl_emitter em);
cl_result cl_render_emitter_set_active(cl_emitter em, bool active);
Every interval_ticks (1…72000) the emitter spawns s at the entity, or at a bone of its glTF model ("": the entity’s feet). s.pos is an offset: blocks along the bone’s right / up / forward axes, or along the world axes without a bone; the other fields are those of cl_level_particle (at most 256 particles per emission). A client entity handle makes a client-only emitter (only this player sees it); a server handle sends the particles like cl_level_particle (Celestial clients get mod particles, vanilla clients the fallback). The emitter ends with the entity, with cl_render_emitter_remove, or when the mod unloads; at most 1024 per mod.
local em = zombie:emit("minecraft:flame", { bone = "head", offset = { 0, 0.4, 0 }, every = 2, count = 1 })
em:pause()
em:resume()
em:remove()