-
Notifications
You must be signed in to change notification settings - Fork 1
CatFrame JSON Model System
CatFrame brings the 26.1+ resource pack model format to Minecraft 1.7.10 — full inheritance chains, blockstate variants, display transforms, tiled textures, and an extension rendering pipeline. If you're writing a mod that needs custom block or item models, this document is your starting point.
Three interfaces form the public surface of the model library:
| Interface | Purpose | Package |
|---|---|---|
IBlockStateProvider |
Register a block whose rendering is driven by a JSON blockstate file | model |
IItemStateProvider |
Register an item whose rendering is driven by a JSON item model | model |
IModelRenderExtension |
Hook into the rendering of every baked quad — tint, brightness, culling | model.render.api |
One-line summary: implement
IBlockStateProvideron yourBlockfor world rendering, implementIItemStateProvideron yourItemfor item rendering, and registerIModelRenderExtensions when you need to reach into the pipeline and tweak individual quads.
- CatFrame Model Library — Developer Guide
Blocks and items enter the CatFrame pipeline through different doors, but both converge on the same quad-processing core:
Block side Item side
────────── ─────────
Block implements IBlockStateProvider Item implements IItemStateProvider
│ │
▼ ▼
BlockStateISBRH (renderType id) ModelRegistry.registerItemModel()
│ │
▼ ▼
VanillaRenderDispatcher.renderBlock RenderJsonItemModel.renderItem() ← Forge IItemRenderer
│ │ (maps ItemRenderType → RenderPhase,
│ │ applies preTransform counter-offsets)
▼ ▼
UniformRenderPipeline.renderBlockQuads UniformRenderPipeline.renderItemQuads
└──────────────┬─────────────────────┘
▼
Extension chain (IModelRenderExtension)
▼
Tessellator / RenderCommandBuffers
| Module | Responsibility |
|---|---|
VMMDataLoader |
Data loading: namespace discovery, blockstate/mappings/items JSON loading |
VanillaModelRegistry / ModelRegistry
|
Registration API: block models, item models, state definitions, rotations |
VMMModelBaking |
Creates lazy model wrappers, registers the Forge IItemRenderer
|
VanillaRenderDispatcher |
Rendering dispatch: blocks (world/GUI), items (GUI/hand/dropped) |
UniformRenderPipeline |
Centralized quad submission: extension chain → Tessellator |
BlockStateISBRH |
Universal ISBRH bridge for mod blocks |
RenderJsonItemModel |
Forge IItemRenderer singleton for mod items |
ModelRenderExtensions |
The only external entry point for registering render extensions |
Source: model/IBlockStateProvider.java
Implement this interface on your Block to register it for blockstate-based JSON model rendering. The system will:
- Load the blockstate JSON from
assets/{namespace}/blockstates/{name}.json - On each render, call
getStateProperties()to obtain the current property map - Match the properties against blockstate variants to select the correct model
This is the code-level registration path — the JSON model itself lives in your resource pack, while the block tells CatFrame where to find it and how to map its metadata into variant properties.
| Method | Required | Description |
|---|---|---|
getBlockstateNamespace() |
✅ | Namespace of the blockstate JSON, e.g. "mymod" → assets/mymod/blockstates/…
|
getBlockstateName() |
✅ | Blockstate file name without .json, e.g. "cake" → blockstates/cake.json
|
getStateProperties(world, x, y, z, metadata) |
✅ | Converts the current world position + metadata into a property map for variant matching |
getStateDefinition() |
❌ (v0.3+) | Optionally returns a typed CatStateDefinition<?> for type-safe property handling |
getBlockState(world, x, y, z, metadata) |
❌ (v0.3+) | Optionally returns a CatBlockState for this position, enabling O(1) state transitions |
A classic metadata → properties example: a cake with metadata=3 returns {"bites": "3"}, which is matched against the variant key bites=3 in the blockstate JSON.
assets/{namespace}/
├── blockstates/{name}.json ← the blockstate file (variants / multipart)
└── models/
├── block/{model}.json ← referenced models
└── item/{model}.json
Blockstate file example:
{
"variants": {
"type=0": { "model": "block/my_block_variant0" },
"type=1": { "model": "block/my_block_variant1" }
}
}Two registration steps are required:
// ① PreInit: register the block for data loading (texture collection, blockstate parsing)
VMMDataLoader.registerBlock(myBlockInstance);
// ② Constructor (or preInit): obtain a dedicated renderType id
int renderTypeId = BlockStateISBRH.register(this);Your block must return that id from getRenderType():
@Override
public int getRenderType() {
return renderTypeId;
}Why this matters: In 1.7.10 every block renders through a global renderType integer. CatFrame's MixinRenderBlocks intercepts only vanilla blocks (renderType 0). Mod blocks instead go through BlockStateISBRH — a universal ISBRH bridge that routes straight into the CatFrame pipeline. BlockStateISBRH.isRegistered(block) is also used by the mixin to skip already-registered mod blocks, avoiding double interception.
public class MyModBlock extends Block implements IBlockStateProvider {
private final int renderTypeId;
public MyModBlock() {
super(Material.rock);
this.renderTypeId = BlockStateISBRH.register(this);
}
@Override
public int getRenderType() {
return renderTypeId;
}
@Override
public String getBlockstateNamespace() {
return "mymod";
}
@Override
public String getBlockstateName() {
return "my_block";
}
@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;
}
}Registration in preInit:
VMMDataLoader.registerBlock(myBlockInstance);For blocks with many properties, the raw Map<String, String> path can be replaced by a typed CatStateDefinition:
public static final Property<Integer> BITES = IntegerProperty.create("bites", 0, 6);
public static final Property<Boolean> WATERLOGGED = BooleanProperty.create("waterlogged");
@Override
public CatStateDefinition<?> getStateDefinition() {
return new CatStateDefinition.Builder<>(this)
.add(BITES, WATERLOGGED)
.create();
}
@Override
public CatBlockState getBlockState(IBlockAccess world, int x, int y, int z, int metadata) {
CatStateDefinition<Block> def = (CatStateDefinition<Block>) getStateDefinition();
return def.any().setValue(BITES, metadata).setValue(WATERLOGGED, false);
}When both are implemented, variant matching runs through CatBlockState with O(1) neighbor jump-tables instead of string maps.
Source: model/IItemStateProvider.java
IItemStateProvider is the single abstraction for CatFrame item rendering. It unifies the two layers that used to be separate ("state discovery interface" and "item model interface"):
-
As a discovery marker: when your
Itemimplements this interface, CatFrame discovers it and collects its textures duringModelManagerDataLoader.init(). -
As a render model: the implementation directly provides
render(stack, phase)andhandles(phase), invoked byRenderJsonItemModelinside the Forge render pipeline.
Note: the historical auto-fallback from
ItemBlockto block models has been removed. Items that don't implementIItemStateProviderfall back to vanilla item rendering — or, if explicitly registered, show the MissingNo model. There is no implicit block-model reuse for items anymore.
| Method | Required | Description |
|---|---|---|
shouldHandle() |
❌ (default true) |
Global switch: false → CatFrame never registers this item, vanilla renders it |
handles(RenderPhase) |
❌ (default true) |
Per-phase switch: false → that phase falls back to vanilla rendering |
render(stack, phase) |
✅ | The actual rendering logic |
render(stack, phase, preTransform) |
❌ | Rendering with a pre-transform matrix (counter-offset), applied before the display transform; default delegates to the 2-arg version |
getGuiModelParts(stack) |
❌ (empty) | GUI-stage selected model parts — used by the oversized-in-gui overflow check to measure geometry bounds |
getPropertyDefinitions() |
❌ (empty) | Declares custom item properties (modid:name → provider), auto-registered during discovery |
When CatFrame looks for the model of an item, it checks — in order:
-
items/{item}.json(ItemState decision tree) — highest priority, data-driven -
model_mappings.jsonitemsfield — legacy flat mapping -
Item implements
IItemStateProvider— code-level registration -
Convention path
assets/{namespace}/models/item/{name}.json— lazy discovery fallback
@Override
public boolean shouldHandle() {
return true; // global switch — false = vanilla renders this item everywhere
}
@Override
public boolean handles(RenderPhase phase) {
// Fine-grained per-phase control
if (phase == RenderPhase.ITEM_GUI) return false; // GUI → vanilla 2D sprite
return true; // hand / ground → custom 3D
}handles() is consulted by RenderJsonItemModel.handleRenderType() — CatFrame only takes over the phases your model opts into.
The phase → Forge ItemRenderType mapping handled by RenderJsonItemModel:
CatFrame RenderPhase
|
Forge ItemRenderType
|
|---|---|
ITEM_GUI |
INVENTORY |
ITEM_HAND_FIRST_PERSON |
EQUIPPED_FIRST_PERSON |
ITEM_HAND_THIRD_PERSON |
EQUIPPED |
DROPPED_ITEM_GROUND |
ENTITY (non-block items) |
DROPPED_BLOCK_GROUND |
ENTITY (block items) |
| (not handled) | FIRST_PERSON_MAP |
Mirroring IBlockStateProvider.getStateDefinition() on the block side, getPropertyDefinitions() lets implementations declare custom item properties in place:
@Override
public Map<String, ItemPropertyProvider> getPropertyDefinitions() {
Map<String, ItemPropertyProvider> defs = new HashMap<>();
defs.put("mymod:charge", (stack, phase) ->
stack.getTagCompound() != null ? stack.getTagCompound().getInteger("charge") : 0);
return defs;
}- Keys must be fully namespaced (
modid:name); bare names are rejected with a warning. - Entries are validated and auto-registered through
CatItemPropertiesduring discovery — you never call the registration facade manually. - Each
ItemPropertyProvider.compute(stack, phase)is evaluated lazily, only when the decision tree actually accesses the property.
public class MyModItem extends Item implements IItemStateProvider {
public MyModItem() {
this.setUnlocalizedName("my_item");
this.setTextureName("mymod:my_item");
}
@Override
public void render(ItemStack stack, RenderPhase phase) {
// Select the model part for this stack/phase and submit its quads.
// CatFrame's decision-tree based implementations collect parts from
// the evaluated ItemState and hand them to UniformRenderPipeline.renderItemQuads().
BlockStateModelPart part = resolvePart(stack, phase);
UniformRenderPipeline.renderItemQuads(part, stack, phase, null, 0, 0, 0, null, null, null);
}
}No manual Forge registration is needed — discovery handles it. For explicit registration at any time:
ModelRegistry.registerItemModel(Items.apple, myModel); // registers Forge IItemRenderer immediatelySource: model/render/api/IModelRenderExtension.java
This is the most powerful of the three interfaces: every quad baked by CatFrame passes through the extension chain before hitting the Tessellator. Extensions can tint, brighten, cull, or re-texture any quad — blocks and items alike — without a single mixin.
All registration goes through the facade ModelRenderExtensions (model.render.api package) — the only external entry point:
// Default priority 0 — appended to the tail of the priority-0 group
ModelRenderExtensions.register(ext);
// Explicit priority — smaller runs first, negatives insert before built-ins
ModelRenderExtensions.register(ext, -2000);
// Remove on hot-reload / unload
ModelRenderExtensions.unregister(ext);
// Current extension count (including built-ins)
int n = ModelRenderExtensions.size();- Register during client
init(orpostInit). - The facade delegates to the internal
ModelRenderRegistry; you should never touch the registry class directly. -
One chain serves every path: block world rendering, GUI rendering, item hand rendering, dropped items — all phases funnel into the same ordered list of extensions. Extensions discriminate by
ctx.phase.
For each "part" (a BlockStateModelPart — the quad container selected for this render), the pipeline drives three hooks:
beforePart(allQuads, phase, part) ← once, before any quad is processed
│ (GL state setup, global decisions)
▼
for each quad:
apply(RenderContext ctx) ← once per quad, in priority order
│ (modify ctx.color / brightness / skip…)
│ └─ if ctx.skip == true → chain terminates, quad is discarded
▼
afterPart() ← once, after all quads processed
(GL state restore, cleanup)
| Hook | Frequency | Default | Typical use |
|---|---|---|---|
beforePart(List<BakedQuad>, RenderPhase, BlockStateModelPart) |
once per part | no-op | Detect model lighting mode and set GL_LIGHTING, apply display-transform GL matrices |
apply(RenderContext) |
once per quad | mandatory | Per-quad tint, brightness, culling, icon override |
afterPart() |
once per part | no-op | Restore GL state modified in beforePart
|
A legacy two-argument beforePart(List, RenderPhase) is kept for compatibility and defaults to delegating to the three-argument version with part = null — prefer overriding the three-argument one.
Phase awareness is your job:
RenderPhasetells you where the quad is being processed.BLOCK_WORLDcarriesworld/x/y/z/block; item phases carrystackand null world fields. Guard your logic accordingly.
RenderContext is a mutable context object per quad. Fields split into inputs (final, read-only) and outputs (mutable — write these to affect rendering):
Input fields (read, don't write):
| Field | Type | Available in |
|---|---|---|
phase |
RenderPhase |
All phases |
quad |
BakedQuad |
All phases (face, tintIndex, icon, vertices) |
world / x / y / z |
IBlockAccess / int |
Block phases only (null/0 for items) |
block |
Block |
Block phases only |
stack |
ItemStack |
Item phases only (read NBT/damage/enchantments) |
metadata |
int | All (block metadata, default 0 — e.g. BLOCK_GUI tinting) |
baselineBrightness |
int | All (renderer-computed base light) |
aoBrightness[4] |
int[] | BLOCK_WORLD (per-vertex, -1 = fall back to uniform) |
aoColorMul[4] |
float[] | BLOCK_WORLD (per-vertex occlusion, 1.0 = none) |
Output fields (write these to change behavior):
| Field | Type | Effect |
|---|---|---|
skip |
boolean |
true → quad is discarded and the chain stops immediately (face culling) |
color |
int 0xRRGGBB
|
Color multiplier, default 0xFFFFFF. Prefer accumulating via mulColor()
|
brightnessOverride |
int |
≥ 0 → force this brightness (self-illumination / shadows); -1 → use baselineBrightness
|
shade |
float | Directional light coefficient (top 1.0 / side 0.8 / bottom 0.5), multiplied into final color |
iconOverride |
IIcon |
Non-null → renderer samples this icon instead of quad.icon (runtime texture swap) |
displayTransform |
Matrix4d |
Display-transform matrix, computed and set by DisplayTransformExtension; the pipeline applies it to vertices before submission |
Convenience methods:
ctx.mulColor(0xFFAA66); // channel-wise multiply into ctx.color (stackable)
int b = ctx.effectiveBrightness(); // override >= 0 ? override : baselineThe final color fed to the Tessellator combines everything: finalColor = color × shade × aoColorMul[i] per vertex, with per-vertex brightness when AO data is present.
- Every extension carries an integer
priority— smaller runs first. - Same priority → stable registration order (first registered runs first).
- Default priority is
DEFAULT_PRIORITY = 0. - Built-in extensions live at
BUILTIN_PRIORITY_BASE = -1000and occupy the head of the chain, so your priority-0 extension sees the built-ins' modifications (AO data, tint result, shade). - To run before a built-in, register with a priority
< -1000. - Re-registering the same instance = re-positioning: the old entry is removed and the instance is re-inserted at the new priority. One instance never occupies two slots.
- Setting
ctx.skip = truein any extension terminates the chain immediately — a clean way to take exclusive control of a quad.
priority: -1000 -999 … -994 0 +10
┌──────────────┬───────────────┬───────────┬──────────┐
│ Built-ins │ (chain head) │ Your ext │ Later mod│
│ FaceCull … │ │ (default) │ │
└──────────────┴───────────────┴───────────┴──────────┘
Applies since v0.5. Rendering may enter from any thread (e.g. Beddium's multithreaded chunk compilation).
- The extension list is a
CopyOnWriteArrayList— registration/unregistration is safe concurrently with render traversal. -
Extensions must not hold shared mutable state across threads. Per-part temporary data goes into
ThreadLocals or is written intoRenderContext. -
Error isolation: an exception (including
Error) thrown by one extension is caught and logged; the rest of the chain and the whole render continue. Your extension can't crash the frame. - Same-priority ordering is stable even under concurrent registration.
Installed lazily on first registration/first render. They stay at the chain head in this fixed order:
| # | Extension | Priority | Job |
|---|---|---|---|
| 1 | FaceCullExtension |
-1000 | Process JSON cullface — cull hidden faces before AO runs |
| 2 | AOComputeExtension |
-999 | Per-vertex AO computation (BLOCK_WORLD only) |
| 3 | AOShadeExtension |
-998 |
ambientocclusion / shade element toggles |
| 4 | GuiLightExtension |
-997 |
gui_light lighting mode + GL_LIGHTING lifecycle |
| 5 | TintRenderExtension |
-996 |
tintindex processing via TintRegistry
|
| 6 | DisplayTransformExtension |
-995 |
display transforms (GUI/hand) — writes ctx.displayTransform
|
| 7 | BlockDestroyExtension |
-994 | Destroy decal: iconOverride + full bright + pure white, BLOCK_DESTROY only |
1. Warm tint on a block's top face (world rendering only):
ModelRenderExtensions.register(ctx -> {
if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
if (ctx.block != MyBlocks.LAVA_ROCK) return;
if (ctx.quad.face != Direction.UP) return;
ctx.mulColor(0xFFAA66);
});2. Custom shadowing — halve brightness of all hand-held items:
ModelRenderExtensions.register(ctx -> {
if (ctx.phase == RenderPhase.ITEM_HAND_FIRST_PERSON
|| ctx.phase == RenderPhase.ITEM_HAND_THIRD_PERSON) {
ctx.brightnessOverride = 0x800080;
}
});3. Face culling — skip a north quad when the north neighbour is opaque:
ModelRenderExtensions.register(ctx -> {
if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
if (ctx.quad.face != Direction.NORTH) return;
if (ctx.world.getBlock(ctx.x, ctx.y, ctx.z - 1).isOpaqueCube()) ctx.skip = true;
});4. Full lifecycle — GL state around a part (explicit implementation):
public class GlowExtension implements IModelRenderExtension {
private boolean lightingDisabled = false;
@Override
public void beforePart(List<BakedQuad> allQuads, RenderPhase phase, BlockStateModelPart part) {
if (phase == RenderPhase.ITEM_GUI) {
lightingDisabled = true; // per-thread! see thread-safety note
GL11.glDisable(GL11.GL_LIGHTING);
}
}
@Override
public void apply(RenderContext ctx) {
if (ctx.stack != null && isGlowing(ctx.stack)) ctx.brightnessOverride = 0xF000F0;
}
@Override
public void afterPart() {
if (lightingDisabled) GL11.glEnable(GL11.GL_LIGHTING);
}
}-
Early return on phase/block/stack before doing any work —
applyruns per quad per frame; keep the hot path cheap. No reflection, no I/O, no string concatenation insideapply. -
Chain order is observable: later extensions see earlier modifications. If you need to "own" a quad, set
skip = true— the chain stops and the quad is dropped. -
Never mutate
BakedQuad— it is shared across render caches; modifying it pollutes other renders. All per-render state goes throughRenderContext. -
Thread-safe by construction: no instance fields that change per render unless they're
ThreadLocal— the render path can enter from Beddium worker threads. -
Accumulate colors with
mulColor()instead of overwritingctx.color, so multiple tint sources compose predictably. -
Unregister on unload: call
ModelRenderExtensions.unregister(yourExt)during hot-reload or mod unload. -
Prefer JSON first:
tintindex,ambientocclusion,shade,gui_light,cullface,displayare all handled by built-ins. Write an extension only when the JSON knobs aren't enough (dynamic per-stack logic, world-neighbour queries, runtime texture swaps).
flowchart TD
subgraph PREINIT["preInit"]
INIT["VMMDataLoader.init()<br/>discover namespaces, collect textures"]
REGB["VMMDataLoader.registerBlock()<br/>BlockStateISBRH.register()"]
end
subgraph TEX["Texture Events"]
TP["TextureStitchEvent.Pre"]
TPOST["TextureStitchEvent.Post<br/>collect IIcon references"]
end
subgraph BAKE["Model Registration"]
REGALL["VMMModelBaking.registerAllModels()<br/>lazy wrappers + Forge IItemRenderer"]
end
subgraph RENDER["Runtime Rendering"]
BLOCK["VanillaRenderDispatcher<br/>→ BakedModelCache (lazy bake on miss)"]
ITEM["RenderJsonItemModel<br/>→ IItemStateProvider.render()"]
PIPE["UniformRenderPipeline<br/>beforePart → apply per quad → afterPart<br/>→ Tessellator / RenderCommandBuffers"]
end
INIT --> REGB
REGB --> TP
TP --> TPOST
TPOST --> REGALL
REGALL --> BLOCK
REGALL --> ITEM
BLOCK --> PIPE
ITEM --> PIPE
Key classes at a glance:
| Class | Responsibility |
|---|---|
IBlockStateProvider |
Block-side code-level registration (blockstate JSON + variant mapping) |
IItemStateProvider |
Item-side code-level registration (discovery marker + render model) |
IModelRenderExtension |
Quad-level hook: tint / brightness / culling / icon override |
ModelRenderExtensions |
External registration facade for extensions |
ModelRenderRegistry |
Internal chain registry (priority-sorted, thread-safe) |
RenderContext |
Per-quad mutable context (inputs + outputs) |
RenderPhase |
Phase enum with getDisplayKey() display mapping |
BlockStateISBRH |
ISBRH bridge routing mod blocks into the pipeline |
RenderJsonItemModel |
Forge IItemRenderer singleton mapping ItemRenderType → RenderPhase
|
UniformRenderPipeline |
Quad submission: builds RenderSubmit, drives the extension chain |
BakedModelCache |
Thread-safe LRU with StampedLock — lazy baking on miss |
The model library exposes three developer-facing interfaces:
-
IBlockStateProvider— put it on yourBlock, point it at a blockstate JSON, return a property map, and your block renders through the modern model pipeline. Optionally upgrade to typedCatStateDefinition/CatBlockStatefor O(1) state dispatch. -
IItemStateProvider— the one abstraction for item rendering: it marks your item for discovery and supplies the render call. Control takeover per phase withshouldHandle()/handles(), and declare custom properties in place viagetPropertyDefinitions(). -
IModelRenderExtension— the surgical instrument: a priority-ordered, thread-safe chain that runsbeforePart → apply → afterPartaround every quad. Tint withmulColor, force light withbrightnessOverride, cull withskip, swap textures withiconOverride— no mixins, no shared state, and one bad extension can't take down the frame.
Start with the JSON knobs, reach for IModelRenderExtension when you need runtime logic, and both worlds run through the same battle-tested pipeline.