-
Notifications
You must be signed in to change notification settings - Fork 1
CatFrame JSON Model System
CatFrame brings the 1.8+ resource pack model format to Minecraft 1.7.10 — full inheritance chains, blockstate variants, display transforms, tiled textures, and all. If you're writing a mod that needs custom block or item models, this is the system you'll use.
- 1. Model Inheritance Chain
- 2. Model JSON Format
- 3. Blockstates
- 4. Model Mappings
- 5. IBlockStateProvider — Dynamic Blockstate Registration
- 6. Rendering Mod Blocks
- 7. Namespaces & Mod Extensions
- 8. Resource Directory Structure
- 9. Render Pipeline Flow
- 10. Core Classes at a Glance
Models form an inheritance chain via the parent field. Child models inherit elements, textures, and display from their parent, and can override any field by the same name.
flowchart BT
ALL["block/cube_all<br/>6 faces: texture #all"]
COL["block/cube_column<br/>Top/bottom: #end, sides: #side"]
BT["block/cube_bottom_top<br/>Top: #top, Bottom: #bottom, Sides: #side"]
IG["item/generated<br/>Overrides display"]
ALL --> CUBE["block/cube<br/>16×16×16 cube elements"]
COL --> CUBE
BT --> CUBE
CUBE --> BLOCK["block/block<br/>Root base: display + gui_light"]
IG --> BG["builtin/generated<br/>Item root: plane element + display"]
A typical block model just needs to specify its textures:
{
"parent": "block/cube_all",
"textures": {
"all": "minecraft:blocks/stone"
}
}It inherits elements from cube and display from block through the entire chain.
{
"parent": "block/cube_all",
"gui_light": "side",
"textures": {
"all": "minecraft:blocks/stone"
},
"elements": [ ... ],
"display": { ... }
}| Field | Description |
|---|---|
parent |
Parent model path, e.g. block/cube, builtin/generated
|
gui_light |
Lighting mode — "side" for 3D blocks, "front" for flat items |
textures |
Texture map — key is variable name, value is texture path or #reference
|
elements |
Geometry definitions (from/to/faces). Child value overrides parent |
display |
Per-scene render transforms |
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"rotation": { "angle": 45, "axis": "y", "origin": [8, 8, 8] },
"faces": {
"north": { "texture": "#north", "uv": [0, 0, 16, 16], "cullface": "north" }
}
}Coordinates range from 0 to 16, representing one block's volume. cullface marks which neighbour face can cull this face.
Defines how the model looks in different rendering contexts:
{
"display": {
"gui": { "rotation": [30, 225, 0], "translation": [0,0,0], "scale": [0.625, 0.625, 0.625] },
"ground": { "rotation": [0,0,0], "translation": [0,3,0], "scale": [0.25, 0.25, 0.25] },
"fixed": { "rotation": [0,0,0], "translation": [0,0,0], "scale": [0.5, 0.5, 0.5] },
"on_shelf": { "rotation": [0,180,0], "translation": [0,0,0], "scale": [1,1,1] },
"thirdperson_righthand": { "rotation": [75,45,0], "translation": [0,2.5,0], "scale": [0.375, 0.375, 0.375] },
"firstperson_righthand": { "rotation": [0,45,0], "translation": [0,0,0], "scale": [0.40, 0.40, 0.40] },
"firstperson_lefthand": { "rotation": [0,225,0], "translation": [0,0,0], "scale": [0.40, 0.40, 0.40] }
}
}| Context | Used For |
|---|---|
gui |
Inventory / GUI display |
ground |
Dropped items on the ground |
fixed |
Item frames and fixed positions |
on_shelf |
Placed on a shelf |
thirdperson_righthand |
Third-person right-hand hold |
firstperson_righthand |
First-person right-hand hold |
firstperson_lefthand |
First-person left-hand hold |
Texture values starting with # reference another texture key:
{
"textures": {
"all": "minecraft:blocks/stone",
"north": "#all",
"south": "#all"
}
}The system resolves reference chains recursively, so feel free to nest # references as deep as you like.
Blockstate files live at assets/{namespace}/blockstates/{name}.json. They determine which model gets rendered based on the block's state.
The simplest form — pick a model based on property combinations:
{
"variants": {
"normal": { "model": "block/stone" },
"facing=north": { "model": "block/furnace", "y": 0 },
"facing=east": { "model": "block/furnace", "y": 90 },
"facing=south": { "model": "block/furnace", "y": 180 },
"facing=west": { "model": "block/furnace", "y": 270 }
}
}Variant fields:
| Field | Description |
|---|---|
model |
Model path |
x |
X-axis rotation (0/90/180/270) |
y |
Y-axis rotation (0/90/180/270) |
uvlock |
Lock UVs when rotating |
weight |
Weighted random weight |
Use an array to represent multiple candidates — the system picks one based on position hash:
{
"variants": {
"normal": [
{ "model": "block/grass_block" },
{ "model": "block/grass_block", "y": 90 },
{ "model": "block/grass_block", "y": 180 },
{ "model": "block/grass_block", "y": 270 }
]
}
}Break a block into multiple parts — conditions determine which parts render together. Perfect for fences, walls, and other connecting blocks:
{
"multipart": [
{ "apply": { "model": "block/fence_post" } },
{ "when": { "north": "true" }, "apply": { "model": "block/fence_side" } },
{ "when": { "south": "true" }, "apply": { "model": "block/fence_side", "y": 180 } }
]
}- No
when→ always renders -
ORlogic:"OR": [{"north": "true"}, {"south": "true"}] - Pipe-delimited values:
"facing": "north|south"
Vanilla 1.7.10 blocks use numeric metadata to distinguish variants, but we want property keys in blockstates (e.g. axis=y,wood=oak). CatFrame provides three ways to convert metadata to properties — in recommended order:
The most flexible approach — register a mapping via lambda in preInit:
final String[] woods = {"oak", "spruce", "birch", "jungle"};
final String[] axes = {"y", "x", "z"};
VanillaModelManager.registerMetadataMapping(Blocks.log, meta -> {
Map<String, String> props = new HashMap<>();
props.put("wood", woods[meta & 3]);
props.put("axis", axes[(meta >> 2) % 3]);
return props;
});During baking the system iterates metadata 0~15, computes properties via the mapper, and matches them against variant keys in the blockstate JSON.
Placed at assets/{namespace}/metadata_map.json. Pure data mapping — no code needed:
{
"glass_pane": {
"0": {"north": "false", "east": "false", "south": "false", "west": "false"},
"1": {"north": "true", "east": "false", "south": "false", "west": "false"},
"15": {"north": "true", "east": "true", "south": "true", "west": "true"}
},
"stained_glass_pane": {
"0": {"color": "white", "north": "false", "east": "false", "south": "false", "west": "false"},
"15": {"color": "black", "north": "false", "east": "false", "south": "false", "west": "false"}
}
}Top-level keys are block registry names. Each block maps "metadataValue": {propertyMap}. The system auto-loads and registers mappers during bakeAllModels().
The original approach — use raw metadata numbers as variant keys. Compatibility-only; triggers deprecation log warnings:
{
"variants": {
"normal": { "model": "block/stone" },
"1": { "model": "block/granite" },
"3": { "model": "block/diorite" },
"5": { "model": "block/andesite" }
}
}Prefer the first two options — property keys are far more readable and align with the 1.16.5 format.
Found at assets/{namespace}/model_mappings.json. A lightweight way to map blocks and items to models without writing a full blockstate file.
{
"blocks": {
"stone": "block/stone",
"dirt": "block/dirt",
"cobblestone": "block/cobblestone"
},
"items": {
"diamond_sword": "item/diamond_sword"
}
}Key is the block/item registry name (no namespace prefix), value is the model path. This only binds metadata=0 — good for blocks with a single appearance.
For blocks with multiple variants (like logs), use the name:metadata syntax:
{
"blocks": {
"log:0": "block/oak_log",
"log:1": "block/spruce_log",
"log:2": "block/birch_log",
"log:3": "block/jungle_log"
},
"items": {
"dye:4": "item/lapis_lazuli",
"dye:15": "item/bone_meal"
}
}The number after the colon is metadata for blocks and damage for items. This way, different sub-types each get their own model.
- Blockstates > model_mappings — if a block already has a complete blockstate file, mappings won't override it
-
name:metadatasupplements won't overwrite metadata slots already baked by blockstates - Use model_mappings for simple cases, blockstates for complex ones (rotation, weighted random, multipart)
Have a block that uses the JSON model system with dynamic state? Implement IBlockStateProvider and the system loads its blockstate JSON automatically:
public class BlockModCake extends Block implements IBlockStateProvider {
@Override
public String getBlockstateNamespace() {
return "mymod";
}
@Override
public String getBlockstateName() {
return "cake";
}
@Override
public Map<String, String> getStateProperties(IBlockAccess world, int x, int y, int z, int metadata) {
Map<String, String> props = new HashMap<>();
props.put("bites", String.valueOf(metadata));
return props;
}
}Register it during preInit:
VanillaModelManager.registerBlock(myBlockInstance);At render time, the system:
- Calls
getStateProperties()to get the property map (e.g.{"bites": "3"}) - Builds a variant key:
"bites=3"(multiple properties sorted alphabetically, comma-separated) - Matches it in the blockstate JSON and renders the corresponding model
Corresponding blockstate file assets/mymod/blockstates/cake.json:
{
"variants": {
"bites=0": { "model": "block/cake" },
"bites=1": { "model": "block/cake_slice1" },
"bites=2": { "model": "block/cake_slice2" },
"bites=3": { "model": "block/cake_slice3" }
}
}Two ways to hook your mod blocks into the JSON model system:
Best for blocks that switch models based on metadata or world state. The Mixin intercepts vanilla rendering — you don't even need to touch getRenderType().
public class MyModBlock extends Block implements IBlockStateProvider {
@Override
public String getBlockstateNamespace() {
return "mymod"; // looks in assets/mymod/blockstates/
}
@Override
public String getBlockstateName() {
return "my_block"; // looks for my_block.json
}
@Override
public Map<String, String> getStateProperties(IBlockAccess world, int x, int y, int z, int metadata) {
Map<String, String> props = new HashMap<>();
props.put("type", String.valueOf(metadata));
return props;
}
}Register it in preInit:
VanillaModelManager.registerBlock(myBlockInstance);Asset structure:
assets/mymod/
├── blockstates/my_block.json
└── models/block/my_block_variant0.json
The older CatFrame registration path — uses ISimpleBlockRenderingHandler instead of Mixin. Good for blocks that need a fixed JSON model without state changes.
public class MyBlock extends Block implements IBlockJsonModel {
@Override
public int getRenderType() {
return renderType(); // provided by interface default method
}
}
// preInit client-side registration
JsonBlock.register(myBlock, false, true, true, 0, "my_model");The model file lives at assets/{modid}/textures/json/block/{name}.json.
IBlockStateProvider |
IBlockJsonModel |
|
|---|---|---|
| State changes | Yes (metadata → properties → variant) | No (fixed model) |
| Render mechanism | Mixin intercepts vanilla render | RenderingRegistry custom renderType |
| Model format | Standard 1.8+ inheritance | Proprietary JSON format |
| Blockstate file | Required | Not needed |
| Best for | Multi-state blocks (cake, facing, growth stages) | Simple decorative blocks |
New blocks? Go with IBlockStateProvider — the format is more standard and extensible.
Register your own namespace so the system picks up your models:
// Call during preInit
VanillaModelManager.registerNamespace("mymod");The system automatically scans:
assets/mymod/model_mappings.jsonassets/mymod/blockstates/*.jsonassets/mymod/models/**/*.json
ModelResolver also searches registered namespaces when resolving parent chains.
flowchart TB
ROOT["assets/{namespace}/"]
ROOT --> BS["blockstates/"]
ROOT --> M["models/"]
ROOT --> MM["model_mappings.json<br/>Quick mapping"]
ROOT --> MD["metadata_map.json<br/>Metadata→property mapping"]
BS --> BS1["stone.json"]
BS --> BS2["cake.json"]
BS --> BS3["grass.json"]
M --> BUILTIN["builtin/"]
M --> BLK["block/"]
M --> ITEM["item/"]
BUILTIN --> BG["generated.json<br/>Item root base model"]
BLK --> B1["block.json<br/>Block root: display + gui_light"]
BLK --> B2["cube.json<br/>Cube geometry"]
BLK --> B3["cube_all.json<br/>6-face same texture"]
BLK --> B4["cube_column.json<br/>Pillar: top/bottom/sides"]
BLK --> B5["cube_bottom_top.json"]
BLK --> B6["cross.json<br/>Cross: flowers/grass"]
BLK --> B7["slab.json<br/>Slab"]
BLK --> B8["stone.json<br/>Specific block model"]
ITEM --> IG["generated.json<br/>Item base: parent builtin/generated"]
ITEM --> DS["diamond_sword.json"]
flowchart TD
subgraph PREINIT["preInit"]
INIT["VanillaModelManager.init()
Load model_mappings.json for all namespaces
Load metadata_map.json for all namespaces
Load all blockstates/*.json
Load IBlockStateProvider-registered blocks
Recursively resolve parent chain
Collect required texture paths"]
REG["VanillaModelManager.registerBlock(block)
← Mod calls this"]
end
subgraph TEX_PRE["TextureStitchEvent.Pre"]
TP["Register collected textures to TextureMap"]
end
subgraph TEX_POST["TextureStitchEvent.Post"]
COLLECT["Collect IIcon references"]
BAKE["Bake all models to BakedQuad"]
end
subgraph RENDER_BLOCK["Runtime — MixinRenderBlocks"]
STATIC["Static blocks
metadata → look up baked model"]
DYNAMIC["IBlockStateProvider blocks
getStateProperties() → match variant → instant bake/render"]
end
subgraph RENDER_ITEM["Runtime — MixinRenderItem"]
ITEM["Look up baked item model → render"]
end
INIT --> REG
REG --> TP
TP --> COLLECT
COLLECT --> BAKE
BAKE --> STATIC
BAKE --> DYNAMIC
BAKE --> ITEM
| Class | Responsibility |
|---|---|
VanillaModelManager |
Orchestrator — init, texture registration, model baking, render entry points |
ModelResolver |
Resolve parent inheritance chain, merge textures/elements/display |
ModelJson |
Model JSON data structure (includes DisplayTransform) |
BlockstateJson |
Blockstate JSON data structure + custom Gson deserialiser |
BlockJsonModelBake |
Bake ModelJson elements into BakedQuad
|
JsonBlock |
Legacy custom model registration (IBlockJsonModel path) |
IBlockStateProvider |
Block interface — dynamic property mapping + blockstate rendering |
IMetadataMapper |
Functional interface — metadata int → property Map |
IBlockJsonModel |
Block interface — legacy RenderingRegistry path |
MixinRenderBlocks |
Intercept vanilla block rendering |
MixinRenderItem |
Intercept vanilla item rendering |