Render Minecraft in the browser.

MineRender turns skins, blocks, items, entities, structures and worlds into interactive three.js scenes. Version 2 is a TypeScript rewrite that also runs in Node, loads assets from any source, and draws repeated models in a single call.

npm install minerender@alpha three

Upgrading? Read the V1 guide. The original minerender.org stays online.

Scroll here to load the preview
Show the code

What changed in version 2

The main differences from V1.

Examples

Each section renders one example at a time and pauses as soon as it scrolls out of view, so the whole page stays light. The code next to each preview is what produced it.

Usage

What you need for most integrations. The generated API reference covers the rest.

Install

three.js is a peer dependency. V2 is published under the alpha tag while the API settles.

npm install minerender@alpha three
# or
yarn add minerender@alpha three

Without a build step, load the bundle. It includes three.js and exposes everything as window.MineRender.

<script src="https://unpkg.com/minerender@alpha/dist/bundle.js"></script>
<script>
    const renderer = new MineRender.Renderer({ controls: { enabled: true } });
    renderer.appendTo(document.getElementById("render"));
    renderer.start();
</script>

Your first render

Create a renderer, attach it to an element that has a size, start the loop, then add content to its scene. Every add method is asynchronous because assets load on demand.

import { AssetKey, BlockStates, Renderer } from "minerender";

const renderer = new Renderer({
    camera: { position: [40, 30, 50], lookingAt: [0, 0, 0] },
    controls: { enabled: true }   // built-in orbit controls
});
renderer.appendTo(document.getElementById("render")!);
renderer.start();

// A player skin
await renderer.scene.addSkin("https://example.com/skin.png");

// A block, from its blockstate
const state = await BlockStates.get(AssetKey.parse("blockstates", "grass_block"));
const block = await renderer.scene.addBlock(state!);
block.setPosition(block.getPosition().set(32, 0, 0));   // 1 block = 16 units

// When the view goes away
renderer.dispose();

Renderer options

All options are optional and deep-merged over these defaults.

OptionDefaultDescription
camera.type"perspective"Or "orthographic".
camera.position[50, 50, 50]Initial camera position as a Vector3 or [x, y, z].
camera.lookingAt[0, 0, 0]Camera target, and the orbit centre when controls are enabled.
camera.near, camera.far1, 5000Clipping planes in scene units.
camera.perspective.fov50Field of view in degrees.
render.fpsLimit60Maximum frames per second. Zero or negative removes the cap.
render.antialiastrueWebGL antialiasing.
render.shadetrueMinecraft-style per-face shading on models.
render.autoResizetrueResize the canvas when the window resizes.
render.renderAlwaysfalseRender every frame instead of only when the scene is dirty.
render.statsfalseShow a stats.js overlay.
composer.enabledtrueRoute output through a post-processing composer. Disable for the fastest path.
controls.enabledfalseCreate renderer-owned orbit controls, available as renderer.controls.
debug.grid, debug.axesfalseAdd grid and axes helpers.

Scene and objects

renderer.scene is a three.js Scene with asset-aware add methods.

scene.addSkin(textureUrl?, options?)    // SkinObject
scene.addBlock(blockState, options?)    // BlockObject
scene.addModel(model, options?)         // ModelObject | InstanceReference
scene.addEntity(entityModel, options?)  // EntityObject | InstanceReference
scene.stats                             // object and instance counts

Scene objects extend Object3D. Beyond the usual three.js properties they offer setPosition, setRotation, setScale, getGroupByName, getMeshByName, toggleGroupVisibility, removeFromScene and disposeAndRemoveAllChildren.

Every add method accepts the same base options, plus type-specific ones.

{
    mergeMeshes: true,        // one geometry per model
    instanceMeshes: true,     // share an InstancedMesh per model
    maxInstanceCount: 50,     // slots per instanced model
    wireframe: false,         // debug outlines

    // models and blocks
    displayPosition: DisplayPosition.GUI,   // apply a display transform
    tints: { 0: 0x79c05a },                 // sRGB color per tint index

    // skins
    slim: undefined,          // true, false, or undefined to detect
    legacy: undefined,        // 64×32 layout; undefined to detect

    // entities
    flip: true                // vanilla models point down the Y axis
}

With instanceMeshes on, repeated models return an InstanceReference that only carries transforms. Pass instanceMeshes: false to get a standalone object you can remove on its own.

Redraws

Frames are drawn only when something changed. The built-in controls and the scene's add methods mark the scene dirty for you. Anything you change by hand needs a nudge.

skin.getGroupByName("head")!.rotation.y = 0.5;
renderer.dirty = true;

// Other event sources can request redraws too
renderer.registerEventDispatcher(myControls, "change");

Assets and versions

Assets are addressed by AssetKey and loaded through an ordered list of sources. The default source is a CDN with vanilla assets for the version in AssetLoader.ROOT.

import { AssetKey, AssetLoader, Caching, HostedAssetSource, Models } from "minerender";

// Keys
AssetKey.parse("blockstates", "minecraft:oak_log");
new AssetKey("minecraft", "diamond_sword", "models", "item", "assets");

// Another game version for everything that follows
AssetLoader.setVersion("1.20.4");

// Sources added later are searched first
AssetLoader.addSource("pack", new HostedAssetSource("https://cdn.example/pack"));

// In-memory caches
Caching.clear();
await Models.clearCache();

Resource-pack ZIPs work in the browser through BrowserArchiveProxy and ArchiveAssetSource; see the example. Persistent caches (IndexedDB in browsers, a directory in Node) are keyed by asset root, so versions never collide.

Lifecycle

stop() pauses the loop and start() resumes it. dispose() is final: it releases the WebGL context, the controls and owned helpers. Shared assets stay cached.

renderer.stop();
renderer.start();
renderer.dispose();

// In Node, or before unloading a page: end caches, queues and timers
import { shutdown } from "minerender";
shutdown();

To grab the current frame, render once and read the canvas.

renderer.renderer.render(renderer.scene, renderer.camera);
const dataUrl = renderer.toImage();

Node

The node export condition installs a platform provider backed by node-canvas and node-persist. Asset loading, model merging, atlas generation and structure parsing work in Node. The Node entry does not create a WebGL context, so drawing to an image there needs one from a package such as headless-gl.

import { AssetKey, AssetLoader, AssetParser, Models, StructureParser, shutdown } from "minerender";

const sword = await Models.getMerged(new AssetKey("minecraft", "diamond_sword", "models", "item", "assets"));
console.log(sword?.elements?.length, "elements");

const key = new AssetKey("minecraft", "igloo/top", "structure", undefined, "data", ".nbt");
const structure = await StructureParser.parse((await AssetLoader.get(key, AssetParser.NBT))!);
console.log(structure.size, structure.blocks.length, "blocks");

shutdown(); // let the process exit

Performance

  • One renderer per visible area. Browsers limit WebGL contexts. This page keeps at most two alive and swaps the rest for snapshots.
  • Dispose what scrolls away and recreate it on demand. Assets stay cached, so the second load is quick.
  • Leave instancing on for repeated models, and set maxInstanceCount above the number of copies you expect.
  • Disable the composer when you do not need post-processing.
  • Lower fpsLimit on busy pages. The dirty flag already skips idle frames.

Coming from V1

V1 had one renderer class per content type, each with its own canvas and options. V2 has one renderer and a scene you add things to. The table maps the concepts and the pairs below show the code.

V1V2
SkinRender, ModelRender, EntityRender, CombinedRenderOne Renderer; scene.addSkin, addBlock, addModel, addEntity
new XRender(options, element)new Renderer(options) then renderer.appendTo(element) and renderer.start()
render.render(...) with callbacksawait scene.addX(...) returns the object
options.assetRootAssetLoader.setVersion(), AssetLoader.addSource(), or a per-key root
options.controls, camera, showAxes, showGridcontrols.enabled, camera.position, debug.axes, debug.grid
render.toImage()renderer.toImage() after a render
render.clearScene(), render.dispose()object.removeFromScene(), renderer.dispose()
Per-frame skinRender CustomEventsMutate objects, then set renderer.dirty = true.
skinRender.playerModel.getObjectByName("head")skin.getGroupByName("head"), skin.getMeshByName("hat")
GuiRender, recipe(), capes, OBJ/glTF exportNot available in V2.

A skin viewer

V1
const skinRender = new SkinRender({
    controls: { enabled: true }
}, document.getElementById("skin"));
skinRender.render("inventivetalent");
V2
const renderer = new Renderer({ controls: { enabled: true } });
renderer.appendTo(document.getElementById("skin")!);
renderer.start();

const skin = await renderer.scene.addSkin();
const texture = await Skins.fromUuidOrUsername("inventivetalent");
if (texture) await skin.setSkinTexture(texture);

A block and an item

V1
const modelRender = new ModelRender({}, element);
modelRender.render([
    { blockstate: "grass_block" },
    { model: "item/diamond_sword", offset: [32, 0, 0] }
]);
V2
const state = await BlockStates.get(AssetKey.parse("blockstates", "grass_block"));
await renderer.scene.addBlock(state!);

const model = await Models.getMerged(AssetKey.parse("models", "item/diamond_sword"));
const sword = await renderer.scene.addModel(model!, { displayPosition: DisplayPosition.GUI });
sword.setPosition(sword.getPosition().set(32, 0, 0));

Animating a part

V1
element.addEventListener("skinRender", event => {
    const head = event.detail.playerModel.getObjectByName("head");
    head.rotation.y += 0.01;
});
V2
const head = skin.getGroupByName("head")!;
function tick() {
    head.rotation.y += 0.01;
    renderer.dirty = true;        // request a frame
    requestAnimationFrame(tick);
}
tick();

Resource packs and versions

V1
new ModelRender({
    assetRoot: "https://assets.mcasset.cloud/1.16.5"
}, element);
V2
AssetLoader.setVersion("1.16.5");

// or layer a pack on top of the vanilla assets
const archive = new BrowserArchiveProxy(zipFile);
AssetLoader.addSource("my-pack", new ArchiveAssetSource(archive));

V1 stays online for GUI and inventory rendering, recipes, capes and 3D export.