Welcome to the blockbench-import-library (BIL) wiki!

Documentation for version 1.1.6 and above.

blockbench-import-library (short BIL) is a library to help create serverside content with custom models and animations, without requiring client mods.

Custom Models and textures will be generated automatically - the resulting resourcepack is hosted through polymer autohost, no need to handle resourcepacks yourself!

With BIL you are able to load Blockbench and Animated Java files - it acts as a bridge between Blockbench and serverside content (like entities and blocks) for fabric, written using Polymer, allowing you to combine fancy custom models, textures and animations with completely custom block / mob logic.

Check out the other pages in the wiki to learn more!

Concepts:

I will try to give a short summary of the concepts used in this library;
As a TLDR of sorts

AnimatedHolder

BIL uses Polymer's ElementHolder concept, which acts as a container for visual and interactive elements (and a bit more). This library extends this by providing specialized interfaces and implementations for animated models in various scenarios:


AnimatedEntityHolder extends AnimatedHolder:

LivingEntityHolder: Easiest to use for living entities. It automatically rotates nodes prefixed with "head" based on the entity's head rotation

LivingEntityHolderWithRideOffset: As above but with a custom ride offset

InteractableEntityHolder: Could be used for very basic entities, like different minecarts, boats or other custom entities like sleds

SimpleEntityHolder: As above but without the interaction


AnimatedHolder:

PositionedHolder: Can be used with polymers BlockWithElementHolder or with Block entities to make custom chests or other animated blocks


BbModel:

This class represents a POJO (Plain Old Java Object) interpretation of a blockbench file. It serves as a structured representation of the data within the blockbench file.

Model:

This class acts as a simplified representation specifically designed for the animation engine

Animation:

  • Holds all matrices for all bones to play in a list. These matrices define the transformation (position, rotation, scale) of each bone at each frame (at 50ms intervals, or 1 tick)

DynamicAnimation (todo): This future feature aims to dynamically sample keyframes and calculate transformations based on the scenegraph hierarchy. Performance impact might be high

Effect Keyframes

Effect keyframes extend the functionality beyond basic animation. They allow for effects such as:

  • Playing sounds
  • Switching between variants.
  • Executing commands

Commands

Commands can be run as effect keyframes. When using the Animated Java blockbench plugin, commands enable you to conditionally trigger sounds, execute other commands, and switch variants based on specific criteria, using commands, within the animation.

Variants

Animated Java exclusive feature; It allows to dynamically switch the model's texture

Merge these into your build.gradle file to add the blockbench-import-library as dependency.
blockbench-import-library requires certain Polymer modules aswell, which are included here.

repositories {
    maven { url 'https://maven.tomalbrc.de' } // blockbench-import-library
    maven { url 'https://maven.nucleoid.xyz' } // Polymer
}

dependencies {
    modImplementation include("de.tomalbrc:blockbench-import-library:[BIL_VERSION]")

    modImplementation "eu.pb4:polymer-core:[POLYMER_VERSION]"
    modImplementation "eu.pb4:polymer-networking:[POLYMER_VERSION]"
    modImplementation "eu.pb4:polymer-resource-pack:[POLYMER_VERSION]"
    modImplementation "eu.pb4:polymer-virtual-entity:[POLYMER_VERSION]"

    // Useful for automatically handling resourcepacks in dev environment, but not required.
    modRuntimeOnly "eu.pb4:polymer-autohost:[POLYMER_VERSION]"
}

It is important to make sure these dependencies are available at runtime.
You can do this by either adding these modules to your dependencies in fabric.mod.json, or by including them in your jar file as Jar-in-Jar.

"blockbench-import-library": "*",
"polymer-core": "*",
"polymer-networking": "*",
"polymer-virtual-entity": "*",
"polymer-resource-pack": "*"

1. Creating a model

To create a model you can use, you will first need to install Blockbench. You can optionally install the Animated Java Blockbench Plugin - it offers a few more features such as variants and conditional effect keyframes (Sounds, Commands, Variants).
In Blockbench you can either choose a Generic Entity Model, or an Animated Java Rig.
From there you can create the model. If you aren't familiar with Blockbench or Animated Java yet, you can find the Blockbench Wiki here and AnimatedJava documentation here.

Once you have finished the model, you can just save the .bbmodel or .ajmodel\

2. Adding the model to mod resources

To make the model available ingame, it needs to be included in your mod's resources folder.
Models in mod resources are referenced by identifier, in format namespace:path.

Add the .bbmodel or .ajmodel file - the model - to /resources/model/<namespace>/<path>.json. This is the file that will be parsed by BIL on the server and a resourcepack will be generated. The models will have to be loaded before the world is loaded, otherwise the resourcepack generation has to be started manually again on order for the models to be available in the resourcepack

Notes

  • Head bones of mobs should have their names starting with head, so BIL can recognize it for head rotations.
  • Child bone transformations of head bone rotations currently have a flaw, this will be addressed soon!

1. Loading the model

To load a model file into the actual game, you can use the utility methods in the BbModelLoader and AjModelLoader classes.
Models required in mods should be retrieved by ID, but it's also possible to load files from the server.
Note that these models are not cached by BIL. They are safe to reuse and should be cached when possible

public static final Model MODEL_FROM_ID = BbModelLoader.load(new ResourceLocation("namespace", "path"));
public static final Model MODEL_FROM_FILEPATH = BbModelLoader.load("file/path/example.bbmodel");

These Model objects can be used with BIL's custom implementations of Polymer's ElementHolder.
Anything extending AbstractAnimatedHolder supports it.

2. Entity Attachments

BIL models can be attached to entities.
Writing a custom entity with model works mostly the same as writing one normally.
You just have to implement AnimatedEntity and initialize an EntityHolder in the constructor with the model.

public class RedstoneGolem extends Monster implements AnimatedEntity {
    public static final ResourceLocation ID = new ResourceLocation("bil", "redstone_golem");
    public static final Model MODEL = BbModelImporter.load(ID);
    private final EntityHolder<RedstoneGolem> holder;

    public RedstoneGolem(EntityType<? extends Monster> type, Level level) {
        super(type, level);

        // Creates model holder with out of the box support for most LivingEntity features.
        // Note that it is always possible to write your own or override some of their methods.
        this.holder = new LivingEntityHolder<>(this, MODEL);

        // Attaches the holder to this entity in Polymer.
        // Make sure that ticking is enabled, as it is required for model updates.
        EntityAttachment.ofTicking(this.holder, this);
    }

    @Override
    public EntityHolder<RedstoneGolem> getHolder() {
        return this.holder;
    }
}

3. Other Attachments

While BIL is mostly targeted towards entities, it is possible to attach model holders to other things.
Polymer provides a few of its own other attachments, like ChunkAttachment and BlockBoundAttachment.
You can attach your model holder those aswell.

4. Variants and Animations

One thing BIL can't automatically do for you is decide when certain animations or variants should be used.
To do this, every holder provides an Animator and VariantController, which can for example be used like this:

Animator animator = holder.getAnimator();

// Completely stops the animation named 'idle'.
animator.stopAnimation("idle");

// Pauses the animation named 'walk'.
animator.pauseAnimation("walk");

// Plays the melee animation with priority 10.
animator.playAnimation("melee", 10); 

VariantController controller = holder.getVariantController();

// Sets the variant to the variant named 'hurt' if the current variant is default.
if (controller.isDefaultVariant()) {
    controller.setVariant("hurt");
}

Locators

Locators are nodes you can add to your model when using the AnimatedJava Blockbench Plugin.
They are very useful for tracking the position of a specific place on the model.
Example uses cases of this are adding hand items, displaying particles and many other things.

BIL has support for these locators, although they work a little bit different here.
AnimatedJava has an Entity Type field for locators, placing that entity on the locator position, this field does not get used here.
With BIL, locators work by registering listeners to it that receive updates when the locators' transform changes.

You can write your own listeners to do exactly what you want, but BIL also provides a few for entity elements.

ElementUpdateListener

Allows you to add any GenericEntityElement and updates its position based on the locators transform.

Locator locator = holder.getLocator("<locator_name>");
GenericEntityElement element = new GenericEntityElement() {
  @Override
  protected EntityType<? extends Entity> getEntityType() {
    return EntityType.COW;
  }
};

// Adds listener for our simple cow element.
locator.addListener(new ElementUpdateListener(element));

// Adds the element to the holder and sends it to nearby clients.
holder.addElement(element);

DisplayElementUpdateListener

Allows you to add any DisplayElement and applies the locators transform on it.

Locator locator = holder.getLocator("<locator_name>");
DisplayElement displayElement = new ItemDisplayElement();

// Puts the display element in a wrapper. This wrapper stores certain information about the element, like whether it is a head element.
// Head elements can transform differently for mobs, as their head rotation can be different from their body rotation.
DisplayWrapper<?> display = new DisplayWrapper<>(displayElement, locator, isHeadElement);
locator.addListener(new DisplayElementUpdateListener(display));

// We initialize the display entity before adding it, to make sure the transform is applied before it gets sent to the client.
holder.initializeDisplay(display);

// Custom method for adding additional display elements. This tells the holder to mount the display as a passenger.
// This internally calls holder#addElement, and should be used instead of it for display elements registered with this listener.
holder.addAdditionalDisplay(displayElement);

Notes

  • When a locator has 0 listeners, it won't have to update. Remove listeners you aren't using for best performance.
  • Locator updates can be called async from a non-server thread. Take this into account when writing your own listeners.

Commands

  • /bil model create id<.ajmodel>|filepath <model>

    Spawns a model ingame based on mob identifier or a file path (from server root folder) to the model json file. These models are not saved and are mostly intended for testing. If you want load an .ajmodel file, make sure to append .ajmodel to either the id or file path.

  • /bil model <targets> animation|variant|scale <args>

    Modifies the model of any entity selected in that has a custom model. Allows you to temporarily change the scale of the model, update the variant and play / pause / stop animations. This is also mostly intended for testing and playing with the models.

Example of a hostile entity (mojang mappings)

import ...

public class Snake extends Monster implements AnimatedEntity {
    public static final ResourceLocation ID = Util.id("snake");
    public static final Model MODEL = AjModelLoader.load(ID); // For .ajmodel files, use AjModelLoader, otherwise BbModelLoader
    private final EntityHolder<Snake> holder; // use a BIL based ElementHolder subclass specifically for entities 

    public static AttributeSupplier.Builder createAttributes() {
        return Mob.createMobAttributes()
                .add(Attributes.ATTACK_DAMAGE, 2.0)
                .add(Attributes.MAX_HEALTH, 8.0)
                .add(Attributes.MOVEMENT_SPEED, 0.4);
    }

    public static boolean checkSnakeSpawnRules(EntityType<? extends Monster> type, ServerLevelAccessor level, MobSpawnType spawnType, BlockPos pos, RandomSource random) {
        return level.canSeeSky(pos) && checkMonsterSpawnRules(type, level, spawnType, pos, random);
    }

    @Override
    public EntityHolder<Snake> getHolder() {
        return this.holder;
    }

    public Snake(EntityType<? extends Snake> type, Level level) {
        super(type, level);

        this.moveControl = new MoveControl(this);
        this.jumpControl = new JumpControl(this);

        this.holder = new LivingEntityHolder<>(this, MODEL); // create a holder for living entities, for head rotations and other features
        EntityAttachment.ofTicking(this.holder, this); // attach the holder to this entity using a polymer EntityAttachment
    }

    @Override
    protected void registerGoals() {
        this.goalSelector.addGoal(0, new FloatGoal(this));
        this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.0, true));
        this.goalSelector.addGoal(2, new LookAtPlayerGoal(this, Player.class, 2.0F));
        this.goalSelector.addGoal(3, new WaterAvoidingRandomStrollGoal(this, 0.59));
        this.goalSelector.addGoal(4, new RandomLookAroundGoal(this));

        this.targetSelector.addGoal(1, new HurtByTargetGoal(this, Snake.class));
        this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
        this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, Chicken.class, true));
    }

    @Override
    public void tick() {
        super.tick();

        if (this.tickCount % 2 == 0) {
            AnimationHelper.updateWalkAnimation(this, this.holder); // util methods, see below
            AnimationHelper.updateHurtVariant(this, this.holder); // util methods
        }
    }

    @Override
    public boolean doHurtTarget(Entity entity) {
        boolean result = super.doHurtTarget(entity);

        if (result) {
            if (entity instanceof LivingEntity livingEntity && this.random.nextInt(5) == 1) {
                livingEntity.addEffect(new MobEffectInstance(MobEffects.POISON, level().getDifficulty().getId() * 2 * 20, 1));
            }
        }

        return result;
    }
}

AnimationHelper implementation:

public class AnimationHelper {

    public static void updateWalkAnimation(LivingEntity entity, AnimatedHolder holder) {
        updateWalkAnimation(entity, holder, 0);
    }

    public static void updateWalkAnimation(LivingEntity entity, AnimatedHolder holder, int priority) {
        Animator animator = holder.getAnimator();
        if (entity.walkAnimation.isMoving() && entity.walkAnimation.speed() > 0.02) {
            animator.playAnimation("walk", priority);
            animator.pauseAnimation("idle");
        } else {
            animator.pauseAnimation("walk");
            animator.playAnimation("idle", priority, true);
        }
    }

    public static void updateHurtVariant(LivingEntity entity, AnimatedHolder holder) {
        updateHurtColor(entity, holder); // if you are using animated java, you could change to a different variant or use a color like we do here 
    }

    public static void updateHurtColor(LivingEntity entity, AnimatedHolder holder) {
        if (entity.hurtTime > 0 || entity.deathTime > 0)
            holder.setColor(0xff7e7e);
        else
            holder.clearColor();
    }
}