# Extending

## Custom extensions

In a few places the renderer accepts fields that aren't part of vanilla Minecraft's model or item format: ways to pass data from blockstates into models, apply arbitrary tints, mark models double-sided, and a few other things vanilla doesn't expose. They're used internally, but you can set them on your own models and blockstates too.

### Model JSON

| Field | Example | Description |
|---|---|---|
| `x`, `y`, `z` | `90` | Rotation angles (in degrees) applied to the whole model around each axis. Normally set by a blockstate variant, but can be set on a model directly too |
| `uvlock` | `true` | Keep face UVs aligned to world space when the model is rotated by `x`/`y`/`z`. Normally set by a blockstate variant |
| `translation` | `[8, 0, 8]` | `[x, y, z]` translation (in voxel units) applied to the whole model before rendering |
| `scale` | `[0.5, 0.5, 0.5]` | `[x, y, z]` scale applied to the whole model before rendering |
| `transformation` | `{ translation: [0,0,0], scale: [1,1,1], left_rotation: [0,0,0,1], right_rotation: [0,0,0,1] }` | Translation, rotation, and scale applied to the whole model before rendering. Accepts the vanilla item-definition transformation form (translation/rotations/scale) or a flat 16-element matrix array |
| `billboard` | `true` | Billboard the model: it always faces the camera, ignoring [display](models.md#display-transforms) and blockstate rotations. Works in [`createScene`](scenes.md#createsceneassets-blocks-args) too. Pair with `shade_direction_override: "up"` on the elements so world-mode shading stays constant as it turns |
| `double_sided` | `true` | Render all faces from both sides |
| `outline` | `{ "from": [7.2, 7.2, 7.2], "to": [8.8, 8.8, 8.8], "color": "#ffbfbf" }` | Draw a stroked box in place of geometry. `from` and `to` are in voxel units, `color` and `opacity` are optional |
| `dynamic` | `"chest"` | Mark the model as one of the built-in [dynamic model kinds](#dynamic-models), animating its `part`-tagged elements |
| `pose` | `{ "openness": 1 }` | Initial [`poseSpecial`](scenes.md#posespecialroot-pose) pose applied when a `dynamic` model loads |
| `tints` | `["#FF0000", "#00FF00"]` | Array of hex color strings. Faces with a `tintindex` look up their tint from this array |
| `shader` | `{ type: "end_portal", layers: 15 }` | Apply the end portal / end gateway shader to the model |
| `type` | `"block"`, `"item"` | Which texture atlas rules to enforce. Block-type models use only the manually provided display settings. Model-defined displays are ignored since they are meant to apply to items, not blocks |
| `ignore_atlas_restrictions` | `true` | Skip texture atlas membership checks for this model, letting it reference textures from any atlas |
| `light_emission` | `15` | Emission floor (0-15) for every element in the model, on top of the block's own in-game glow. Elements with a higher [`light_emission`](rendering.md#lighting-modes) keep theirs, lower ones are raised. Unlike the element field this isn't gated to 1.21.2+, and the `emission` render option still overrides it outright. See [Lighting modes](rendering.md#lighting-modes) |
| `version` | `"26.3"` | Minecraft version the model is for. Enables era-appropriate behavior, see [Legacy Minecraft versions](versions.md#legacy-minecraft-versions) |

### Element JSON

| Field | Example | Description |
|---|---|---|
| `part` | `"lid"` | Tag this element as one of a [`dynamic`](#model-json) model's moving pieces, using its kind's [part names](#dynamic-models). Any number of elements can share a name: each is posed around its own pivot. An element with a `rotation` pivots at its rotation `origin`, otherwise set `pivot` |
| `pivot` | `[8, 8, 7]` | The pivot point (model coordinates) for a `part` element that has no `rotation` |

### Dynamic models

The kinds a model can declare with `dynamic`, and the `part` names each animates. The set is fixed: these drive the game's block entity animations, not custom ones. What a pack *can* do is remodel one of these blocks with its own geometry and tag the moving elements, and they keep animating. How the animations run and pose live is covered in [Dynamic models](scenes.md#dynamic-models).

| Kind | Parts | Animation |
|---|---|---|
| `"banner"` | `flag` | Waves on the game's 100-tick cycle, phase-offset per block position |
| `"bell"` | `bell_body` | Swings with the game's decaying oscillation when rung via `.ring(direction?)` |
| `"chest"` | `lid` | Rotates 90° around its hinge via `.open()`/`.close()`, eased like the game |
| `"decorated_pot"` | `pot` | Wobbles via `.wobble(style?)`: the positive tilt or the negative shake |
| `"dragon_head"` | `jaw` | The jaw chatters continuously |
| `"enchanting_book"` | `cover_left`, `cover_right`, `book_spine`, `pages_left`, `pages_right`, `flipping_page_left`, `flipping_page_right` | Plays the full enchanting table animation against the render camera |
| `"piglin_head"` | `left_ear`, `right_ear` | The ears flap continuously |
| `"shulker_box"` | `lid` | Lifts 8 voxels and twists 270° via `.open()`/`.close()`, linear like the game |

### Blockstate JSON

| Field | Example | Description |
|---|---|---|
| `allow_invalid_rotations` | `true` | Allow variant `x`/`y`/`z` rotation values that aren't multiples of 90 |
| `light_emission` | `15` | Top level, alongside `variants`/`multipart`. Passes the [model `light_emission`](#model-json) floor to every model the blockstate picks, so a block glows without its models carrying the field. Used for the glow item frame, which the game draws fullbright without emitting light |

### Item components

Extra fields that can be passed through the `components` arg on [`renderItem`](standard-api.md#renderitemargs), or the `data` arg on [`parseItemDefinition`](scenes.md#parseitemdefinitionassets-id-args). These aren't real Minecraft item components, they stand in for runtime context that the game would normally provide:

| Field | Example | Description |
|---|---|---|
| `team` | `"red"` | Team color context used by the `team` tint source |
| `context_entity_type` | `"pig"` | The entity type holding the item, used by `context_entity_type` selects |
| `context_dimension` | `"the_nether"` | The dimension the item is rendered in, used by `context_dimension` selects |

Any future non-component select properties vanilla adds will work without renderer updates. The renderer looks up the property by name in `components` and checks whether its value equals any of the select's listed cases, so as long as the property is a plain string and you pass it in `components`, it resolves correctly.

## Default blockstates

Blockstate properties you don't pass to [`renderBlock`](standard-api.md#renderblockargs) are filled in for you, and the filled values merge with whatever `blockstates` you do provide, per property. There are two tables behind that, because "the caller didn't say" and "the data left it out because it was default" are different questions:

```
assets/block-model-renderer/default_blockstates.json            every block's real default state
assets/block-model-renderer/default_blockstates_preferred.json  overrides for how blocks look
```

The base file is each block's actual default state, the one the game gives a freshly placed block. The preferred file sits on top of it and holds only the values the renderer would rather show: stairs facing the camera, crops fully grown, vines with a face attached. `defaults: "game"` skips the preferred overlay, for data whose format omits properties that are at their default (26.3+ structure and worldgen files). Pass it per call to [`parseBlockstate`](scenes.md#parseblockstateassets-id-args), [`renderBlock`](standard-api.md#renderblockargs) or [`createScene`](scenes.md#createsceneassets-blocks-args), or pin it for everything sharing one assets instance with [`prepareAssets(sources, { defaults: "game" })`](assets.md#default-blockstate-mode). Both files are pack files, so any pack can extend or override them:

```json
{
  "properties": {
    "facing": "north",
    "half": "bottom"
  },
  "blocks": [
    { "match": "*_stairs|*_glazed_terracotta", "defaults": { "facing": "south" } },
    { "match": "my_mod_block", "defaults": { "open": true } }
  ]
}
```

* `properties` are per-property fallbacks used for any block. In the preferred file a value can be an array of candidates tried in order (the first one the blockstate actually has wins); the base file always uses a single value so a lookup has exactly one answer
* `blocks` is an ordered rule list. `match` matches block ids with `*` wildcards and `|` alternatives; within one file, the first matching rule's `defaults` are used whole
* Files from every pack merge, higher packs win: per property for `properties`, and higher packs' rules go first for `blocks` (a matching rule in a higher pack completely replaces lower ones)
* Across the two files the merge is per property instead, so a preferred rule only needs the values it wants to change and the base still supplies the rest of that block's properties
* The library's own tables ship in its [bundled fallback pack](assets.md#bundled-packs) at the very bottom of the stack, so anything a pack defines beats them

Declare a modded block in the base file: that one entry serves both modes. Only reach for the preferred file when the block's real default and its best-looking default genuinely differ.

Lookup order for a property: the `blockstates` option → the first matching preferred `blocks` rule → the first matching base `blocks` rule → preferred `properties` → base `properties`, with the two preferred tiers skipped in `defaults: "game"`.

The base file is generated from the game by `tools/generate/generate.js`, so it covers every vanilla block for the version it was built from. Blocks it doesn't know fall through to `properties`, which means an unrecognised modded block still resolves rather than failing.

## Block data and colors

The renderer's built-in block behavior data and color tables are pack files too, resolved through the same layered stack, so any pack can extend or override them:

```
assets/block-model-renderer/waterlogging.json
assets/block-model-renderer/culling.json
assets/block-model-renderer/lighting.json
assets/block-model-renderer/offsets.json
assets/block-model-renderer/colors.json
assets/block-model-renderer/items.json
```

### `waterlogging.json`, `culling.json`, `lighting.json`, `offsets.json`, `items.json`

Id-matching rules for block and item behaviors, split by concern:

| File | Sections |
|---|---|
| `waterlogging.json` | `waterloggable` (the `waterlogged` property works), `waterlogged` (inherently water-filled, like kelp) |
| `culling.json` | `nonOccluding` (never hides neighbor faces), `selfCullAll` (culls against its own kind on all sides), `selfCullY` (vertically, plus connected sides: panes, bars) |
| `lighting.json` | `lightEmission` (in-game light levels), `shapeLightOcclusion` (light blocked by model shape rather than as a full cube: slabs, stairs), `lightDampening` (levels a block dims passing light: leaves, fluids, tinted glass), `aoBlocking` (crowds ambient-occlusion corners without fully occluding) |
| `offsets.json` | `horizontalOffset` (the furthest, in blocks, a block shifts sideways with [`randomOffset`](scenes.md#createsceneassets-blocks-args)), `verticalOffset` (the furthest it sinks) |
| `items.json` | `alwaysGlint` (items that render the enchantment glint without being enchanted: enchanted golden apple, nether star, debug stick...) |

The boolean sections each hold one rule of the form `{ "suffix": [...], "exact": [...], "except": [...] }`: an id matches by exact name or by suffix, unless listed in `except`. The `lighting.json` and `offsets.json` sections are ordered rule lists where each rule adds a `value`: a flat level, or `{ "default": n, "cases": [[{ "prop": "value" }, level], ...] }` resolved against the blockstate properties.

```json
{
  "lightEmission": [
    { "value": { "default": 0, "cases": [[{ "on": "true" }, 15]] }, "suffix": [], "exact": ["my_mod_lamp"] }
  ]
}
```

Every pack's file applies. The boolean sections are additive: a block matches when any pack's rule matches it. The valued sections consult higher packs first, so a pack can both add modded blocks and override the built-in level of a vanilla one. The built-in data (generated from the game) sits at the bottom of the stack, and already matches loosely on purpose: modded ids like `ruby_torch` or `crystal_lantern` pick up sensible values from the suffix rules without any pack file at all.

### `colors.json`

The tint tables, in the same shape as [`COLORS`](models.md#colors): `colormap`, `fixed`, `indexed`, `tintindex`, `dye`, `effects`, `potions`, and `team`. Files merge per entry with higher packs winning, so `{ "fixed": { "birch_leaves": "#FF0000" } }` recolors birch leaves and touches nothing else. Listing a block under a `colormap` map assigns it to that map, overriding any lower pack's assignment.

Pack overrides apply to what renders; the assets-free helpers ([`getLightEmission`](models.md#getlightemissionid-properties-resolvedefault), [`isWaterloggable`](models.md#iswaterloggableid), [`isWaterlogged`](models.md#iswaterloggedid), [`fluidTypeOf`](fluids.md#fluidtypeofid-properties), [`COLORS`](models.md#colors)) keep reporting the built-in data, since they have no pack stack to consult.

## Custom model loaders

Mods extend the model format in their own ways: extra keys on standard models, embedded mesh data, references to other formats entirely. `ModelLoader` lets you plug that in without forking the pipeline. Loaders are global and identical on Node and in the browser, and consulted by `priority` (a number, default `0`, higher first; ties keep registration order), read live so setting `loader.priority` any time takes effect on the next render.

```js
import { ModelLoader } from "block-model-renderer"
```

| `ModelLoader` method | Description |
|---|---|
| `register(loader)` | Register a loader (the object described below). Returns the loader |
| `remove(loaderOrName)` | Unregister by the loader object or its `name`. Returns `true` if one was removed |
| `list()` | The registered loaders in consultation order (`priority` first, then registration order) |

(There's also `ModelLoader.variantKey(model, block)`, a helper for callers that cache built models themselves; see [Placement-aware models](#placement-aware-models).)

Say a pack ships an otherwise-vanilla model with a custom key on it:

```json
{
  "textures": { "all": "block/stone" },
  "elements": [
    {
      "from": [0, 0, 0],
      "to": [16, 16, 16],
      "faces": { "down": { "texture": "#all" }, "up": { "texture": "#all" }, "north": { "texture": "#all" }, "south": { "texture": "#all" }, "west": { "texture": "#all" }, "east": { "texture": "#all" } }
    }
  ],
  "my_mod:overlays": [
    { "texture": "block/glowstone", "size": [8, 8, 8] },
    { "texture": "block/ice", "size": [16, 16, 16] }
  ]
}
```

To handle that key, you register a loader: a plain object that claims it and builds a box from each overlay, added alongside the standard `elements` (the base cube still renders) rather than in place of them (`replaceElements` opts into that):

```js
ModelLoader.register({
  name: "my_mod:overlays",
  priority: 10,

  // claim a custom model json key and merge it across the parent chain yourself
  mergeKey(key, values) {
    // values is this key from every layer (child first); concatenate the lists
    if (key === "my_mod:overlays") {
      return values.flat()
    }
  },

  // which resolved models this loader builds for
  match(model) {
    return !!model["my_mod:overlays"]
  },

  // build geometry from the model data this loader claimed
  async build({ group, model, helpers }) {
    // one box per overlay in the merged list, from its own texture and size
    for (const overlay of model["my_mod:overlays"]) {
      const material = await helpers.createMaterial(overlay.texture)
      const [w, h, d] = overlay.size
      group.add(new helpers.THREE.Mesh(new helpers.THREE.BoxGeometry(w, h, d), material))
    }
  },

  // if build varies by placement, key the variants apart so caches don't share them
  variantKey(model, block) {
    return block ? `facing_${block.properties?.facing}` : null
  }
})
```

### Fields

Everything a loader object can hold. The hooks are detailed below.

| Field | Description |
|---|---|
| `name` | Optional string identifying the loader, so `ModelLoader.remove(name)` works by name |
| `priority` | Number, default `0`. Higher is consulted first; ties keep registration order. Read live, so changing it takes effect on the next render |
| `replaceElements` | If `true`, suppresses the standard `elements` build for matched models, so the format fully owns its geometry instead of adding alongside |
| [`mergeKey`](#mergekeykey-values-merged-stack) | Optional. Claim and merge a custom model json key across the parent chain |
| [`match`](#matchmodel) | Which resolved models this loader builds for |
| [`build`](#build-group-model-assets-args-block-helpers) | Optional. Add your own three.js geometry to a matched model |
| [`variantKey`](#variantkeymodel-block) | Optional. Key placement variants apart so caches don't share geometry |

### `mergeKey(key, values, merged, stack)`

Optional, may be async. Called for **every** model json key during parent-chain merging. Return a value to own the key, or `undefined` to let the vanilla merge handle it. A key you claim stays on the resolved model, including `parent` and `model` (otherwise stripped as plumbing), which is how the Forge OBJ convention keeps its `.obj` path readable in `match`/`build`.

| Argument | Description | Example |
|---|---|---|
| `key` | The model json key being merged | `"my_mod:overlays"` |
| `values` | That key's value from every layer of the parent chain, child first. It holds all levels, so merging them loses nothing | `[[{ texture: "block/glowstone", size: [8, 8, 8] }], [{…}]]` |
| `merged` | The sibling fields resolved so far (`type`, `model`, `x`/`y`/`z`, etc.), for context. `merged[key]` isn't set yet | `{ type: "block", model: "block/glowstone" }` |
| `stack` | The raw json layers of the parent chain, child first. Use it when a key should only count from files that opted into your format | `[{ parent: "block/cube_all", "my_mod:overlays": [{…}] }, { elements: [{…}] }]` |

Placement decisions don't belong here: resolved models are cached per reference, so the merge must stay placement-independent. Pick variants in `build`.

### `match(model)`

Which resolved models this loader builds for. Return `true` to have `build` (and `variantKey`) run for that model.

| Argument | Description | Example |
|---|---|---|
| `model` | The resolved model data, after merging | `{ type: "block", "my_mod:overlays": [{…}], textures: {…} }` |

### `build({ group, model, assets, args, block, helpers })`

Optional, may be async. Adds three.js objects to the model's group. Runs after the standard elements build, so vanilla elements and loader geometry coexist; display transforms, lighting modes, and mirroring all apply automatically. Geometry added here participates in [culling masks](culling.md#culling-hidden-faces) (occlusion rasterizes real triangles), and meshes can tag `userData.cullface = [dir]` to make their faces droppable per placement like element faces.

| Property | Description | Example |
|---|---|---|
| `group` | The `THREE.Group` to add your objects to | `new THREE.Group()` (empty) |
| `model` | The resolved model data | `{ type: "block", "my_mod:overlays": [{…}] }` |
| `assets` | The prepared assets | `[…]` (prepared assets) |
| `args` | The render args | `{ width: 128, height: 128, lighting: "world" }` |
| `block` | Placement context `{ id, properties, pos, neighbors }`, or `null` when the caller gave no placement info. See [Placement-aware models](#placement-aware-models) | `{ id: "minecraft:oak_stairs", properties: { facing: "east" }, pos: [4, 64, -2], neighbors: ([x, y, z]) => … }` |
| `helpers` | Helpers that keep loader geometry consistent with the rest (below) | `{ THREE, createMaterial, … }` |

The `helpers` object:

| Helper | Description |
|---|---|
| `THREE` | The three.js instance the library uses |
| `lighting` | The active [lighting mode](rendering.md#lighting-modes) |
| [`readFile(path, hint?)`](assets.md#readfilepath-assets-hint) | Read any file from the asset stack (obj files, custom json, whatever the format needs) |
| `loadTexture(id, tint?)` | Load a texture by id with the standard caching, animation frames, and optional tint |
| `resolveTexture(ref)` | Follow `#slot` references through the model's texture map |
| `buildElements(elements)` | Run an array of vanilla-format elements through the standard cube pipeline (uv defaults, face rotation, uvlock, cullfaces, rotation with rescale, mesh merging) and get back a group to add. For formats that are "vanilla elements, chosen differently", so they don't reimplement cube building |
| `createMaterial(id, opts?)` | A material matching the active lighting mode. `opts`: `tint`, `shade` (false = unshaded in world mode, the pre-26.3 element field), `shade_direction` (shade as if facing this direction, the 26.3+ replacement; see [Legacy Minecraft versions](versions.md#legacy-minecraft-versions)), `double_sided`, `light_emission` (0-15, the element emission floor as in [Lighting modes](rendering.md#lighting-modes)), `shader` |

### `variantKey(model, block)`

Optional. A loader whose output varies by placement returns a short string (e.g. `"connected_east"`) identifying which variant a placement gets, so cached or instanced meshes don't share geometry across variants. Consumers call `ModelLoader.variantKey(model, block)`, which combines the keys from every matching loader (or returns `null` when none vary) for use in their own cache keys.

| Argument | Description | Example |
|---|---|---|
| `model` | The resolved model data | `{ type: "block", "my_mod:overlays": [{…}] }` |
| `block` | Placement context `{ id, properties, pos, neighbors }`, or `null`. See [Placement-aware models](#placement-aware-models) | `{ id: "minecraft:oak_stairs", properties: { facing: "east" }, pos: [4, 64, -2], neighbors: ([x, y, z]) => … }` |

### Placement-aware models

Some formats build different geometry depending on where the block sits (connected textures, models that extend toward matching neighbors). The `block` argument to `build` and `variantKey` carries that context: `{ id, properties, pos, neighbors }`. `pos` is the block's world position, or `null` when the caller didn't give one.

`neighbors` looks up blocks relative to this one. Pass one offset for one block, or a list of offsets for a list of blocks in the same order. Each block is `{ id, ...properties }`, or `null` for air or anything outside what the caller knows about:

```js
const above = block.neighbors([0, 1, 0])
const [north, south] = block.neighbors([[0, 0, -1], [0, 0, 1]])
```

Offsets are world-space `[x, y, z]`, unaffected by the model's `x`/`y` rotation, and can reach any distance. In [`createScene`](scenes.md#createsceneassets-blocks-args) the lookup covers the whole scene, including `context` blocks. [`renderBlock`](standard-api.md#renderblockargs) and [`loadModel`](scenes.md#loadmodelscene-assets-model-args) answer from their `neighbors` arg, so callers there control what's visible. `block` is `null` when the caller gave no placement info, so loaders should fall back to a sensible default variant.

A loader whose output varies by placement must implement [`variantKey`](#variantkeymodel-block), reading `block.neighbors` the same way `build` does. [`createScene`](scenes.md#createsceneassets-blocks-args) builds one model per block state and reuses it for every placement, unless a loader with a `variantKey` claims one of the state's models: then each block's key picks which build it shares, and blocks with the same key share one. Only blocks those loaders claim pay for the lookup.

Two worked loaders ship as examples in [`examples/node/render_loader.js`](../examples/node/render_loader.js): a from-scratch polygon model format with concatenating inheritance, and the (Neo)Forge OBJ format (`"loader": "forge:obj"`, mtl materials resolving `#slot` textures, `flip_v`).
