Skip to content

CatFrame JSON Model System

dfdvdsf edited this page Jun 23, 2026 · 9 revisions

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 UniformRenderPipeline for unified rendering with AO, tint, display transforms, face culling, and other extensions.


Table of Contents


1. Model Inheritance Chain

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)"]
Loading

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.


2. Model JSON Format

{
  "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

2.1 Elements

{
  "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.

Rotation Format Compatibility

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 Coordinate Convention

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_size is needed
  • Example: UV [6.75, 7.25, 7.5, 7.75] on a 64x64 texture maps to pixels (27, 29) to (30, 31)

2.2 Display Transforms

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 RenderPhase to a display key and applies glTranslate → glRotate(zyx) → glScale before submitting quads via Tessellator. 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: ItemModern no longer carries its own handheld 3D toggle (setRender3DInHand / shouldRender3DInHand have been removed). All handheld 3D rendering is now uniformly managed by the JSON model system's UniformRenderPipeline, which applies per-context display transforms based on RenderPhase. 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 vanilla isFull3D() flag (set in the constructor) controls hand rendering — same as any vanilla Item.

2.3 Texture Variables

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 particle

Missing texture references also fall back to Missingno.


2.4 Builtin Models

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.

builtin/generated

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)

builtin/missing

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 #all reference
  • Renders as the purple/black checkerboard texture

Note: builtin/generated.json and builtin/missing.json do 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.


3. Blockstates

Blockstate files live at assets/{namespace}/blockstates/{name}.json. They determine which model gets rendered based on the block's state.

3.1 Variants Format

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

3.2 Weighted Random

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 }
    ]
  }
}

3.3 Multipart Format

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
  • OR logic: "OR": [{"north": "true"}, {"south": "true"}]
  • Pipe-delimited values: "facing": "north|south"

3.4 Metadata Mapping

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:

Option 1: IMetadataMapper Code Registration (Recommended)

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.

Option 2: metadata_map.json Data-Driven

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().

Option 3: Numeric Keys in Blockstate (Compatibility)

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.


4. Model Mappings

Found at assets/{namespace}/model_mappings.json. A lightweight way to map blocks and items to models without writing a full blockstate file.

4.1 Basic Format

{
  "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.

4.2 Metadata / Damage Binding

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.

4.3 Priority

  • Blockstates > model_mappings — if a block already has a complete blockstate file, mappings won't override it
  • name:metadata supplements won't overwrite metadata slots already baked by blockstates
  • Use model_mappings for simple cases, blockstates for complex ones (rotation, weighted random, multipart)

5. IBlockStateProvider — Dynamic Blockstate Registration

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:

  1. Calls getStateProperties() to get the property map (e.g. {"bites": "3"})
  2. Builds a variant key: "bites=3" (multiple properties sorted alphabetically, comma-separated)
  3. 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" }
  }
}

6. Rendering Mod Blocks

Two ways to hook your mod blocks into the JSON model system:

6.1 IBlockStateProvider (Recommended)

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

6.2 IBlockJsonModel (Legacy)

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.

6.3 Which One?

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.


7. Namespaces & Mod Extensions

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.json
  • assets/mymod/blockstates/*.json
  • assets/mymod/models/**/*.json

ModelResolver also searches registered namespaces when resolving parent chains.


8. Texture Registration & Dual Atlas System

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.json blocks section → textures registered to block atlas
  • model_mappings.json items section → 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 from TextureStitchEvent.Post handlers for either atlas type.


9. Resource Directory Structure

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"]
Loading

10. Render Pipeline Flow (v0.2.0)

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&lt;Block&gt;()
        .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
Loading

Block and item rendering both go through UniformRenderPipeline, ensuring consistent AO, shading, and extension chain behavior across all JSON-modeled content.


11. Core Classes at a Glance

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

Rendering Extension System

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.

Architecture Overview

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

Core Classes

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.

Quick Start

Register an Extension

@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
        });
    }
}

RenderContext Fields Quick Reference

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).

Built-in Extensions

Tint System

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);

AO & Shade Control

ambientocclusion field (element-level):

  • Default: Not writing it = true, enables per-vertex AO
  • false: Disables AO, all faces forced to max brightness 0xF000F0 (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 to 1.0 brightness 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

GuiLight Lighting Mode

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 if gui_light="front", disables GL_LIGHTING, saves previous state
  • apply() — per-quad: forces ctx.shade = 1.0 for "front" mode
  • afterPart() — restores GL_LIGHTING to its previous state

Registration Timing

  • Can be registered during client init or postInit.
  • On first registration or first render, ModelRenderRegistry lazily installs built-in extensions:
    1. FaceCullExtension — cullface processing (registered at chain head for early culling)
    2. AOComputeExtension — per-vertex AO computation (BLOCK_WORLD only)
    3. AOShadeExtension — ambientocclusion/shade toggle
    4. GuiLightExtension — gui_light lighting mode + GL_LIGHTING lifecycle
    5. TintRenderExtension — tintindex processing
    6. DisplayTransformExtension — display transform + GL matrix lifecycle
    7. 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.

Design Constraints & Hints

  1. 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.
  2. Order-sensitive: Extensions registered later see modifications made by earlier extensions. To "take exclusive control", set skip = true to terminate the chain immediately.
  3. Do not modify quad geometry: BakedQuad is shared across multiple render caches; modifying its fields will pollute other renders.
  4. Unregistering: Call ModelRenderRegistry.unregister(yourExt) on mod hot-reload or unload for cleanup.


12. BlockState Property System (v0.2.0)

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.

12.1 Architecture

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()]

12.2 Neighbor Jump-Table Mechanism

The key performance feature is the pre-computed neighbor table. When CatStateDefinition.Builder.create() runs, it:

  1. Enumerates all property value combinations (Cartesian product)
  2. Creates one CatBlockState per combination (all states are singletons)
  3. 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])

12.3 Usage: Type-Safe Block Properties

// 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 table

12.4 Integration with IBlockStateProvider

Implement 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.

12.5 Matrix of Property Types

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)

12.6 Backward Compatibility

All existing mechanisms continue to work unchanged:

  • IBlockStateProvider.getStateProperties()Map<String,String> → variant matching (legacy path)
  • IMetadataMapper → metadata to property mapping
  • JsonBlock.register() → legacy IBlockJsonModel path
  • 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.

Clone this wiki locally