block-model-renderer

Minecraft block and item model rendering for Node.js and the browser.

Render any block, item, or custom model JSON, with full support for vanilla resource pack features.

Features

  • Renders blocks, items, and custom models from a resource pack
  • Runs on Node.js and in the browser from the same package
  • Full vanilla model, blockstate, item-definition, and texture atlas support, with accurate lighting and tints
  • Bundled overrides for block entities that Minecraft renders dynamically (banners, chests, heads, etc)
  • Stack resource pack folders, zips, and virtual handlers, with higher packs overriding lower ones just like in Minecraft
  • Animated textures: WebP and GIF output on Node, live self-updating canvases in the browser
  • Scene optimization: near game-accurate hidden-face culling from neighboring blocks, and the whole scene merged into a handful of draw calls, with far fewer polygons
  • Extensible model loaders: write your own to support modded formats (OBJ models, connected textures, etc)
  • PNG, JPEG, WebP, GIF, and AVIF output on Node

Basic Usage

Give renderBlock an id and an assets source and it returns a canvas, rendered with the same blockstate, model, and texture handling as the game. Everything below is rendered live on this page; try any block id yourself:

Loading…
import { renderBlock, prepareAssets } from "block-model-renderer"

// a resource pack zip from a file input, fetch, anywhere
const assets = await prepareAssets([resourcePackZip])

// returns a canvas you can append
const canvas = await renderBlock({ id: "grass_block", assets, width: 256, height: 256 })
document.body.append(canvas)

Blocks like chests, beacons, and enchanting tables don't have real models in the assets; Minecraft renders them dynamically in code. The bundled overrides fill those gaps so they render like the game shows them.

Blockstates

Pass blockstate property values to render a specific variant, exactly as they appear in the game's debug screen:

await renderBlock({ id: "cake", assets, blockstates: { bites: "3" } })
await renderBlock({ id: "oak_fence", assets, blockstates: { north: "true", east: "true" } })
await renderBlock({ id: "sea_pickle", assets, blockstates: { pickles: "3" } })

Rendering Items

renderItem goes through the pack's item definitions, so layered textures, tints, and model selection all behave like the game. Try any item id:

import { renderItem } from "block-model-renderer"

await renderItem({ id: "diamond_sword", assets })

// item components drive definition selection
await renderItem({ id: "bow", assets, components: { using_item: true } })

GUI Rendering

Whole interfaces can be put together from nothing but the pack. readFile reads any file from the pack stack, so the screen texture and the bitmap font below come straight from the loaded jar, and every item is rendered into its slot with placement mode. The crafting grid is even showing a real recipe, read from the jar's data files; randomise to roll another:

import { renderItem, readFile } from "block-model-renderer"

// any file in the pack stack is readable
const png = await readFile("assets/minecraft/textures/gui/container/crafting_table.png", assets)
ctx.drawImage(await createImageBitmap(new Blob([png])), 0, 0, 176, 166, 0, 0, 352, 332)

// including data files, like recipes
const recipe = JSON.parse(new TextDecoder().decode(await readFile("data/minecraft/recipe/cake.json", assets)))

// placement mode draws into a region of an existing canvas without clearing it
await renderItem({ id: "diamond_sword", assets, canvas, width: 32, height: 32, x: 248, y: 70 })

Animated Textures

With animated: true you get a live player instead of a canvas: a self-updating canvas that plays the model's texture animations, and pauses itself while scrolled offscreen:

const player = await renderBlock({ id: "magma_block", assets, animated: true })
document.body.append(player.canvas)

// screen-space shaders (the end portal) size their pattern to the viewport;
// shaderScale sets the density, like the end gateway above
await renderBlock({ id: "end_gateway", assets, animated: true, shaderScale: 0.5 })

// in your own scene, the uniforms are live on the material. the marquee at
// the top of this page runs on a wide canvas, so it sets both per resize
scene.traverse(obj => {
  if (obj.material?.uniforms?.Aspect) {
    obj.material.uniforms.Aspect.value = canvas.clientWidth / canvas.clientHeight
    obj.material.uniforms.Scale.value = 4
  }
})

Custom Models

renderModel renders a model JSON directly, no blockstate or item definition lookup. It supports every vanilla model feature, so it works with anything a resource pack could contain. When a static image isn't enough, loadModel puts the same models into your own three.js scene instead, which is how the windmill under Rotation spins live:

import { renderModel } from "block-model-renderer"

// a whole sea turtle, every face UV mapped from its entity texture
await renderModel({
  assets,
  model: {
    textures: { skin: "entity/turtle/turtle" },
    elements: [
      { // shell
        from: [-1.5, 3, -3],
        to: [17.5, 9, 17],
        faces: {
          up:    { texture: "#skin", uv: [4.75, 10.75, 7.125, 15.75] },
          north: { texture: "#skin", uv: [1.625, 9.25, 4, 10.75] },
          west:  { texture: "#skin", uv: [0.875, 10.75, 1.625, 15.75], rotation: 90 } // …
        }
      },
      { from: [5, 1, -9], to: [11, 6, -3],  faces: { /* head */ } },
      { from: [-10, 2, -2], to: [3, 3, 3],  faces: { /* 4 flippers */ } } // …
    ]
  }
})
// any shape you can describe with vanilla elements works: a simple chair
await renderModel({
  assets,
  model: {
    textures: { planks: "block/oak_planks", log: "block/stripped_oak_log", wool: "block/red_wool" },
    elements: [
      { from: [3, 0, 3], to: [5, 7, 5],           faces: { /* front leg */ } },
      { from: [2.5, 7, 2.5], to: [13.5, 8, 13.5], faces: { /* seat */ } },
      { from: [2.5, 8, 2.5], to: [13.5, 9, 13.5], faces: { /* cushion */ } },
      { // reclined backrest post
        from: [3, 9, 11],
        to: [5, 21, 13],
        rotation: { angle: 7.5, axis: "x", origin: [8, 9, 11] },
        faces: { /* … */ }
      }
    ]
  }
})
import { makeModelScene, resolveModelData, loadModel } from "block-model-renderer"

// elements rotate about their pivot, and can span the full -16..32 range,
// so this windmill is three blocks tall. split the model in two and load the
// sails separately, and you can spin them live around the axle
const statics = { ...windmill, elements: windmill.elements.filter(e => e.name !== "rotate") }
const sails = { ...windmill, elements: windmill.elements.filter(e => e.name === "rotate") }

const { scene, camera } = await makeModelScene()
await loadModel(scene, assets, await resolveModelData(assets, { model: statics }), { display })
const sailRoot = await loadModel(scene, assets, await resolveModelData(assets, { model: sails }), { display })

// wrap the sails in a group pivoted on the axle (x 8, y 16 in model space)
const spin = new THREE.Group()
const inner = new THREE.Group()
spin.position.y = 8
inner.position.y = -8
inner.add(...sailRoot.children[0].children[0].children)
spin.add(inner)
sailRoot.children[0].children[0].add(spin)

// in your render loop:
spin.rotation.z -= dt * 0.6
renderer.render(scene, camera)

Building Scenes

The low-level API loads models straight into your own three.js scene, so you can build viewers, editors, and whole worlds around it. Each block is culled against its neighbors with getCullFaces, fluids shape their surfaces from the blocks around them (waterfalls, flowing rivers, waterlogged blocks), and optimizeScene merges everything into a handful of draw calls. World lighting runs on a live day/night clock, with light-emitting blocks keeping their in-game glow. Everything here is live; drag to rotate:

import { getThree, parseBlockstate, resolveModelData, loadModel, createAnimator } from "block-model-renderer"

const THREE = await getThree()
const group = new THREE.Group()

for (const model of await parseBlockstate(assets, "campfire", { data: { lit: "true" } })) {
  const resolved = await resolveModelData(assets, model)
  await loadModel(group, assets, resolved, { lighting: "world", animate: true })
}
scene.add(group)

const animator = createAnimator(group)
// in your render loop:
animator.update()
import { computeSceneLight, getCullFaces, optimizeScene } from "block-model-renderer"

// flood-fills block and sky light the way the game does: torches and
// campfires glow, light wraps around corners, and interiors darken
const light = await computeSceneLight(blocks, { assets })

const placements = []
for (const block of blocks) {
  // faces hidden by neighboring blocks get culled, like in the game
  const cull = await getCullFaces({ id: block.id, blockstates: block.properties, neighbors: block.neighbors, assets })
  const group = new THREE.Group()
  // a per-position seed picks weighted variants, so grass and stone vary like in the game
  for (const model of await parseBlockstate(assets, block.id, { data: block.properties, seed: block.x | block.y << 8 | block.z << 16 })) {
    const resolved = await resolveModelData(assets, model)
    await loadModel(group, assets, resolved, { lighting: "world", animate: true, cull, neighbors: block.neighbors, block, light })
  }
  placements.push({ pos: [block.x, block.y, block.z], group })
}

// merge the whole scene into a handful of draw calls, with far fewer polygons
const optimized = await optimizeScene(placements)
light.setOffset(optimized.group.position)
scene.add(optimized.group)
12:00
// bigger scenes work exactly the same way: the waterfall and river shape
// themselves from their neighbors, every hidden face gets culled, and the
// whole valley still merges down to a handful of draw calls
const optimized = await optimizeScene(placements)
console.log(`${optimized.drawCalls} draws, ${(optimized.tris / 1000).toFixed(1)}K tris`)

// world lighting runs on a shared day/night clock: seed every block's group
// with one uniform and the entire scene follows it live, with light sources
// (the campfire, lanterns, glowing plants) keeping their in-game glow
const daytime = { value: 6000 } // noon, in ticks
group.userData.daytime = daytime
// later, from the slider:
daytime.value = 18000 // midnight

Resource Packs

Asset sources stack just like in Minecraft: pass more than one and higher packs override lower ones. Load a resource pack (or mod jar) and every render on this page rebuilds with it layered over vanilla, right down to the GUI texture, the font, and the recipes. Anything it adds in its own namespaces shows up too, so a mod's custom blocks and items appear in the pickers:

// higher packs override lower ones, just like in Minecraft
const assets = await prepareAssets([resourcePackZip, vanillaJar])