-
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
- Table of Contents
- 1. Architecture Overview
- 2. IBlockStateProvider — Registering JSON-Model Blocks
- 3. IItemStateProvider — Registering JSON-Model Items
- 4. IModelRenderExtension — The Rendering Extension Chain
- 5. Resource Pack Author Guide — Replacing Models with JSON Only
- 6. Pipeline at a Glance
- Summary
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).
TL;DR: This section is for resource pack authors (no Java required) — what can and cannot be replaced with JSON alone, and how to actually replace block / item models. One-line conclusion:
model_mappings.jsoncan now be overridden by a pack (whole-file replacement, merged byResourcePackModelDetectoron reload), and so canmodels/model files andblockstates//items/files — the full mapping chain is pack-editable.
| Source | Load path | Pack override works? |
|---|---|---|
models/**/*.json |
ModelResolver → IResourceManager (pack-aware), re-read on every (re)bake |
✅ Yes |
blockstates/{name}.json |
classpath at startup + ResourcePackModelDetector merge on reload |
✅ Yes (override & pure-add) |
items/{name}.json (ItemState tree) |
classpath at startup + ResourcePackModelDetector merge on reload |
✅ Yes (override & pure-add) |
model_mappings.json |
classpath at startup + ResourcePackModelDetector merge on reload (whole-file replacement) |
✅ Yes (override & pure-add) |
Why blockstates/, items/ and model_mappings.json work: ResourcePackModelDetector
re-scans on every resource reload — blockstate/item candidates are the already-loaded entries
∪ registry entries of the namespace, mappings candidates are all registered namespaces. Pack
layers whose content differs from the classpath baseline are merged into loadedBlockstates /
loadedItemStates / loadedMappings, then models are re-registered (registerAllModels for
blocks, incremental rebuild for items). Pure additions count too — a pack can ship a
blockstate for a block that had none, or a model_mappings.json for a namespace whose jar
shipped none.
Key points:
models/is re-read throughIResourceManageron every (re)bake, so pack overrides take effect naturally;blockstates/,items/andmodel_mappings.jsonare content-compared byResourcePackModelDetectoron every reload, and both overrides and pure additions are merged into the data layer and trigger re-registration. One caveat: the mappings merge is whole-file replacement, not per-entry merging — a pack must ship the complete file, and entries missing from the pack version vanish.
blocks entries carry two semantics selected by the state_mapping flag:
| Mode | Value | Registered as | Behavior |
|---|---|---|---|
legacy (state_mapping absent / false) |
model path, e.g. "block/stone"
|
LazySingleBlockModel |
Single lazy-baked model per block; no metadata variants; only applies to blocks without a blockstate |
state_mapping: true |
blockstate name (optional ns: prefix) |
Full blockstate pipeline (registerResidentBlockModel) |
variants / multipart / Pane / Stairs / redirect all supported |
Registration order in registerAllModels makes blockstate win over model_mappings.blocks:
Step 2 registers every loaded blockstate (including pack overrides) as a resident model, Step 3
skips any block already registered.
Key points: the
blocksfield is only a fallback for blocks without a blockstate: legacy mode = a single lazy-baked model with no metadata variants;state_mapping: true= old IDs routed straight into the full blockstate pipeline. Any loaded blockstate (including pack overrides) outranks a mappings entry — editing mappings is useless for a block that already has a blockstate.A pack can override the whole file (see §5.1): drop
assets/{ns}/model_mappings.jsoninto your pack and re-ship every entry you want to keep plus your additions — entries omitted from the pack file are dropped (whole-file replacement). Keep thestate_mappingflag consistent with the entries you ship.
items entries mirror the blocks dual semantics, selected by the same state_mapping flag:
| Mode | Value | Registered as | Behavior |
|---|---|---|---|
legacy (state_mapping absent / false) |
model path, e.g. "item/stick"
|
ItemStateModel (single-model wrapper) |
One model per item — no decision-tree logic; textures collected into the item atlas, model pre-baked by AsyncBakePipeline
|
state_mapping: true |
ItemState tree name (optional ns: prefix) |
ItemStateModel wrapping the referenced tree |
Full decision-tree behavior; tint bridges and oversized_in_gui flags follow the referenced state |
- Keys are item registry IDs; the namespace defaults to the file's location (
assets/{ns}/), so bare names like"stick"are the norm. -
Priority: an item already registered by
items/{name}.jsonalways wins — its mappings entry is skipped. Effective order:items/tree >model_mappings.items>IItemStateProvider> convention pathmodels/item/{name}.json. - In legacy mode the value is a plain model path —
parent/display/gui_lightstill apply, but per-stack logic (damage, NBT, display context) requires an ItemState tree; usestate_mapping: trueto route an old ID straight into one. - Pack overrides: whole-file replacement, same as blocks (see §5.1). Re-ship every
itemsentry you want to keep, with astate_mappingflag consistent with the entries you ship.
Key points: the
itemsfield is the item-side twin ofblocks— legacy values are plain model paths (single-model wrapper, no decision-tree logic), whilestate_mapping: truevalues are ItemState tree names (full tree behavior, with tint bridges andoversized_in_guifollowing the referenced state). Items already registered byitems/files always outrank mappings entries.
assets/minecraft/
└── models/
└── block/stone.json ← overwrite the target model JSON
- Works for both blockstate-referenced models and legacy
model_mappingsmodels. - On reload,
BakedModelCache.clear()+ModelResolver.clearCache()drop old bakes;AsyncBakePipelinere-bakes throughIResourceManager— the pack version wins. - The model must use the 26.1+ JSON format CatFrame supports (
parent,textures,elements,display,gui_light, …).
assets/minecraft/
└── blockstates/
└── stone.json ← new or overriding blockstate
-
ResourcePackModelDetectorpicks it up (candidates include every registered block of the namespace), merges it, andregisterAllModelsre-registers the block as a resident state model — full variants / multipart support. - This also upgrades a block that previously had only a
model_mappingsentry (no blockstate): the new blockstate takes over, the mappings entry is skipped.
- Overwrite
items/{name}.json(ItemState decision tree — highest priority tier), ormodels/item/{name}.json(convention-path fallback). SeeITEM_STATE.mdfor the tree format. - Or replace the whole
model_mappings.jsonin your pack (whole-file replacement — keep thestate_mappingflag consistent and re-ship every entry you want to keep).
Key points:
- Recipe A (recommended): overwrite the target model file — geometry / texture references / properties are replaced, effective in both modes;
- Recipe B: overwrite or purely add a
blockstates/file — upgrades the block to the full state pipeline (variants / multipart), also works for blocks that previously had only amodel_mappingsentry and no blockstate;- Recipe C: on the item side, overwrite the
items/decision tree, themodels/item/convention path, or the wholemodel_mappings.json(whole-file replacement).
-
New textures are not in the atlas. Texture collection runs at startup over classpath
models; a pack model referencing a texture that was never registered falls back to
missingno (purple-black). Geometry / property / decision overrides work fully; brand-new
textures require mod-side registration (
preInittexture collection). -
model_mappings.jsonoverride = whole-file replacement. A pack that ships the file replaces the jar version entirely — partial edits are impossible, so re-ship every entry you want to keep. -
Legacy
blocksentries are single-model, no metadata variants — use a blockstate for per-metadata visuals. -
Blockstate priority: to change a block that already has a blockstate, use Recipe A or B;
editing
model_mappings.jsonis useless for it.
Key points: the biggest trap is new textures are not in the atlas — geometry replacement is fine, but any texture referenced by the model must already have been collected by CatFrame at startup (reusing vanilla / existing textures is safest), otherwise it renders as missingno. Other limits: mappings overrides are whole-file replacements, legacy single models have no metadata variants, and blockstates always outrank mappings.
- Confirm the referenced textures are already in CatFrame's collected set — reuse existing textures when in doubt.
- Pick the override level: geometry only → Recipe A; state logic (variants / multipart) → Recipe B.
- Reload resources in-game (F3+T in 1.7.10 triggers
refreshResources) and check the log:ResourcePackModelDetector: detected N model overrides, N blockstate overrides, N item state overrides, N mapping overridesVanillaModelManager: Registered N block models, N item models (N Forge renderers)
- If the block renders missingno, the referenced texture was never collected — switch to an existing texture or ask the mod author to register it.
Key points: verification = confirm the textures are in the collected set → drop the files at the right override level → reload with F3+T → check the
ResourcePackModelDetector/VanillaModelManagerlines in the log to confirm the override took effect; missingno means the texture was never collected — switch to an existing texture or have the mod side register it.
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.