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
  • Game-accurate world lighting: torches glow and light up spaces, with working day/night cycles
  • 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

renderBlock takes a block id and an assets source and returns a canvas. Try any block id:

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 the game draws in code, like chests, beacons, and enchanting tables, render through bundled overrides.

Blockstates

Pass blockstate property values to render a specific variant:

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 uses the pack's item definitions, so layers, tints, and model selection match 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

readFile reads any file in the pack stack, so the screen texture, font, and recipe below all come from the loaded jar, with each item rendered into its slot:

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

animated: true returns a player: a self-updating canvas that pauses while offscreen:

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

// shaderScale sets the density of the end portal pattern
await renderBlock({ id: "end_gateway", assets, animated: true, shaderScale: 0.5 })

// in your own scene, set the pattern's uniforms when the canvas resizes
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, with every vanilla model feature. loadModel puts the same models into a three.js scene, which is how the windmill spins:

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 can span -16 to 32, so this windmill is three blocks tall.
// loading the sails separately lets them spin
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 })

// pivot the sails 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

loadModel puts a block into your own three.js scene. createScene builds a whole block list in one call: hidden faces culled, fluids shaped by their neighbors, world lighting on a live day/night clock, and everything merged into a handful of draw calls. 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 { createScene } from "block-model-renderer"

// culls, shapes fluids, lights, and merges the scene in one call
const handle = await createScene(assets, [
  { id: "grass_block", pos: [0, 0, 0] },
  { id: "campfire", properties: { lit: "true" }, pos: [0, 1, 0] },
  { id: "water", pos: [1, 1, 0] },
  // ...
])
scene.add(handle.group)
12:00
const handle = await createScene(assets, blocks)
console.log(`${handle.drawCalls} draws, ${(handle.tris / 1000).toFixed(1)}K tris`)

// the whole scene follows one live clock, in ticks
handle.group.userData.daytime.value = 18000 // midnight

The Sky

createSky draws the game's sky behind any three.js scene: the day/night gradient, the pack's sun and moon, stars, and the sunrise glow, on the same clock as the world lighting. createClouds adds the cloud layer. Drag to look around:

00:00
import { createSky, createClouds } from "block-model-renderer"

// sharing the scene's daytime keeps the sky and lighting in step
const sky = await createSky(assets, { daytime: handle.group.userData.daytime })
scene.add(sky.group)

const clouds = await createClouds(assets, { daytime: handle.group.userData.daytime, height: 128 })
scene.add(clouds.group)

Resource Packs

Asset sources stack like in Minecraft, with higher packs overriding lower ones. Load a resource pack or mod jar and every render on this page rebuilds with it, including a mod's own blocks and items:

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