Kzeroko KTM2

Particlize

The compose and decompose effect that dissolves an entity into shards: the profile JSON, every field it accepts, how inheritance resolves, and the client budget.

Updated Sep 18, 2026 9 min read fxparticlizeshadersresource pack

Particlize takes an entity’s rendered model and either scatters it into shards or assembles it from them. A profile decides how that looks: how long it runs, what the dissolve edge is tinted, how many shards fly off, and how much of it survives at distance. Profiles are resource-pack JSON, so an effect can be retuned without touching code. What plays at which moment is Entity FX.

Where the files live

assets/<namespace>/particlize_fx/<name>.json

One file is one profile, and its identifier is <namespace>:<name>. The bundled set lives under assets/isekaiexpansion/particlize_fx/ — twelve files, with base.json as the one every other profile inherits from.

Because these are assets rather than data, a resource pack overrides a profile the same way it overrides a texture: the pack highest in the list wins the whole file. There is no per-field merging between packs. Inheritance inside a profile is a different mechanism, described below.

Minimal example

Every field falls back to a built-in default, so the smallest accepted file is {}. The smallest useful one inherits and changes one thing:

{
  "extends": "isekaiexpansion:base",
  "surface": {
    "tint": "#7FE6FFFF"
  }
}

extends names another profile by identifier. The parent’s JSON is deep-merged with this file’s before anything is parsed: nested objects merge key by key, and any key present here replaces the parent’s. That is why the example above keeps every other surface setting from base.

Inheritance chains as far as you like, and a cycle is detected and reported rather than hanging.

Fields

FieldTypeDefaultWhat it does
extendsidentifierno parentProfile to inherit from, deep-merged before parsing. A missing target fails this profile.
timelineobjectbuilt-in defaultsWhen the effect runs and for how long.
surfaceobjectbuilt-in defaultsThe dissolve pass over the entity’s own geometry.
shardsobjectbuilt-in defaultsThe loose particles thrown off during the dissolve.
lodobjectbuilt-in defaultsDistance thresholds and the complexity budget.

The four objects are independent. Omit one and it takes its defaults whole; include it and only the keys you write are changed, the rest keeping their defaults or the parent’s values.

timeline

FieldTypeDefaultWhat it does
duration_ticksinteger32Length of the dissolve, clamped to 1…1200.
delay_ticksinteger0Wait before anything renders, clamped to 0…1200. Use it to line up with a wind-up.
hold_ticksinteger8How long the finished state stays on screen, clamped to 0…1200.
easingenumsmoother_stepCurve applied to progress: linear, smooth_step, smoother_step, quad_in, quad_out. An unrecognised name silently becomes smoother_step.

surface

The dissolve itself — the shader pass over the entity’s own geometry.

FieldTypeDefaultWhat it does
shapeenumplaneThe mask the dissolve sweeps with: plane, sphere, cylinder, box, noise. Unrecognised names become plane.
directionarray<number>[0.0, 1.0, 0.0]Sweep direction, normalised on load. A zero-length vector becomes straight up.
tintcolour#B873FFFFColour washed over the dissolving surface. #RRGGBBAA.
tint_strengthnumber0.22How much of tint reaches the surface, 0.0…1.0.
edge_colorcolour#7629FFFFColour of the advancing dissolve edge.
edge_widthnumber0.13Thickness of that edge, 0.001…1.0. It cannot be zero — an edge of nothing renders as a hard seam.
emissionnumber0.85Self-lit brightness of the edge, 0.0…4.0. Above 1.0 it blows out, which is the point for magic.
noise_scalenumber5.5Frequency of the dissolve noise, 0.01…128.0. Higher is grainier.
noise_strengthnumber0.16How much that noise distorts the edge, 0.0…1.0.
cell_scalenumber14.0Size of the shatter cells, 1.0…128.0. Higher means more, smaller fragments.
shatternumber0.18How strongly the surface breaks into those cells, 0.0…1.0.
displacementnumber0.025How far fragments drift from the model, 0.0…2.0, in blocks.
bounds_paddingnumber0.06Extra render bounds so drifting fragments are not culled, 0.0…1.0. Raise it if the effect clips at the edges.

shards

The loose particles thrown off during the dissolve.

FieldTypeDefaultWhat it does
enabledbooleantruefalse renders the surface pass only. The client config can also switch shards off globally.
colorcolour#B873FFE6Shard colour, alpha included.
countinteger84Shards at close range, clamped to 0…512.
far_countinteger24Shards past lod.far_distance, clamped to 0…count — a larger value is reduced to count, never the other way round.
sizenumber0.035Shard size in blocks, 0.002…1.0.
size_variancenumber0.55Random spread around size, 0.0…1.0.
speednumber0.55Initial outward speed, 0.0…4.0.
spreadnumber0.45Cone width of the initial scatter, 0.0…4.0.
gravitynumber0.015Downward pull, -1.0…1.0. Negative floats the shards upward.
lifetime_ticksinteger14How long a shard lives, 1…1200.

lod

Distance and complexity budget. These are the fields that decide whether a crowded fight stays playable.

FieldTypeDefaultWhat it does
near_distancenumber24.0Full quality inside this radius, 1.0…512.0 blocks.
far_distancenumber56.0Reduced quality past it; clamped to at least near_distance.
max_distancenumber96.0Nothing renders past it; clamped to at least far_distance.
complex_model_verticesinteger6000Above this vertex count the model is treated as complex, 256…1000000.
extreme_model_verticesinteger18000Above this it takes the cheapest path; clamped to at least complex_model_vertices.
fallback_high_subdivisionsinteger8Subdivision level for the fallback mesh near the camera, 1…12.
fallback_medium_subdivisionsinteger5Mid-range level; clamped to at most fallback_high_subdivisions.
fallback_low_subdivisionsinteger3Far level; clamped to at most fallback_medium_subdivisions.

The three distances and the three subdivision levels are each clamped in order, so a profile cannot describe a far distance nearer than its near distance, or a low level richer than its high one. Write them in a sensible order and the clamps never fire.

Validation

Nothing here throws at the player.

  • A malformed or unparseable profile is logged as a warning naming the resource, and that one profile is dropped. Every other profile still loads.
  • extends pointing at a profile that does not exist fails that profile the same way, with Unknown inherited profile in the log.
  • A cycle in extends is detected while resolving and logged with the full path, rather than recursing until the client dies.
  • An out-of-range number is clamped, not rejected. A count of 9999 becomes 512 and nothing tells you in game — check the log only for load failures, not for clamping.
  • A non-finite number falls back to the low end of its range.
  • An unknown enum name falls back: plane for shape, smoother_step for easing. Case and surrounding whitespace do not matter.
  • If no profile loads at all, the registry keeps one entry — arcane_pulse mapped to the built-in default — so the renderer always has something rather than crashing on an empty map.

Asking the registry for an identifier it does not have returns nothing. That is a caller’s problem to handle, not a silent substitution.

Reloading

ParticlizeProfileRegistry is a client resource reload listener, so profiles reload with F3+T and with any resource pack change. Parsing happens off-thread and the parsed map is swapped in atomically, so an effect mid-play finishes with the profile it started with.

Nothing about profiles crosses the network. The server sends which profile id to play; the client resolves it from its own resource packs. Two players with different packs will correctly see different effects.

Playing a profile

Server-side, ParticlizeFxBroadcaster decides who sees the effect:

MethodWho sees it
playTracking(target, profileId, mode, …)Everyone tracking the entity. Returns an effect id.
playTrackingObservers(…)Trackers, with an explicit observer filter.
playNearby(entity, profileId, mode, seed, priority, radius)Everyone inside radius of the entity.
playFor(…)One named viewer.
cancelTracking(target, effectId)Stops a running effect by the id playTracking returned.
clearFor(viewer)Drops everything queued for one player, for logout and dimension change.

The seed argument makes a scatter reproducible: the same seed produces the same shard pattern on every client, which matters when two players compare a recording and not at all otherwise.

ParticlizeMode is DECOMPOSE, COMPOSE or PULSE. ParticlizePriority is AMBIENT, NORMAL, IMPORTANT or CRITICAL, and decides what survives when the client is over budget.

Client-side, ParticlizeFx starts an effect locally and returns a ParticlizeHandle for cancelling it. Use it only for something purely cosmetic and purely local; anything another player should see goes through the broadcaster.

Client config

Under particlize_fx in config/isekaiexpansion.json. All client-side: the server never enforces these.

OptionTypeDefaultWhat it does
enableParticlizeFxbooleantruefalse disables every particlize effect on this client.
particlizeFxShardsbooleantruefalse keeps the surface dissolve and drops the loose shards.
particlizeFxRenderDistanceinteger128Hard cap in blocks, 8…256. Applied on top of each profile’s lod.max_distance; the smaller of the two wins.
particlizeFxShaderCrowdThresholdinteger4How many simultaneous shader-path effects before the renderer starts downgrading, 1…16.
particlizeFxNativeCrowdThresholdinteger8The same limit for the cheaper native path, 1…32.
particlizeFxMaxShardsPerFrameinteger1024Upper bound on shards drawn in one frame, 128…2048. Profiles asking for more are trimmed, priority first.
  • Entity FX — which profile plays on death, teleport and respawn.
  • Effekseer Particles — the other effect system, for authored emitters rather than entity dissolves.