Skip to content

Getting Started

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

Getting Started with VoxelFX

1. Add the dependency

VoxelFX is published via JitPack. Add the repository and dependency to your build.gradle:

repositories {
    maven { url = 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.StainlessStasis:VoxelFX:MOD_VERSION-MINECRAFT_VERSION'
}

Replace the placeholders with their respective values. E.g. 1.0.0-26.1.2.

2. Building An Animation

Call VfxAnimationBuilder.create() to get a new builder, then chain methods to set properties of the animation. For example, to create an infinitely looping, spinning block:

VfxAnimation animation = VfxAnimationBuilder.create()
  .blockState(Blocks.DIAMOND_BLOCK.defaultBlockState(), builder -> {})
  .scale(0.5f, builder -> {}) // constantly half scale - no additional keyframes
  .translation(builder -> builder
          .addKeyframe(0.5f, 0, 4, 0, EasingType.LINEAR) // move up 4 blocks
          .addKeyframe(1f, 0, 0, 0, EasingType.LINEAR)) // move back down
  .loopInfinite()
  .build(80); // animation length defined in ticks

For more examples, see VfxDemos and NovaBombDemo.

3. Playing Animations

Animations are played from a VfxEntity, which is obtained via VfxEntity.create(level, position). Once you create the entity, you don't need to set its position or do anything else.

Do not add the entity to the level! VFX entities are automatically added to a cache (VfxEntityCache) and ticked/rendered there. Adding the entity to the level can break things or even cause crashes.

To play an animation, simply call playAnimation(animation). You can also use playAnimation(animation, progressOffset) to start it mid-animation, where the offset is from 0-1. There is also playAnimationWithOffset which accepts seconds (float) or ticks (int).

These methods will immediately start the animation, overriding the currently playing one. To queue animations so that they play one after another, use playOrQueueAnimation(animation). Play two animations in sequence like so:

VfxAnimation animation1 = VfxAnimationBuilder.create()
                .blockState(Blocks.DIAMOND_BLOCK.defaultBlockState(), builder -> {})
                .scale(0.5f, builder -> {})
                .translation(builder -> builder
                        .addKeyframe(0.5f, 0, 4, 0,  EasingType.LINEAR)
                        .addKeyframe(1f, 0, 0, 0,  EasingType.LINEAR))
                // removed the infinite loop here
                .build(80);
VfxAnimation animation2 = VfxAnimationBuilder.create()
                .blockState(Blocks.NETHERITE_BLOCK.defaultBlockState(), builder -> {})
                .scale(0.5f, builder -> builder
                        .addKeyframe(0.5f, 1f, EasingType.LINEAR)) // scale to 1x (normal) size half way through
                .translation(builder -> builder
                        .addKeyframe(0.5f, 0, 4, 0, EasingType.LINEAR)
                        .addKeyframe(1f, 0, 0, 0, EasingType.LINEAR))
                .build(80);
        VfxEntity vfxEntity = VfxEntity.create(level, pos);
        vfxEntity.playOrQueueAnimation(animation1);
        vfxEntity.playOrQueueAnimation(animation2);

4. Advanced

Looping

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

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:

.onKeyframeReached(0.5f, e -> level.playLocalSound(...))
.onLoop(e -> doSomething())
.onEnd(e -> doSomethingElse())

Playback Control

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);

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:

VfxAnimation squish = VfxAnimationBuilder.create()
        .inheritTranslation()
        .inheritBlockState()
        .scale(1f, s -> s
                .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().

Per-Frame Modifiers

For motion that isn't easily expressed as keyframes - orbiting, wobble, 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.

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);

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();

Things To Consider

  • Culling: VFX entities all have culling disabled by default. You can reenable culling by calling setAffectedByCulling on the entity, but be warned - it may not be desirable.

  • 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 discard() or stopAnimations() them yourself when done. You can also set the ticks to persist with setTicksToPersist.

  • 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.

  • 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.

Clone this wiki locally