Armor and Attachments
The attachment system is for cases where you do not want to replace the whole entity renderer. A player, zombie, cow, or custom living entity can keep its normal vanilla renderer, while PulseLib draws extra animated model parts on selected ModelPart anchors.
This is useful for custom armor, tails, backpacks, masks, equipment on arms, or other accessories. Definitions can be tied to an item stack, registered globally for living entities, hide vanilla armor for a slot, and render first-person arm attachments.
Important classes:
PLivingAttachmentDefinitionPLivingAttachmentsPLivingAttachmentSourcePLivingAttachmentSourcesPAttachmentBindingPAttachmentAnchorPAttachmentAnchorResolversPTransformPHumanoidAnchorsPHumanoidBindingsPLivingAttachmentLayerPHumanoidAttachmentLayerPArmorClientExtensions
PulseLib adds attachment render layers for players and living entities automatically through PLibArmorHandler. Attachment definitions should be contributed on the mod event bus through PulseLibEvents.AttachmentRegistrationEvent.
Armor item example¶
Armor is the most direct use case: register a living attachment definition for the armor item, say which equipment slot owns it, and map PulseLib bones to humanoid anchors.
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public final class ExampleClientEvents {
@SubscribeEvent
public static void registerAttachments(PulseLibEvents.AttachmentRegistrationEvent event) {
event.registration().registerLiving(MyItems.EXAMPLE_HELMET.get(),
new PLivingAttachmentDefinition(
new PModelData.Builder(
Identifier.fromNamespaceAndPath("examplemod", "armor/example_helmet"),
"entity").build(),
PLivingAttachmentSources.equipmentSlot(EquipmentSlot.HEAD),
List.of(PHumanoidBindings.head("helmet")),
PLivingMeshRenderResolvers.defaultLit(),
true));
}
}
For the default glTF loader this model path resolves to:
hideVanilla = true makes PArmorClientExtensions hide the vanilla armor model for matching equipment slots. Use it when the PulseLib model fully replaces the vanilla armor layer.
General attachment item¶
Attachments are not limited to armor slots. A held item can render a tail, an amulet can render on the body, or a global definition can render on an entity even without a specific item stack.
event.registration().registerLiving(MyItems.TAIL.get(),
new PLivingAttachmentDefinition(
new PModelData.Builder(
Identifier.fromNamespaceAndPath("examplemod", "attachment/tail"),
"entity").build(),
PLivingAttachmentSources.hand(),
List.of(PHumanoidBindings.bind(
PHumanoidAnchors.BODY,
"tail",
PTransform.of(
new Vector3f(0.0f, -0.2f, 0.4f),
new Vector3f(90.0f, 0.0f, 0.0f),
new Vector3f(1.0f, 1.0f, 1.0f)))),
PLivingMeshRenderResolvers.defaultLit(),
false));
PLivingAttachmentSource controls when a definition renders. Built-in factories:
PLivingAttachmentSources.anyEquipmentSlot();
PLivingAttachmentSources.equipmentSlot(EquipmentSlot.HEAD);
PLivingAttachmentSources.hand();
PLivingAttachmentSources.entityPredicate(entity -> entity.isCrouching());
PLivingAttachmentSources.equipmentSlotPredicate(EquipmentSlot.CHEST, entity -> entity.isAlive());
Global attachments use the same definition type:
event.registration().registerGlobalLiving(new PLivingAttachmentDefinition(
MODEL_DATA,
PLivingAttachmentSources.entityPredicate(entity -> entity.isInvisible()),
List.of(PHumanoidBindings.body("aura")),
PLivingMeshRenderResolvers.defaultLit(),
false));
Humanoid anchors¶
Built-in anchors from PHumanoidAnchors:
HEADHATBODYRIGHT_ARMLEFT_ARMRIGHT_LEGLEFT_LEG
Use PHumanoidBindings for common bindings:
PHumanoidBindings.head("helmet");
PHumanoidBindings.body("chest");
PHumanoidBindings.rightArm("right_arm");
PHumanoidBindings.leftLeg("left_leg");
Use PHumanoidBindings.bind(anchor, bone, transform) when you need offset, rotation, or scale. PTransform.of accepts offset, Euler rotation in degrees, and scale vectors.
Custom anchors¶
Register anchors for custom vanilla EntityModel classes through PAttachmentAnchorResolvers:
public static final PAttachmentAnchor SHELL =
PAttachmentAnchor.of(Identifier.fromNamespaceAndPath("examplemod", "shell"));
public static void registerAnchors() {
PAttachmentAnchorResolvers.register(MyEntityModel.class, SHELL,
(entity, model) -> ((MyEntityModel<?>) model).shell);
}
Custom mesh rendering¶
Each definition takes a PLivingMeshRenderResolver. The resolver receives the living entity, stack, baked bone, baked mesh, inherited render context, and partial tick.
PLivingMeshRenderResolver resolver = (entity, stack, bone, mesh, inherited, partialTick) -> {
int color = stack.hasFoil() ? 0xFF80FFFF : inherited.color();
return new PMeshRenderContext(
inherited.renderType(),
color,
inherited.packedLight(),
inherited.packedOverlay());
};
Use PLivingMeshRenderResolvers.defaultLit() or inherited() when you do not need per-mesh overrides.
Animated attachments¶
PLivingAttachmentDefinition can supply standalone controllers. This is useful when the attachment is not itself an item/entity/block entity implementing PAnimatable.
PLivingAttachmentDefinition.ControllerProvider provider = (entity, stack, model, partialTick) -> {
PAnimationController<MyAttachmentAnimatable> controller =
new PAnimationController<>("idle", state -> {
state.controller().play(IDLE);
return ControllerState.PLAY;
});
controller.tick(MY_ATTACHMENT_ANIMATABLE, 1, model);
return List.of(controller);
};
For real items, cache controllers per entity or stack instead of creating a new controller every frame.