Skip to content

Advanced Usage

Stasis, the Shattered edited this page Jun 20, 2026 · 4 revisions

Looping

loop(n) plays the animation n + 1 times total, and loopInfinite() loops forever. Combine with onLoop for periodic effects, like a pulsing effect that plays a sound.

.loop(2) // plays 3 times total
.loopInfinite() // plays forever, until stopped

Callbacks

Hook into the animation lifecycle with onStart, onEnd, onLoop, and onKeyframeReached(time, callback). Useful for triggering sounds, particles, or other logic at specific points:

.onStart(e -> doSomething()) // runs only on the start FIRST loop
.onKeyframeReached(0.5f, e -> level.playLocalSound(...)) // runs each loop
.onLoop(e -> doSomething()) // does NOT run on the start of the first loop, or when reaching the end
.onEnd(e -> doSomethingElse()) // runs only on the start of the LAST loop (or when the animation is manually ended)

Playback Controls

Animations can be paused, resumed, reversed, or sped up/slowed down at any time:

vfxEntity.pauseAnimation();
vfxEntity.resumeAnimation();
vfxEntity.setPlaySpeed(-1f); // reverses the animation
vfxEntity.setPlaySpeed(2f);

Calling setPlaySpeed with a negative value reverses direction starting from wherever the animation currently is, not from the end.

Reversing with a queue: reversing the currently playing animation works fine, but reversing back through a queue is not supported, since inheritance only flows forward. If you need a "play this in reverse" sequence, build the reverse stages explicitly rather than trying to reverse a forward queue.

Inheritance

When animations are queued, the next animation can inherit properties from wherever the previous one left off, avoiding hardcoded start values and snapping. Just call the relevant inherit* method instead of declaring a starting value:

// stage 1: slam down and squish
VfxAnimation drop = VfxAnimationBuilder.create()
                .blockState(Blocks.ANVIL.defaultBlockState(), builder -> {})
                .scale(1f, builder -> {})
                .translation(0, 5, 0, builder -> builder
                        .addKeyframe(1f, 0, 0, 0, EasingType.IN_QUART))
                .build(20);

// stage 2: squish on impact, inheriting the block state, and where it landed
VfxAnimation squish = VfxAnimationBuilder.create()
                .inheritBlockState()
                .inheritTranslation()
                .scale(1f, builder -> builder
                        .addKeyframe(0.3f, 2f, 0.2f, 2f, EasingType.OUT_EXPO)
                        .addKeyframe(1f, 1f, 1f, 1f, EasingType.OUT_BOUNCE))
                .build(30);

Available for translation, scale, rotation, overlay color, overlay intensity, block state, and item stack. You can also use inheritAll() to inherit every property.

Per-Frame Modifiers

For motion that isn't easily expressed as keyframes - like orbiting, wobble, or constant spin - use onFrame*. These run after the keyframed value is evaluated each frame, so they combine with whatever the channel is already doing:

.onFrameTranslation((translation, ctx) -> {
    float angle = ctx.interpolatedTicks() * 0.1f;
    translation.x += (float) Math.cos(angle) * radius;
    translation.z += (float) Math.sin(angle) * radius;
})

Always scale by interpolatedTicks(), not a raw tick counter. This keeps motion speed consistent regardless of framerate.

Available for translation, scale, rotation, overlay color, and overlay intensity (onFrameTranslation, onFrameScale, onFrameRotation, onFrameOverlayColor, onFrameOverlayIntensity).

Overlays

Overlays tint a block with a color and intensity, layered on top of the block's normal rendering. Like translation/scale, overlay color and intensity are interpolated channels — they keyframe and blend smoothly:

.overlay(1f, 0f, 0f, 0f, o -> o // start: red, 0 intensity (invisible)
        .addColorKeyframe(0.25f, new Vector3f(1f, 0f, 0f))
        .addIntensityKeyframe(0.25f, 0.8f, EasingType.OUT_QUAD)
        .addColorKeyframe(0.5f, new Vector3f(0f, 1f, 0f))
        .addColorKeyframe(0.75f, new Vector3f(0f, 0f, 1f))
        .addIntensityKeyframe(1f, 0f, EasingType.IN_QUAD))

Intensity controls how opaque the tint is - 0 is invisible, 1 is fully opaque. Overlays are commonly timed alongside block state keyframes to mask the instant snap between block types (see Block State & Item Stack Channels) with what reads as a smooth color transition.

Items are not currently supported by overlays. Overlay tinting only works on the block state channel.

Rotation Pivot

By default, rotation pivots around the center of the block/item, using a pivot of (0.5, 0.5, 0.5). You can change this with rotationPivot:

.rotationPivot(0, 0, 0)  // pivot around a corner instead of the center

This is useful for effects like a door swinging on a hinge, or anything that should rotate around a point other than its own center.

Entity Binding

Bind an entity to follow another entity, optionally with an offset in local or global space:

VfxEntity trail = VfxEntity.createBoundTo(level, projectile, new Vector3f(), /* localSpace */ true);
trail.setOnBoundEntityRemoved(VfxEntity::stopAnimations);

localSpace = true rotates the offset along with the bound entity's facing direction. Useful for things like weapon trails or effects that should stay oriented relative to the entity they're attached to. false keeps the offset fixed in world space.

setOnBoundEntityRemoved lets you react when the bound entity dies or is removed. Commonly used to stop or transition the VFX entity's animation at that point.

Since VoxelFX is client-only, you may want to create a purely visual projectile or something, without having access to an entity that was initialized from the server. This can be done by creating an entity, and rather than adding it to the client level, manually tick it. See VfXDemos or NovaBombDemo for how this is done.

Snapshots

You can manually capture an entity's current rendered state. Useful when building a follow-up animation that needs to branch off from wherever the entity currently is, outside of the normal queue/inheritance flow:

VfxSnapshot snap = entity.captureCurrentSnapshot();

This is how effects like shard fly-outs in NovaBombDemo work — capturing the exact position of an orbiting entity at the moment of detonation, then building a new animation that starts from that captured position.

Common Utilities

The VfxUtils class provides some commonly used math stuff, like forEachPointOnSphere, or options for getting random values. Use these to help reduce boilerplate.

There is also the ClientTaskScheduler which can be used to schedule tasks that run on the client tick event. Useful for more complex effects. NovaBombDemo uses task scheduling a decent bit.

Lastly, the EffectPresets class has (currently only 1) presets that can easily be customized, for common effects like shockwaves. Simply define the effect's config and let it do the work for you.

Common Issues & Limitations

  • Culling: VFX entities all have culling disabled by default. You can reenable culling by calling setAffectedByCulling(true) on the entity. It may not be desirable for fast-moving or large-scale effects. If you want culling enabled but with a larger bounding box (rather than fully disabling it), use setCullingRadius(radius) to inflate the box used for the culling check.

  • Persistence: VFX entities despawn automatically once their animation (and queue) finishes. Use setInfinitePersist(true) for entities you intend to control manually forever (e.g. bound to a projectile, or polled in onTick), and remember to call discard() or stopAnimations() on them yourself when done. You can also control how long an entity lingers after its animation completes with setTicksToPersist(ticks).

  • onTick vs per-frame modifiers: entity.setOnTick(...) runs every tick regardless of whether an animation is playing, and is the right place for logic like polling a bound entity's state. Per-frame modifiers (onFrameTranslation, etc.) are scoped to a single animation and only run while that animation is active.

  • Overlay limitations: overlays don't currently support items, only block states.

  • Reversing with a queue: see the note under Playback Control above.

  • BlockState / ItemStack channels are discrete, not interpolated: see Block State & Item Stack Channels.