Cosmetic

Render a Blockbench 3D model on any player armor slot. The model naturally follows vanilla bones (head tilt, sneak, walk cycle, arm swing) without a single line of OpenGL.

fr.eri.eriapi.cosmetic Client + Server Forge 1.12.2 v1.8.0

The Cosmetic module solves a classic 1.12.2 modding problem: displaying a 3D Blockbench model on a player without writing a custom LayerRenderer per item, without touching the vanilla render pipeline, and without breaking the equipped armor.

The builder creates a regular ItemArmor with a fully transparent armor texture (the vanilla layer is invisible) then attaches a shared LayerRenderer that draws your Blockbench model on top, hooked to the right player bone.

What you can do

  • A mask, helmet, crown, hat on the HEAD slot
  • A cape, backpack, decorative chestplate on the CHEST slot
  • Custom leggings on the LEGS slot
  • 3D boots on the FEET slot
  • A full set with multi-bone dispatch (each part follows its vanilla bone: arms, body, legs)

Quick start in 3 steps

1. Design the model in Blockbench

Use the Blockbench « Item / Modded Block » preset (or « Modded Entity » if you want multi-bone). Recommended standard size for a helmet: a cube centered on (8, 28, 8) in Blockbench pixels. Save the JSON model to:

assets/yourmod/models/item/barry_mask.json

And the associated textures in:

assets/yourmod/textures/blocks/barry_mask/<textureName>.png

2. Create the item with the builder

Java — CommonProxy or init registry
import fr.eri.eriapi.cosmetic.EriCosmetics;
import net.minecraft.inventory.EntityEquipmentSlot;

Item barryMask = EriCosmetics.armor()
    .modId("eriniumfaction").registryName("barry_mask")
    .slot(EntityEquipmentSlot.HEAD)
    .displayName("Barry Mask")
    .creativeTab(MyCreativeTab.INSTANCE)
    .model("eriniumfaction:item/barry_mask")
    .build();

3. Register the item with the Forge registry

The builder does NOT auto-register with the Forge registry (same convention as EriItem). You must do it in your registry handler:

Java — @SubscribeEvent register
@SubscribeEvent
public static void registerItems(RegistryEvent.Register<Item> event) {
    event.getRegistry().register(barryMask);
}

On the client side, do nothing more — the layer renderer is attached automatically by EriAPI on each skin variant's RenderPlayer (default + slim) during init.

Recommended (v1.8.5+): call EriCosmetics.preInit() in your FMLPreInit

Since EriAPI 1.8.5, you can call EriCosmetics.preInit() from your ClientProxy's FMLPreInitializationEvent. This explicitly registers the ModelBakeEvent listener that installs the ArmorCosmeticBakedModel. Optional — EriAPI already does it automatically from its own preInit, but the explicit call documents the dependency and stays valid if a future EriAPI version removes the implicit registration.

@SideOnly(Side.CLIENT)
public class ClientProxy extends CommonProxy {
    @Override
    public void preInit(FMLPreInitializationEvent e) {
        super.preInit(e);
        EriCosmetics.preInit();
    }
}
3D rendering in every context (since v1.8.2)

Since EriAPI 1.8.2, the Cosmetic module renders the Blockbench model in 3D everywhere: inventory, hotbar, first person, third person, ground, fixed (item frame), head. No item/generated model to create — the ArmorCosmeticBakedModel is generated automatically in ModelBakeEvent and delegates the geometry to ArmorCosmeticTEISR. The Blockbench JSON's display block (keys gui, firstperson_*, thirdperson_*, ground, fixed, head) is honored automatically per context — Blockbench-pixel translations are divided by 16 to match the vanilla OpenGL transform convention.

Textures: no dimension constraints

Rendering goes through TextureManager.bindTexture() (via SimpleTexture), bypassing the item atlas entirely. The result: any PNG dimension works (POT not required, non-square dimensions allowed, e.g. 121x152). Multi-texture is supported (each key in the JSON's textures map can point to a different PNG) and the custom UVs per face are honored verbatim — convenient for complex Blockbench models with non-standard texture_size.

Builder API

Entry point: EriCosmetics.armor() returns an ArmorCosmeticBuilder.

Fluent methods

MethodDefaultDescription
modId(String)Registry namespace. Required.
registryName(String)Registry path. Required. Final id: modId:registryName.
slot(EntityEquipmentSlot)HEAD, CHEST, LEGS or FEET. Required.
model(String)Blockbench id ("mod:item/name"). Required.
displayName(String)nullTooltip name. If null, uses the translation key.
creativeTab(CreativeTabs)nullCreative tab. If null, the item does not appear in any tab.
scale(float)1.0fUniform scale around the bone origin.
offset(float x, float y, float z)(0, 0, 0)Extra translation in GL blocks (= Blockbench pixels / 16), applied AFTER the default bone offset.
rotation(float x, float y, float z)(0, 0, 0)Euler rotation in degrees around the bone origin, in order X then Y then Z.
followBones(boolean)trueIf true, Blockbench groups whose names match a vanilla bone are rendered at that bone (see next section). Otherwise everything goes to the slot's primary bone.
build()Creates and returns the Item. You must then register it with the Forge registry.

Validation at build()

build() throws IllegalStateException if:

  • modId is null or empty
  • registryName is null or empty
  • slot is null or not one of the 4 allowed slots
  • model is null or empty

Bone mapping per slot

Each armor slot points to a primary bone. If your Blockbench model has only one group (or several groups without specific names), everything is rendered to that bone.

SlotPrimary boneAdditional bones recognized (multi-bone)
HEADbipedHead(none)
CHESTbipedBodybipedRightArm, bipedLeftArm
LEGSbipedRightLegbipedLeftLeg
FEETbipedRightLegbipedLeftLeg
i
Why does FEET use the leg bone?

In 1.12.2, vanilla boots are drawn on the bipedRightLeg / bipedLeftLeg bone, not on a dedicated « foot » bone. The module follows the same convention — your 3D boots move with the leg.

Multi-bone models

For a cosmetic that covers multiple body parts (e.g. chestplate + sleeves), name your Blockbench groups with the following conventions (case-insensitive, spaces and underscores accepted):

Blockbench group nameTarget vanilla bone
headbipedHead
bodybipedBody
right_arm, rightarmbipedRightArm
left_arm, leftarmbipedLeftArm
right_leg, rightlegbipedRightLeg
left_leg, leftlegbipedLeftLeg

Each root group whose name matches is rendered at its bone. Groups with non-matching names fall back to the slot's primary bone.

Example: a chestplate with sleeves that follow the arms

Blockbench JSON structure (summary)
"groups": [
  { "name": "body",      "children": [...] },
  { "name": "right_arm", "children": [...] },
  { "name": "left_arm",  "children": [...] }
]

Result: body is rendered on the torso, right_arm / left_arm follow the player's arms (and so the arm-swing animation during walk, attack motion, etc.).

Disabling multi-bone dispatch

If you want EVERYTHING on the primary bone (e.g. a conical hat with an inner group named « body » for the support mesh), use .followBones(false):

Java
EriCosmetics.armor()
    .modId("mymod").registryName("conical_hat")
    .slot(EntityEquipmentSlot.HEAD)
    .model("mymod:item/conical_hat")
    .followBones(false)        // everything goes to bipedHead
    .build();

Default offsets & Blockbench coordinates

When the module draws your model, it first calls bone.postRender(0.0625f) on the target bone (places the OpenGL matrix at the bone pivot with its rotation applied — same pipeline as vanilla armor), then applies a default offset that brings the Blockbench origin (0, 0, 0) to the expected location in the bone's coordinate frame.

BoneDefault offset (x, y, z) in GL blocks
bipedHead(-0.5, -1.5, -0.5)
bipedBody(-0.5, -1.5, -0.5)
bipedRightArm(-0.5, -1.5, -0.5)
bipedLeftArm(-0.5, -1.5, -0.5)
bipedRightLeg(-0.5, -0.75, -0.5)
bipedLeftLeg(-0.5, -0.75, -0.5)

Visual checkpoints

Design your model in Blockbench starting from these reference points:

  • Helmet: center the head on (8, 28, 8) in pixels — the cube center lands right on the vanilla head.
  • Chestplate: same reference as the helmet (centered on (8, 18, 8)) since the body bone shares the same Y pivot as the head.
  • Legs: model each leg centered on (8, 6, 8), height 12px (from y=0 to y=12).

The builder's .offset() adds on top of the default offset — use it for fine adjustments (e.g. if your helmet should be slightly forward: .offset(0, 0, -0.05f)).

Calculation example

For a mask whose front face is drawn in Blockbench from [4,23,3.9] to [12,32,3.9] — center (8, 27.5, 3.9):

  • Conversion to blocks: (0.5, 1.71875, 0.24375)
  • After the offset (-0.5, -1.5, -0.5): (0, 0.21875, -0.25625) in the bipedHead bone frame
  • The vanilla head extends from y=-0.5 (neck) to y=0 (top) in bone local. y=0.21875 is slightly above the top — correctable with .offset(0, -0.22f, 0) if needed.
  • The vanilla head's front face is at z=-0.25. The mask lands at z=-0.256 — right in front of the head.

BlockbenchRenderer (public helper)

Internally, the Cosmetic module uses BlockbenchRenderer, a helper that renders an already-parsed AnimatedBlockModel with or without an AnimationPose. This helper was extracted from AnimatedBlockTESR and is public — you can use it for your own custom render systems (TileEntity, in-hand items, 3D GUI, etc.).

API

// Full model render in static pose, auto-bind textures
BlockbenchRenderer.renderModel(AnimatedBlockModel model);

// Render with an interpolated AnimationPose
BlockbenchRenderer.renderModel(AnimatedBlockModel model, AnimationPose pose);

// Full control (caller binds textures manually)
BlockbenchRenderer.renderModel(AnimatedBlockModel model, AnimationPose pose, boolean bindTextures);

// Render a single group at the current position
BlockbenchRenderer.renderGroup(ModelGroup group);
BlockbenchRenderer.renderGroup(ModelGroup group, AnimationPose pose);
BlockbenchRenderer.renderGroup(ModelGroup group, AnimationPose pose,
                               Map<String, ResourceLocation> textures, boolean bindTextures);

// Variant with texture overrides (used by AnimatedBlockTESR)
BlockbenchRenderer.renderModelWithOverrides(AnimatedBlockModel model, AnimationPose pose,
                                            Map<String, String> textureOverrides);
!
OpenGL state

The helper assumes the GL state is already prepared: matrices in place, blending / culling / lighting set up, lightmap correct. It does not push / pop the root matrix and does not reset color or blend mode. Wrap the call in pushMatrix() / popMatrix() and configure your state before.

Troubleshooting

The model doesn't appear at all

  • Verify you registered the item with the Forge registry (the builder does not do it).
  • Check the model path: .model("mod:item/name") must point to assets/mod/models/item/name.json.
  • Look at the logs on startup: EriAPI - ArmorCosmeticManager: attached cosmetic layer to N player renderers must appear. If N=0, your ClientProxy runs before the render manager is ready — open an issue.

The vanilla helmet is still visible

The transparent texture is embedded in EriAPI under assets/eriapi/textures/models/armor/transparent_layer_1.png. If you see a white / pink helmet (missing texture), check that the EriAPI jar is in your mods/ folder (in dev: add it as compile files() in build.gradle).

The model is offset / in the wrong position

  • Verify your Blockbench model is designed in the right coordinate frame — see Default offsets.
  • Tune with .offset(x, y, z) (in GL blocks = Blockbench pixels / 16).
  • For a model imported from other software, try .rotation(0, 180, 0) — some exporters flip the Z axis.

The sleeves don't follow the arms

Check that your Blockbench groups are named exactly right_arm / left_arm (case-insensitive, underscore tolerated). The mapping looks for the exact name — a group named « ArmRight » will not be recognized.

The model flickers / disappears at certain angles

Classic cause: your model has transparent faces poorly sorted for OpenGL blend. The module enables disableCull() and enableBlend() by default, but the draw order remains that of the Blockbench JSON elements. For a clean effect, avoid overlapping multiple transparent faces — prefer a single cube with an alpha texture.

Performance

The layer is called every frame for every visible player. The cost is minimal because:

  • The Item -> ArmorCosmeticDefinition lookup is an O(1) HashMap.get().
  • The Blockbench model is cached in memory (first load only).
  • No per-frame allocations — loops use for(int i) over existing lists.
  • If the player is invisible (isInvisible()), the layer is short-circuited.