-
Notifications
You must be signed in to change notification settings - Fork 1
CatFrame Universal Model Rendering Extension System
Applicable to all rendering of blocks (in the world) and items (GUI / held) under the CatFrame JSON model pipeline. Design goal: allow 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.
flowchart TD
A[JSON Model] --> B[Parsing + Baking]
B --> C[BakedQuad]
C --> D[VanillaModelManager.render* per quad]
D --> E[ModelRenderRegistry.apply]
E --> F1[TintRenderExtension built-in]
E --> F2[IModelRenderExtension #2]
E --> F3[IModelRenderExtension #N]
F1 --> G[Modify context: color, brightness, shade, skip]
F2 --> G
F3 --> G
G --> H[Tessellator setColor + addVertex]
Three core classes (package decok.dfcdvadstf.catframe.model.render):
| Class | Role |
|---|---|
IModelRenderExtension |
Functional interface implemented by mods, processes rendering parameters for a single quad. |
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 / ITEM_GUI / ITEM_HAND. |
Subpackage model.render.tint provides the built-in "high-version tintindex" implementation as an example.
@SideOnly(Side.CLIENT)
public class MyClientInit {
public void init() {
ModelRenderRegistry.register(ctx -> {
// ① Handle only specific phases
if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
// ② Target only your own block
if (ctx.block != MyBlocks.CRYSTAL) return;
// ③ Modify output fields
ctx.brightnessOverride = 0xF000F0; // always full bright (self-illumination)
ctx.mulColor(0xCCDDFF); // add cool tint
});
}
}| Field | Type | Phase | Purpose |
|---|---|---|---|
phase |
RenderPhase |
All | Indicates the current rendering scenario. |
quad |
BakedQuad |
All | Contains face, tintIndex, icon, and four vertex coordinates/UVs. |
world / x / y / z |
Block coordinates | BLOCK | null/0 for item phases. |
block |
Block |
BLOCK | null for item phases. |
stack |
ItemStack |
ITEM | null for block phases. Can read NBT/damage/enchantments for fine logic. |
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. |
- Can be registered during client
initorpostInit. - On first registration or first render,
ModelRenderRegistrylazily installs the built-inTintRenderExtension. - The built-in extension always stays at the head of the chain; mod extensions are appended in registration order.
Sources: TintRenderExtension
Convenience API: TintRegistry
Inspired by 1.13+ grass blocks (double elements: bottom dirt + side main texture, top overlay with tintindex):
{
"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 }
}
}
]
}Any face with "tintindex" will be tinted by TintRenderExtension during rendering.
| Scenario | Default Source |
|---|---|
| Block (in world) | block.colorMultiplier(world, x, y, z) |
| Item (GUI / held) | If it is an ItemBlock, uses block.getRenderColor(damage)
|
Vanilla grass, leaves, water, lily pads, etc., already return biome colors in their own colorMultiplier — just add tintindex to the JSON and it works.
// Block side: use world + coordinates to decide color (biome/gradient/block state…)
TintRegistry.registerBlockTint(MyBlocks.ARCTIC_GRASS,
(world, x, y, z, b, idx) -> 0xB8E0FF);
// Item side: use ItemStack to decide color (NBT / damage / enchantments)
TintRegistry.registerItemTint(MyItems.DYE_BAG,
(stack, idx) -> stack.getTagCompound() != null
? stack.getTagCompound().getInteger("color")
: 0xFFFFFF);All of the following can be achieved by simply writing and registering an IModelRenderExtension — no changes to CatFrame core needed.
ModelRenderRegistry.register(ctx -> {
if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
if (ctx.quad.face == EnumFacing.UP && ctx.world.getBlock(ctx.x, ctx.y + 1, ctx.z) == Blocks.snow_layer) {
// Darken top face when covered by snow
ctx.brightnessOverride = Math.max(0, ctx.baselineBrightness - 0x100010);
}
});ModelRenderRegistry.register(ctx -> {
if (ctx.block instanceof BlockNeon) ctx.shade = 1.0f;
});ModelRenderRegistry.register(ctx -> {
if (ctx.phase != RenderPhase.BLOCK_WORLD || ctx.quad.face == null) return;
EnumFacing f = ctx.quad.face;
int nx = ctx.x + f.getFrontOffsetX();
int ny = ctx.y + f.getFrontOffsetY();
int nz = ctx.z + f.getFrontOffsetZ();
if (ctx.world.getBlock(nx, ny, nz) == ctx.block) ctx.skip = true; // Don't render face adjacent to same block
});ModelRenderRegistry.register(ctx -> {
if (ctx.phase == RenderPhase.ITEM_HAND && ctx.stack != null && ctx.stack.getItem() == MyItems.TORCH) {
ctx.brightnessOverride = 0xF000F0;
}
});-
High call frequency: The extension chain is invoked per quad per block per frame. Avoid expensive operations like reflection, I/O, string concatenation inside
apply. -
Order-sensitive: Extensions registered later see modifications made by earlier extensions. To "take exclusive control", set
skip = trueto terminate the chain immediately. -
Do not modify quad geometry:
BakedQuadis shared across multiple render caches; modifying its fields will pollute other renders. To change vertices, readquad.vx/vy/vzand output to Tessellator — but the current extension interface intentionally does not expose vertex writing to prevent misuse. -
Block/item duality: Vanilla
RenderBlocksuses the world path — as long as a block has a JSON model, it will also display correctly in the inventory with biome/default tinting. -
Unregistering: Call
ModelRenderRegistry.unregister(yourExt)on mod hot-reload or unload for cleanup.
| Path | Description |
|---|---|
src/main/java/.../model/render/IModelRenderExtension.java |
Extension interface |
src/main/java/.../model/render/RenderContext.java |
Context object |
src/main/java/.../model/render/RenderPhase.java |
Rendering phase enum |
src/main/java/.../model/render/ModelRenderRegistry.java |
Registry |
src/main/java/.../model/render/tint/TintRenderExtension.java |
Built-in: tintindex processing |
src/main/java/.../model/render/tint/TintRegistry.java |
Convenience API for tint registration |
src/main/java/.../model/render/tint/IBlockTintProvider.java |
Tint interface (block side) |
src/main/java/.../model/render/tint/IItemTintProvider.java |
Tint interface (item side) |
src/main/java/.../model/VanillaModelManager.java#renderQuads / drawItemQuads |
Two call points for the extension chain |