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.
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
HEADslot - A cape, backpack, decorative chestplate on the
CHESTslot - Custom leggings on the
LEGSslot - 3D boots on the
FEETslot - 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
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:
@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.
EriCosmetics.preInit() in your FMLPreInitSince 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();
}
}
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.
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
| Method | Default | Description |
|---|---|---|
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) | null | Tooltip name. If null, uses the translation key. |
creativeTab(CreativeTabs) | null | Creative tab. If null, the item does not appear in any tab. |
scale(float) | 1.0f | Uniform 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) | true | If 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:
modIdis null or emptyregistryNameis null or emptyslotis null or not one of the 4 allowed slotsmodelis 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.
| Slot | Primary bone | Additional bones recognized (multi-bone) |
|---|---|---|
HEAD | bipedHead | (none) |
CHEST | bipedBody | bipedRightArm, bipedLeftArm |
LEGS | bipedRightLeg | bipedLeftLeg |
FEET | bipedRightLeg | bipedLeftLeg |
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 name | Target vanilla bone |
|---|---|
head | bipedHead |
body | bipedBody |
right_arm, rightarm | bipedRightArm |
left_arm, leftarm | bipedLeftArm |
right_leg, rightleg | bipedRightLeg |
left_leg, leftleg | bipedLeftLeg |
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
"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):
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.
| Bone | Default 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 thebipedHeadbone frame - The vanilla head extends from
y=-0.5(neck) toy=0(top) in bone local.y=0.21875is 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 atz=-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);
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 toassets/mod/models/item/name.json. - Look at the logs on startup:
EriAPI - ArmorCosmeticManager: attached cosmetic layer to N player renderersmust appear. IfN=0, yourClientProxyruns 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 -> ArmorCosmeticDefinitionlookup 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.