-
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, extension rendering pipeline, and all. If you're writing a mod that needs custom block or item models, this is the system you'll use.
Architecture: The model system handles loading, parsing, and baking JSON models; the rendering system uses
UniformRenderPipelinefor unified rendering with AO, tint, display transforms, face culling, and other extensions.
-
CatFrame JSON Model & Rendering System
- Table of Contents
- 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. Texture Registration & Dual Atlas System
- 9. Resource Directory Structure
- 10. Render Pipeline Flow (v0.2.0)
- 11. Core Classes at a Glance
- Rendering Extension System
- 12. BlockState Property System (v0.2.0)
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 base model (hardcoded)"]
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 |
texture_size |
Texture file pixel dimensions, e.g. [64, 64]; UV coordinates are in 16x16 abstract space (see below) |
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.
CatFrame supports two rotation formats:
Standard format (recommended):
"rotation": { "angle": 45, "axis": "y", "origin": [8, 8, 8] }Compatibility format (exported by newer Blockbench):
"rotation": { "x": -47.5, "y": 0, "z": 0, "origin": [8, 8, 11.7] }Direct x/y/z fields instead of angle + axis. When multiple non-zero axes exist, the first non-zero one is used (priority: x → y → z).
UV values in [u0, v0, u1, v1] represent positions in 16x16 abstract texture space.
Even if texture_size is [64, 64] (actual texture 64x64 pixels), UV values in JSON already are normalized to 16x16 space by Blockbench:
-
getInterpolatedU(6.75)= 42% position on the texture - No additional scaling by
texture_sizeis needed - Example: UV [6.75, 7.25, 7.5, 7.75] on a 64x64 texture maps to pixels (27, 29) to (30, 31)
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 |
Pipeline note (v0.3.1+): These transforms are actually applied now. The renderer maps each
RenderPhaseto a display key and appliesglTranslate → glRotate(zyx) → glScalebefore submitting quads viaTessellator. See §9 Pipeline Flow for details.
| RenderPhase | Display Key | Auto-mapped For |
|---|---|---|
ITEM_GUI |
"gui" |
All items in inventory / GUI |
ITEM_HAND_FIRST_PERSON |
"firstperson_righthand" |
Local player, 1st-person view |
ITEM_HAND_THIRD_PERSON |
"thirdperson_righthand" |
Other players, or 3rd-person view |
BLOCK_GUI |
"gui" |
Block rendered in GUI (needs explicit renderBlockQuadsGUI) |
DROPPED_ITEM_GROUND |
"ground" |
Dropped item on the ground |
DROPPED_BLOCK_GROUND |
"ground" |
Dropped block on the ground |
For item blocks (e.g. stone in hand), the system automatically looks up display transforms from the
block's resolved model (chain: stone → cube_all → cube → block). This means block.json's display
transforms are applied to any block item that doesn't have a dedicated item model.
Note on ModernItem:
ItemModernno longer carries its own handheld 3D toggle (setRender3DInHand/shouldRender3DInHandhave been removed). All handheld 3D rendering is now uniformly managed by the JSON model system'sUniformRenderPipeline, which applies per-context display transforms based onRenderPhase. When a ModernItem has a registered JSON model, the model system fully controls its appearance in GUI (firstperson_righthand). When no JSON model is registered, the vanillaisFull3D()flag (set in the constructor) controls hand rendering — same as any vanilla Item.
Texture values starting with # reference another texture key:
{
"textures": {
"all": "minecraft:blocks/stone",
"north": "#all",
"south": "#all"
}
}The system resolves reference chains recursively until reaching an actual texture path.
⚠ Circular Reference Detection: Circular mappings such as
"particle": "#texture"combined with"texture": "#particle"are not allowed. When a cycle is detected, the texture variable falls back to Missingno (invalid texture) and a warning is logged:Unable to resolve texture due to reference chain particle->texture->particle in particleMissing texture references also fall back to Missingno.
Models whose path starts with builtin/ are hardcoded in ModelResolver and are never loaded
from JSON files. Builtin models take the highest priority — even if a JSON file with the same
name exists on disk, it will not be read.
The root base for the standard flat item model. Defines a single-layer element at Z=8 with
north/south faces using the #layer0 texture:
| Field | Value |
|---|---|
gui_light |
"front" (flat lighting) |
| Elements | Single quad, from [0,0,8] → to [16,16,8]
|
| Texture ref |
#layer0 (provided by child models) |
| Display | Full 6-context transforms (gui / ground / fixed / thirdperson / firstperson) |
This is the top-level parent of the item model inheritance chain. item/generated.json's
parent: "builtin/generated" points here:
Specific item (provides textures: {"layer0": "..."})
└─ item/generated (overrides display)
└─ builtin/generated [hardcoded] (provides elements)
The MissingNo invalid texture model. Used as a fallback when a model is missing or texture resolution fails:
textures: { "all": "minecraft:missingno" }- Single-layer plane element using
#allreference - Renders as the purple/black checkerboard texture
Note:
builtin/generated.jsonandbuiltin/missing.jsondo not need to exist on disk. If you want to override a builtin model, you can create a JSON file with the same path — but the builtin version always wins.
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.DataLoading.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.
Minecraft 1.7.10 maintains two independent texture atlases:
| Atlas | Type | Lookup Directory |
|---|---|---|
| Block Atlas | type 0 | assets/{ns}/textures/blocks/ |
| Item Atlas | type 1 | assets/{ns}/textures/items/ |
CatFrame determines which atlas a texture is registered on based on model type (not texture path prefix):
-
model_mappings.jsonblockssection → textures registered to block atlas -
model_mappings.jsonitemssection → textures registered to item atlas -
blockstates/*.json→ block atlas (blockstates are always block models)
For manual texture collection, use the isItemModel parameter:
// Item model → textures registered to item atlas
VanillaModelManager.TextureManagement.collectTextures("item/my_item", true);
// Block model (default) → textures registered to block atlas
VanillaModelManager.TextureManagement.collectTextures("block/my_block");Path-agnostic: Whether your texture files live under
textures/items/,textures/item/, or any other folder makes no difference to the registration logic. The system only looks at whether the model is an item or block model — texture path prefixes are irrelevant.
TextureStitchEvent.Post dual-type timing: The two Post events fire in order: type 0 (block atlas) first, then type 1 (item atlas). At type 0 Post time, the item atlas is not yet stitched — item texture IIcon references must be collected during type 1 Post. Manual texture collection via
collectTextures()is safe to call fromTextureStitchEvent.Posthandlers for either atlas type.
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/<br/>(hardcoded in code)"]
M --> BLK["block/"]
M --> ITEM["item/"]
BUILTIN --> BG["generated.json<br/>Hardcoded builtin 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"]
CatFrame v0.2.0 adds BlockState Property System dispatch alongside the existing paths:
flowchart TD
subgraph PREINIT["preInit"]
INIT["VanillaModelManager.DataLoading.init()
Load model_mappings.json
Load metadata_map.json
Load blockstates/*.json
Resolve parent chains
Collect texture paths"]
REG["VanillaModelManager.DataLoading.registerBlock(block)
VanillaModelManager.DataLoading.registerNamespace(ns)"]
STATE["CatStateDefinition.Builder<Block>()
.add(properties).create()
→ Pre-computes all state combos
→ Builds neighbor jump-table
→ Stored in state definitions map"]
end
subgraph TEX_PRE["TextureStitchEvent.Pre"]
TP_BLOCK["getTextureType()==0 (Block Atlas)\nRegister pendingTextures"]
TP_ITEM["getTextureType()==1 (Item Atlas)\nRegister pendingItemTextures"]
end
subgraph TEX_POST["TextureStitchEvent.Post"]
COLLECT["Collect IIcon references from both atlases"]
BAKE["bakeAllModels()
→ Old data auto-wrapped into
MetadataBlockModel + ItemModelWrapper
→ Registered in BlockStateModel/ItemModel maps"]
end
subgraph RENDER_BLOCK["Runtime — Block World Rendering"]
DISPATCH_B["VanillaModelManager.renderBlock()
① Check CatStateDefinition (v0.2.0)
② Check registeredBlockModels
③ Fallback: IBlockStateProvider dynamic
④ Fallback: old baked map → BlockStateModelPart.fromQuads()"]
CATA["CatBlockState.toVariantKey()<br/>→ Match blockstate JSON variant<br/>→ StateBlockModel.collectParts()"]
COLLECT_B["BlockStateModel.collectParts()
→ Returns BlockStateModelPart
(direction-grouped quads)"]
PIPE_B["UniformRenderPipeline.renderBlockQuads()
1. Per-vertex AO computation
2. ModelRenderRegistry extension chain
3. Tessellator submit"]
end
subgraph RENDER_ITEM["Runtime — Item Rendering"]
HANDLES["Mixin intercepts:
① hasItemModel()? → yes
② Detect 1st/3rd person → ITEM_HAND_FIRST_PERSON / _THIRD_PERSON
③ ItemModel.handles(phase)?
├─ false → vanilla render
└─ true → cancel
"]
DISPATCH_I["VanillaModelManager.renderItem()/renderItemInHand()
→ registeredItemModels lookup
→ ItemModel.render()"]
EXEC_I["ItemModel.render()
→ ItemModelWrapper delegates to BlockStateModel
→ BlockStateModelPart
→ UniformRenderPipeline.renderItemQuads(part, stack, phase, display)"]
PIPE_I["UniformRenderPipeline.renderItemQuads()
⚠ Display transform (if available):
phaseToDisplayKey(phase)
→ 'gui' / 'firstperson_righthand' / 'thirdperson_righthand'
→ applyDisplayTransform(): glTranslate → glRotate(zyx) → glScale
1. ModelRenderRegistry extension chain
2. Tessellator submit"]
end
INIT --> REG
REG --> STATE
STATE --> TP
TP --> COLLECT
COLLECT --> BAKE
BAKE --> DISPATCH_B
BAKE --> HANDLES
HANDLES --> DISPATCH_I
DISPATCH_B --> CATA
DISPATCH_B --> COLLECT_B
CATA --> COLLECT_B
COLLECT_B --> PIPE_B
DISPATCH_I --> EXEC_I
EXEC_I --> PIPE_I
Block and item rendering both go through UniformRenderPipeline, ensuring consistent AO, shading, and extension chain behavior across all JSON-modeled content.
| Class | Responsibility |
|---|---|
VanillaModelManager |
Orchestrator — init, texture registration, model baking, BlockStateModel/ItemModel dispatch, render entry points |
BlockStateModel (v0.2.0) |
Block model scheduling interface — collectParts() returns a BlockStateModelPart
|
BlockStateModelPart (v0.2.0) |
Direction-grouped quad container; getQuads(EnumFacing), getAllQuads()
|
SingleBlockModel (v0.2.0) |
BlockStateModel impl — wraps a static BlockStateModelPart
|
MetadataBlockModel (v0.2.0) |
BlockStateModel impl — dispatches by metadata int |
StateProviderBlockModel (v0.2.0) |
BlockStateModel impl — delegates to IBlockStateProvider.getStateProperties()
|
MultipartBlockModel (v0.2.0) |
BlockStateModel impl — evaluates multipart conditions and composes parts |
StateBlockModel (v0.2.0) |
BlockStateModel impl — property-based dispatch via CatBlockState.toVariantKey()
|
ItemModel (v0.2.0) |
Item render interface — render(ItemStack, RenderPhase); handles(RenderPhase) controls whether this model takes over a phase (default: excludes ITEM_HAND_FIRST_PERSON) |
ItemModelWrapper (v0.2.0) |
ItemModel impl — reuses BlockStateModel for item rendering via UniformRenderPipeline; carries display data |
BlueyPlushyItemModel |
ItemModel example impl: handles() returns false for ITEM_GUI (vanilla 2D icon), true for handheld phases (custom 3D model) |
UniformRenderPipeline (v0.2.0) |
Centralized quad rendering: per-vertex AO computation → extension chain → Tessellator submit |
Property (v0.2.0) |
Type-safe property base class — getName(), getValues(), getInternalIndex(). Package: model.state.property
|
BooleanProperty (v0.2.0) |
Property impl — true(0) / false(1). Package: model.state.property
|
IntegerProperty (v0.2.0) |
Property impl — integer range [min, max], index = value - min. Package: model.state.property
|
EnumProperty (v0.2.0) |
Property impl — enum values with ordinal-to-index mapping. Package: model.state.property
|
CatBlockState (v0.2.0) |
State instance — values[] + neighbors[][] jump-table; setValue() O(1), toVariantKey() for JSON matching |
CatStateDefinition (v0.2.0) |
State definition — Builder pattern, Cartesian product generation, neighbor table pre-computation |
ModelResolver |
Resolve parent inheritance chain, merge textures/elements/display |
ModelJson |
Model JSON data structure (includes DisplayTransform, gui_light, texture_size) |
ModelJson.Rotation |
Rotation definition: supports standard angle+axis+origin and compatibility x/y/z fields |
BlockstateJson |
Blockstate JSON data structure + custom Gson deserialiser |
BlockJsonModelBake |
Bake ModelJson elements into BakedQuad
|
BlockJsonModelBake.BakedQuad |
Baked quad: vertices, UV (0-16 space), icon, face, tintIndex, ambientOcclusion, shadeEnabled, guiLight (model-level lighting mode) |
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 |
IItemJsonModel |
Item model auto-discovery interface — getModelPath(), shouldHandle(), handles(RenderPhase). Three-layer priority: model_mappings.json > IItemJsonModel > convention path |
MixinRenderBlocks |
Intercept vanilla block rendering |
MixinItemRenderer |
Intercept ItemRenderer.renderItem(); detects 1st-person (entity == player && thirdPersonView == 0) vs 3rd-person → passes correct RenderPhase; calls ItemModel.handles(phase) to gate custom rendering |
MixinRenderItem |
Intercept RenderItem.renderItemIntoGUI() / renderItemAndEffectIntoGUI() — calls ItemModel.handles(ITEM_GUI) to decide whether to dispatch CatFrame item model or fall back to vanilla 2D icon |
CatFrameConfig |
Mod configuration (debugLogThingsEnabled, enableBlueyPlushy); provides shouldLogDebug() to check dev environment or debug mode |
GuiLightExtension |
Built-in extension: controls directional shading based on BakedQuad.guiLight — "front" forces shade=1.0 (flat lighting), "side" preserves default shading |
CatFrame provides a universal model rendering extension system that allows mods to insert custom tinting, shadows, self-illumination, face culling, and other rendering effects through a unified registration point, without needing to mixin the rendering pipeline.
JSON Model
│ Parsing + Baking (ambientocclusion / shade / display / gui_light passed to BakedQuad)
▼
BakedQuad (contains tintIndex, face, icon, vertices,
│ ambientOcclusion, shadeEnabled, guiLight, modelDisplay)
▼
UniformRenderPipeline.render*Quads()
│
│ ① ModelRenderRegistry.applyBeforePart() — Lifecycle: before all quads
│ └→ GuiLightExtension: GL_LIGHTING setup
│ DisplayTransformExtension: display transform GL matrix
│
│ ② for each quad: ModelRenderRegistry.apply() — Extensions run in order
│ ┌────────────────────────────────────────────────┐
│ │ AOComputeExtension (per-vertex AO computation) │
│ │ FaceCullExtension (face culling) │
│ │ AOShadeExtension (AO/shade toggles) │
│ │ GuiLightExtension (gui_light shade handling) │
│ │ TintRenderExtension (tintindex tinting) │
│ │ Your extensions… │
│ └──────────────┬─────────────────────────────────┘
│ │ Modify ctx.color/
│ │ brightnessOverride/
│ │ shade/aoBrightness/aoColorMul/
│ │ skip
│ ▼
│ ③ ModelRenderRegistry.applyAfterPart() — Lifecycle: after all quads
│ └→ GuiLightExtension: GL_LIGHTING restore
│ DisplayTransformExtension: GL matrix restore
▼
Tessellator
│ hasVertexAO?
├─true → per-vertex setBrightness + setColorOpaque_F
└─false→ uniform rendering
| Class | Role |
|---|---|
IModelRenderExtension |
Interface with beforePart(List, RenderPhase) → apply(RenderContext) → afterPart() lifecycle. beforePart/afterPart have default no-op, only apply is mandatory. |
RenderContext |
Mutable context for a quad; extensions read the environment and write color/brightness/culling. |
ModelRenderRegistry |
Registry that chains extensions together in registration order. |
RenderPhase |
Enum: BLOCK_WORLD / BLOCK_GUI / ITEM_GUI / ITEM_HAND_FIRST_PERSON / ITEM_HAND_THIRD_PERSON. |
UniformRenderPipeline |
Unified pipeline: creates RenderContext, dispatches lifecycle + extension chain, submits to Tessellator. |
@SideOnly(Side.CLIENT)
public class MyClientInit {
public void init() {
ModelRenderRegistry.register(ctx -> {
// ① Handle only specific phases
if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
// ② Target only your own block
if (ctx.block != MyBlocks.CRYSTAL) return;
// ③ Modify output fields
ctx.brightnessOverride = 0xF000F0; // always full bright (self-illumination)
ctx.mulColor(0xCCDDFF); // add cool tint
});
}
}| Field | Type | Phase | Purpose |
|---|---|---|---|
phase |
RenderPhase |
All | Indicates the current rendering scenario. |
quad |
BakedQuad |
All | Contains face, tintIndex, icon, and four vertex coordinates/UVs. |
world / x / y / z |
Block coordinates | BLOCK | null/0 for item phases. |
block |
Block |
BLOCK | null for item phases. |
stack |
ItemStack |
ITEM | null for block phases. Can read NBT/damage/enchantments for fine logic. |
metadata |
int | All | Block metadata value (default 0). Useful for tint/extension logic in BLOCK_GUI phase. |
baselineBrightness |
int (final) | All | Default brightness computed by the renderer (neighbor light for blocks, full bright or GUI default for items). |
skip |
bool | All | If set to true: this quad is discarded and the chain stops. |
color |
int 0xRRGGBB | All | Color multiplier. Default 0xFFFFFF. Prefer using mulColor() to combine. |
brightnessOverride |
int | All | Overrides brightness when ≥0; -1 falls back to baselineBrightness. |
shade |
float | All | Directional light intensity (top 1.0 / side 0.8 / bottom 0.5). Extensions can override for flat shading. |
aoBrightness[4] |
int[] | BLOCK | Per-vertex AO brightness (packed int). -1 falls back to uniform rendering. Block phase only. |
aoColorMul[4] |
float[] | BLOCK | Per-vertex AO occlusion factor (0~1), multiplied with color×shade. Block phase only. Default 1.0f. |
iconOverride |
IIcon |
All | When non-null, renderer uses this icon instead of quad.icon for UV sampling. Used for runtime texture switching (e.g. leaves fancy/fast). |
Source: model/render/extension/tint/TintRenderExtension.java
Convenience API: model/render/extension/tint/TintRegistry.java
JSON Model Syntax (inspired by 1.13+ grass blocks):
{
"elements": [
{
"from": [0, 0, 0], "to": [16, 16, 16],
"faces": {
"down": { "texture": "#bottom" },
"up": { "texture": "#top", "tintindex": 0 },
"north": { "texture": "#side" },
"south": { "texture": "#side" }
}
},
{
"from": [0, 0, 0], "to": [16, 16, 16],
"faces": {
"north": { "texture": "#overlay", "tintindex": 0 },
"south": { "texture": "#overlay", "tintindex": 0 }
}
}
]
}Default Behavior (zero configuration works):
| Scenario | Default Source |
|---|---|
| Block (in world) | block.colorMultiplier(world, x, y, z) |
| Item (GUI / held) | If it is an ItemBlock, uses block.getRenderColor(damage)
|
Custom Tinting:
// Block side: use world + coordinates to decide color
TintRegistry.registerBlockTint(MyBlocks.ARCTIC_GRASS,
(world, x, y, z, b, idx) -> 0xB8E0FF);
// Item side: use ItemStack to decide color
TintRegistry.registerItemTint(MyItems.DYE_BAG,
(stack, idx) -> stack.getTagCompound() != null
? stack.getTagCompound().getInteger("color")
: 0xFFFFFF);ambientocclusion field (element-level):
-
Default: Not writing it =
true, enables per-vertex AO -
false: Disables AO, all faces forced to max brightness0xF000F0(self-illuminating)
shade field (element-level):
-
Default: Not writing it =
true, enables directional shading (top 1.0 / side 0.8 / bottom 0.5) -
false: Disables directional shading, all faces forced to1.0brightness coefficient
Combined Usage:
| ambientocclusion | shade | Effect Description |
|---|---|---|
| Not set (default) | Not set (default) | Standard rendering, affected by both AO and directional shading |
false |
Not set | Full bright, but retains directional shading |
| Not set | false |
Affected by surrounding lighting, but all faces uniformly lit |
false |
false |
Fully self-illuminating effect |
Source: model/render/extension/GuiLightExtension.java
Processes the JSON model's "gui_light" field:
| gui_light value | Renderer Behavior | Vanilla Equivalent |
|---|---|---|
"front" |
Disables GL_LIGHTING + shade=1.0 (flat lighting) | Like items, flat images |
"side" |
Preserves GL_LIGHTING + directional shade (side lighting) | Like blocks, 3D objects |
| not set | Preserves GL_LIGHTING, directional shade by face normal | Default |
GL_LIGHTING state management is handled through the extension lifecycle:
-
beforePart()— checks ifgui_light="front", disables GL_LIGHTING, saves previous state -
apply()— per-quad: forcesctx.shade = 1.0for"front"mode -
afterPart()— restores GL_LIGHTING to its previous state
- Can be registered during client
initorpostInit. - On first registration or first render,
ModelRenderRegistrylazily installs built-in extensions:-
FaceCullExtension— cullface processing (registered at chain head for early culling) -
AOComputeExtension— per-vertex AO computation (BLOCK_WORLD only) -
AOShadeExtension— ambientocclusion/shade toggle -
GuiLightExtension— gui_light lighting mode + GL_LIGHTING lifecycle -
TintRenderExtension— tintindex processing -
DisplayTransformExtension— display transform + GL matrix lifecycle -
LeavesGraphicsExtension— leaves fancy/fast graphics switch (registered by mods)
-
- The built-in extensions always stay at the head of the chain; mod extensions are appended in registration order.
-
High call frequency: The extension chain is invoked per quad per block per frame. Avoid expensive operations like reflection, I/O, string concatenation inside
apply. -
Order-sensitive: Extensions registered later see modifications made by earlier extensions. To "take exclusive control", set
skip = trueto terminate the chain immediately. -
Do not modify quad geometry:
BakedQuadis shared across multiple render caches; modifying its fields will pollute other renders. -
Unregistering: Call
ModelRenderRegistry.unregister(yourExt)on mod hot-reload or unload for cleanup.
CatFrame v0.2.0 introduces a type-safe BlockState Property System, directly inspired by Minecraft 1.21.5's Property / StateDefinition / StateHolder architecture. It provides typed properties (Boolean, Integer, Enum), pre-computed state instances with O(1) neighbor jump-tables, and seamless integration with the existing blockstate JSON rendering pipeline.
Property<T> (abstract base class)
├── BooleanProperty — true(0) / false(1)
├── IntegerProperty — integer range, index = value - min
└── EnumProperty<T> — enum values with ordinal-to-index mapping
CatStateDefinition.Builder<O> (builds all state combinations)
→ add(Property<?>...) — declare properties
→ create() — Cartesian product: N_states = prod(property_value_counts)
→ CatStateDefinition<O> — manages all CatBlockState instances
→ CatBlockState — single state instance
├── values: Comparable<?>[] (current property values)
└── neighbors: CatBlockState[][] [propertyIdx][valueIdx]
└── O(1) setValue(): neighbors[propIdx][value.getInternalIndex()]
The key performance feature is the pre-computed neighbor table. When CatStateDefinition.Builder.create() runs, it:
- Enumerates all property value combinations (Cartesian product)
- Creates one
CatBlockStateper combination (all states are singletons) - For each state, for each property, for each possible value: computes the target state index and stores it in
neighbors[propertyIdx][valueIdx]
At runtime, state.setValue(Property, Value) is a direct array lookup — no hashing, no allocation.
neighbor[0] (TYPE dimension)
┌────────┬────────┬────────┐
│ BOTTOM │ TOP │ DOUBLE │
neighbor[1] ┐ ┌───┼────────┼────────┼────────┤
(WATERLOGGED) │ T │ state0 │ state1 │ state2 │
│ F │ state3 │ state4 │ state5 │
└───┴────────┴────────┴────────┘
state0.setValue(TYPE, TOP) → state1 (neighbor[0][1])
state0.setValue(WATERLOGGED, true) → state3 (neighbor[1][0])
state5.setValue(TYPE, BOTTOM) → state3 (neighbor[0][0])
// Define properties as static final fields on your block class
public static final Property<Boolean> WATERLOGGED = BooleanProperty.create("waterlogged");
public static final Property<Integer> BITES = IntegerProperty.create("bites", 0, 6);
public enum WoodType { OAK, SPRUCE, BIRCH, JUNGLE }
public static final Property<WoodType> WOOD = EnumProperty.create("wood", WoodType.class);
// Build the state definition (in preInit or constructor)
CatStateDefinition<Block> stateDef = new CatStateDefinition.Builder<>(this)
.add(WATERLOGGED, BITES)
.create();
// Register with VanillaModelManager
VanillaModelManager.ModelRegistration.registerStateDefinition(this, stateDef);
// O(1) state transitions
CatBlockState any = stateDef.any(); // default: waterlogged=true, bites=0
CatBlockState dry = any.setValue(WATERLOGGED, false); // O(1) via neighbor table
CatBlockState specific = any.setValue(BITES, 3); // O(1) via neighbor tableImplement the optional getStateDefinition() and getBlockState() methods to give the renderer type-safe access to your block's current state:
public class MyCakeBlock extends Block implements IBlockStateProvider {
public static final Property<Integer> BITES = IntegerProperty.create("bites", 0, 6);
private final CatStateDefinition<Block> stateDef;
public MyCakeBlock() {
this.stateDef = new CatStateDefinition.Builder<>(this)
.add(BITES)
.create();
}
@Override
public CatStateDefinition<?> getStateDefinition() { return stateDef; }
@Override
public CatBlockState getBlockState(IBlockAccess world, int x, int y, int z, int metadata) {
return stateDef.any().setValue(BITES, Math.min(metadata, 6));
}
// Legacy method — keep for backward compatibility
@Override
public Map<String, String> getStateProperties(IBlockAccess world, int x, int y, int z, int metadata) {
return Collections.singletonMap("bites", String.valueOf(metadata));
}
}When the renderer detects a registered CatStateDefinition and the block provides a non-null CatBlockState, it calls CatBlockState.toVariantKey() to generate the variant key (e.g. "bites=3") and matches it against the blockstate JSON — just like the legacy Map<String,String> path, but with type safety and O(1) transitions.
| Property Type | Value Class | Index Method | Creation |
|---|---|---|---|
BooleanProperty |
Boolean |
v ? 0 : 1 |
BooleanProperty.create("name") |
IntegerProperty |
Integer |
v - min |
IntegerProperty.create("name", min, max) |
EnumProperty<T> |
T extends Enum<T> |
ordinalToIndex[ordinal] |
EnumProperty.create("name", Clazz.class) / EnumProperty.create("name", Clazz.class, filter...)
|
Property.create() (generic) |
Any Comparable
|
values.indexOf(v) |
Property.create("name", Clazz.class, valuesList) |
All existing mechanisms continue to work unchanged:
-
IBlockStateProvider.getStateProperties()→Map<String,String>→ variant matching (legacy path) -
IMetadataMapper→ metadata to property mapping -
JsonBlock.register()→ legacyIBlockJsonModelpath -
model_mappings.json→ quick block/item → model binding
The new CatBlockState path is opt-in — blocks that don't implement getStateDefinition() / getBlockState() simply follow the existing rendering path.