Particles and decals
Status: Implemented (API 1.13). Mod particle types from sprite sheets, spawning from the server (with a vanilla fallback for vanilla clients) and from the client, and decals projected on block faces. Particle emitters on entities and bones arrived in API 1.20 (Particles and decals). Particles with their own tick code and decals on entities are Planned.
This guide adds sparks to a forge and leaves scorch marks where a fireball lands. Everything works the same from Lua, C and C++; the server sends particles and decals to every Celestial client with CLight nearby, so you write the effect once.
Reference: Rendering, function tables render and level.
1. A particle type
A particle type is a sprite sheet in your mod’s assets and a few numbers. Register it on both sides at setup (the server needs to know the fallback, the client the texture):
cl.on("lifecycle.register", function()
cl.render.particle_type("spark", {
texture = "forge:textures/particle/spark.png", -- 2 x 2 frames
frames = 4, columns = 2,
lifetime = 30, lifetime_jitter = 10, -- ticks
size = 0.12, size_end = 0.02, -- half size in blocks, shrinking over the life
gravity = 0.02, friction = 0.96, collide = true,
color = "#FFFFD080", color_end = "#FFFF4000", -- ARGB: hot yellow to dark orange
emissive = true, additive = true, -- glows, also in the dark
fallback = "minecraft:flame", -- what vanilla clients see
})
end)
| Field | Meaning |
|---|---|
texture |
a PNG of your mod, frames frames in columns columns (row by row) |
frame_ticks |
ticks per frame; 0 spreads the frames over the particle’s life |
lifetime, lifetime_jitter |
life in ticks, plus up to lifetime_jitter more per particle |
size, size_end |
half the quad’s size in blocks; size_end < 0 keeps the size |
gravity, friction, collide |
blocks per tick squared, the share of the velocity kept per tick, stop on blocks |
color, color_end |
ARGB tint and the tint at the end (0: no change); fade fades the last third out |
emissive, additive |
full brightness; added on top (glow) instead of blended |
facing |
CL_PARTICLE_FACE_CAMERA (default), VERTICAL (turns around Y), FLAT (lies on the ground), VELOCITY (stretched along its motion) |
fallback |
a vanilla particle for vanilla clients ("minecraft:flame", "dust{color:[1,0,0],scale:1}"), "" for nothing |
2. Spawning
-- on the server: everyone near sees it (vanilla clients get the fallback)
world:particle("forge:spark", forge_pos + cl.vec(0.5, 1.1, 0.5), cl.vec(0, 0.08, 0), 12, 0.2)
-- the table form has every option
world:particle("forge:spark", { pos = p, vel = cl.vec(0, 0.1, 0), count = 30, spread = cl.vec(0.3, 0.1, 0.3),
speed = 0.05, color = "#FF80C0FF", scale = 1.5, force = true })
spreadplaces each particle up to that far away (gaussian per axis, like/particle’s delta);speedadds a random velocity of up to that much.forceshows the particles beyond 32 blocks (up to 512) and even with Particles: Minimal. Without it the player’s particle setting applies like vanilla: Decreased drops a third, Minimal shows none.playersends them to one player only.- On a client the same call draws only there: use it for effects only one player should see (a hit marker, a magic aura in first person).
- Vanilla particle names work too (
world:particle("minecraft:flame", ...)): the server sends the vanilla packet to everyone.
In C:
cl_particle_spawn s = CL_PARTICLE_SPAWN_INIT;
s.pos = (cl_vec3){x + 0.5, y + 1.1, z + 0.5};
s.vel = (cl_vec3){0, 0.08, 0};
s.count = 12;
s.spread = (cl_vec3){0.2, 0.2, 0.2};
cl_level_particle(level, CL_STR("forge:spark"), &s);
C++: clight::level::particle(level, "forge:spark", &s).
Cost. Mod particles live in their own small engine (at most 16384 at a time, the oldest go first, like vanilla’s groups) and are drawn as one instanced batch per type next to vanilla particles, so shader packs draw them with gbuffers_particles. Without mod particles nothing runs.
3. Decals
A decal is a texture lying on blocks: blood, scorch marks, footprints, graffiti.
-- a scorch mark where the fireball hit, facing the side it hit, gone after 30 seconds
world:decal("forge:textures/decal/scorch.png", hit.pos, hit.normal, 2.0, math.random(0, 359), 600)
world:decal{ texture = "forge:textures/decal/footprint.png", pos = feet, normal = cl.vec(0, 1, 0),
size = 0.6, rotation = player.yaw, lifetime = 200, color = "#C0FFFFFF" }
- The normal snaps to the nearest block face. The decal is clipped to every full face under it, so it bends over nothing: a scorch mark at the edge of a cliff stops at the edge, and nothing is drawn in the air or on faces another block covers.
- It is lit by the light in front of it and fades out over the last fifth of its life (
lifetime <= 0: it stays until the player leaves the level or you clear it).emissive = truemakes it glow. - At most 1024 decals at a time (the oldest go first).
world:clear_decals(pos, radius)removes yours near a position (no arguments: all of yours). - From the server, decals reach the Celestial clients with CLight within 64 blocks; vanilla clients see nothing.
The decal is computed once when it is created: if the block under it is broken later, the decal stays where the face was until it fades. For marks that must follow changes, clear and place them again in level.block_changed.
4. Assets and resource packs
Textures are ordinary PNGs in assets/<your id>/textures/...; resource packs can replace them like any texture. Use transparent borders on decal textures: everything outside the texture’s square is cut off, but the edge pixels are sampled up to the border.
Try it
Leave a glowing footprint under every player in creative mode twice a second, and a small burst of sparks above it.
Show a solution
local t = 0
cl.on("tick.server_post", function()
t = t + 1
if t % 10 ~= 0 then return end
for _, p in ipairs(cl.entity.server_players()) do
if p.gamemode == "creative" then
p.level:decal{ texture = "forge:textures/decal/footprint.png", pos = p.pos, size = 0.5,
lifetime = 100, emissive = true, color = "#FFFFD040" }
p.level:particle("forge:spark", p.pos + cl.vec(0, 0.2, 0), cl.vec(0, 0.05, 0), 4, 0.1)
end
end
end)
The decal lies on the block the player stands on (the position is just above its top face), and the footprints fade after five seconds.