MineRender V2 is a from-scratch TypeScript rewrite of MineRender, a three.js-based library for interactive 3D renders of Minecraft content: player skins, block/item models, entities, GUIs, and structures/worlds. Published on npm as minerender; V2 is in beta.
V2's goals beyond V1 parity: cleaner code, better performance, usable from both browser and server-side Node, and large-scale rendering of full Minecraft worlds (V1 topped out at structure files).
See ROADMAP.md for the V1→V2 feature-parity matrix, the full known-bug list, and the ordered continuation plan. Start there before picking up work.
The private Yarn workspace root contains the public library and its consumers. Unless a path starts with apps/, examples/, or packages/, library paths below (including src/, test/, dist/, and tsup.config.ts) are relative to packages/minerender/. Run the documented Yarn commands from the repository root.
| Repo / service | Role |
|---|---|
packages/minerender/ |
Public minerender library, moved from the V2 repository root. Browser, Node, and IIFE delivery formats retain their package paths. |
apps/web/ |
MineRenderWeb demo/test pages, built with esbuild against the library workspace. |
apps/embed/ |
Scene-driven iframe page at /embed/, built separately with esbuild and configured as its own Cloudflare Worker. See its README and URL reference. |
apps/api/ |
Node HTTP rendering service. POST /v1/renders accepts scene documents; metadata and PNGs live in a bounded, expiring memory cache. Pooled worker processes reuse asset caches between renders. See its README for the request contract and configuration. |
apps/site/ |
V2 website (Vite + TypeScript, no framework): feature overview, live examples, usage docs. Examples live in src/examples/*.ts; src/viewport/ lazily creates renderers and caps how many are alive at once. See its README. |
examples/vite/ |
Vue 3 + Vite consumer, imported from MineRender/example-vite. Uses ESM named imports and a workspace dependency. |
examples/script-tag/ |
Plain HTML consumer, imported from MineRender/example-bundle. Loads the library's IIFE as MineRender. |
InventivetalentDev/MineRender (V1 checkout: MineRenderV1) |
master contains V1 (JS, webpack 4, three 0.93, browser-only), the feature-parity reference. V2 development shares this repository on typescript and the stacked refactor branches. Preserve V1 tags, bundles, and website URLs. |
InventivetalentDev/MineRenderServer |
V1-era headless render HTTP API (Express + headless-gl + patched node-canvas + three-png-stream under xvfb). Historical reference only; not imported. Its GET /render/skin/[:texture], GET /render/model/:type/:model, minerender-options header, and MD5-keyed PNG cache are not compatibility requirements for the V2 API. |
| minecraft-entity-models | Per-model entity and block-entity geometry at assets.mcasset.cloud/<version>/entity-models/<namespace>/<id>.json. Registry and rendering data load separately from minerender-data. |
| minerender-data | Versioned item defaults, potion colors, block states, block tints, fluids, dyes, and legacy mappings at assets.mcasset.cloud/<version>/minerender-data/. |
assets.mcasset.cloud |
Primary vanilla-asset CDN, defaults to MC 1.21.11 in src/assets/AssetDefaults.ts (DEFAULT_ROOT). Provides synthetic _list.json directory indexes that getList() APIs depend on. |
minecraft-skin-proxy.inventive.workers.dev |
Own Cloudflare worker for CORS-safe skin/cape/UUID lookups (src/skin/Skins.ts); also api.mineskin.org, api.capes.dev. |
-
Runtime: Node.js 22.12+, or a browser with WebGL 2, native Fetch, and AbortController. Node builds target Node 22.
-
Package manager: yarn 4 (
packageManager: yarn@4.5.3, corepack),nodeLinker: node-modules. Do not use npm here. -
Install dependencies on the OS that runs the build; esbuild, Rollup, and
canvasuse platform-specific binaries. -
Root commands delegate to the workspaces:
Command Scope yarn buildBuild the library, web demos, and both examples. yarn build:libBuild the public library only. yarn build:api,yarn start:api,yarn test:apiBuild the library and HTTP service, run the built service, or test its HTTP contract. yarn build:embed,yarn dev:embed,yarn test:embedBuild the library and iframe app, start it at http://127.0.0.1:3001/embed/, or test its URL contract and loading.yarn test:embed:browserAfter yarn build:embed, test iframe lifecycle and sandboxed storage behavior in Chrome. SetCHROME_PATHoutside the default macOS installation.yarn testRun the library's AVA tests. yarn test:nodeAfter yarn build:lib, test native WebGL 2 rendering and Node package exports withcanvasandgl; usexvfb-run -a yarn test:nodeon headless Linux with the platform dependencies installed.yarn typecheckTypecheck the library and Vite example. yarn docGenerate the shared, browser, and Node API reference and build VitePress into packages/minerender/docs/.yarn doc:dev,yarn doc:previewGenerate the API and start docs development, or serve the built docs. yarn deploy:docs,yarn deploy:docs:previewBuild the docs and deploy the minerender-docsCloudflare Worker (reference/wrangler.jsonc), or upload a non-promoted preview version.yarn dev:web,yarn dev:vite,yarn dev:script-tag,yarn dev:siteStart the corresponding local consumer. yarn build:siteBuild the library and the V2 website into apps/site/dist/. -
Author docs in
packages/minerender/reference/;reference/api/is generated and ignored.scripts/typedoc-navigation.mjsgroups symbols by declaration path and omits inherited three.js members while preserving MineRender inheritance.yarn workspace minerender doc:generaterefreshes only the API pages; setDOCS_BASE(for exampleDOCS_BASE=/v2/docs/ yarn doc) to host the build under a subdirectory. -
tsup builds the library (
tsup.config.tsexports five passes; Rollup only appears through the declaration build). The web demos use esbuild, and the Vue example uses Vite.Pass Entry Output Notes browsersrc/index.browser.tsdist/browser/index.{js,d.ts}CJS, deps external browser-esmbrowser source modules dist/browser/esm/**/*.mjs,index.browser.d.mtsESM per module, entry index.browser.mjs, deps externalbrowser-workersrc/env/browser/section.worker.tsdist/browser/esm/env/browser/section.worker.mjsbundled ESM worker nodesrc/index.node.tsdist/node/index.{js,mjs,d.ts,d.mts}canvasandglexternal (optional deps)iifesrc/index.browser.tsdist/bundle.jsglobalName: MineRender, everything inlined incl. three, node-polyfilled for prismarine-nbt'szlibpackage.jsonexportsroutesbrowser/nodeconditions to the matching build;dist/bundle.jskeeps its historical path for unpkg/script-tag consumers. The ESM worker is emitted atdist/browser/esm/env/browser/section.worker.mjs, next toBrowserEnv.mjs, soBrowserEnv.createWorkercan load it relative toimport.meta.url; bundlers that pre-bundle the package without that file fall back to synchronous section builds. -
Only
src/util/_internal/merge.tsimportsts-deepmerge, so the per-module build inlines one copy.ts-deepmergeis deliberatelynoExternal(inlined): it is CJS exporting{default: fn}, which Node's ESM loader will not unwrap, soimport merge from "ts-deepmerge"breaks in.mjsoutput unless esbuild does the interop at build time. -
package.jsonsideEffectslets bundlers drop unused per-module files. Barrelindexfiles stay listed because esbuild omits the initializer of a module reached only through a side-effect-free re-export file when the bundle contains a localimport()../src/**stays listed so tsup's bundled passes and theesbuild-runnertests never prune library sources. -
yarn testruns AVA tests fromtest/*.test.tsthroughesbuild-runner; native tests undertest/node/run separately throughyarn test:node. Follow the existing test style and cover behavior that can regress. -
Publish only the
minerenderworkspace: useyarn publish:alphafor development releases andyarn publish:betafor selected milestones. Both commands build and increment the current prerelease version; set its suffix explicitly when switching channels. Commit and tag the release separately. The package includesdist, README, and LICENSE. -
src/index.tsis autogenerated byscripts/make-exports.sh— the shared barrel, and everything in it must work on both platforms.src/env/is excluded on purpose (see the Env seam below), as are the two entry files. Don't hand-edit it beyond regenerating (yarn workspace minerender exports).
Rendering core (src/renderer/): render.pixelRatio (default 1) scales the drawing buffer and exported images; layout and inspector picking use CSS pixels. Renderer owns a WebGLRenderer and an optional postprocessing EffectComposer and a dirty-flag render loop — frames render only when dirty (or options.render.renderAlways). MineRenderScene extends THREE.Scene with asset-aware addModel/addBlock/addSkin/addEntity/addGui. SceneObject extends Object3D is the base for ModelObject, BlockObject, SkinObject, EntityObject, and GuiObject; it provides named-group/mesh addressing (group:head, mesh:head via getGroupByName/getMeshByName) and the instancing plumbing.
Node rendering (src/env/node/NodeRenderer.ts): The Node entry exports NodeRenderer.create({ width, height, ...options }) for an owned native WebGL 2 context through optional gl@9.0.0-rc.10 (experimental WebGL 2). The factory disables antialiasing and rejects an explicit true; render.pixelRatio increases image resolution. new Renderer(options, surface) and new NodeRenderer(options, surface) accept a caller-owned RendererSurface outside the deep-merged options: canvas, context, positive integer logical dimensions, and an optional native resize callback receiving physical dimensions. Alpha-enabled contexts injected into NodeRenderer require premultipliedAlpha: true. Node renderers disable stats, controls, and automatic window resizing, and reject start()/toImage(); use renderOnce() or renderToBuffer(). See platform setup.
Scene documents (src/scene/): Versioned SceneDocument JSON is validated before any asset requests. SceneDocumentLoader loads each object without instancing inside its own nested DocumentObjectScene under a transform Group. A document attaches to its parent only after every object loads successfully. Entity animations accept either one name or a nonempty clips list of names and optional nonnegative weights, with shared loop, speed, time, and pause settings. Item definitions and GUI item layers accept an optional context (SceneItemContext: components, properties, count, item references by item ID, and displayContext on definitions) that the loader passes to Models.getMerged. Async encodeSceneDocument / decodeSceneDocument validate and convert portable links using v1. plus zlib-wrapped deflate in unpadded base64url; compressed data and UTF-8 JSON are each limited to 1 MiB.
Embeds (apps/embed/): One /embed/ page accepts exactly one shorthand (skin, block, item, entity, model), encoded #scene, or HTTPS src. URL camera/version values override document values; missing cameras fit the scene. Documents use the shared scene schema and a 1 MiB payload limit. Drawing buffers are limited to 4,096 pixels per side and 4,194,304 pixels in total independently of iframe size. Fragment changes replace the scene and discard stale loads. Off-screen embeds release their renderer and WebGL context, then recreate them with the saved camera and entity clip playback time when visible; hidden tabs pause rendering. Context loss shows a recovery message and Retry button; native context restoration redraws the scene. The editor's Copy embed encodes the document through the shared codec, preserving camera, background, and aspect ratio; EMBED_URL overrides its build-time destination. Host-page messaging and legacy query aliases are unsupported. V1 embed routes remain unchanged.
Instancing (src/instance/): InstanceManager dedupes models with the same AssetKey.serialize(), display position, UV-lock rotation, tint palette, and culling mask into shared InstancedMeshes. maxInstanceCount sets the initial capacity (default 50, 2000 for world blocks); buffers grow as needed without changing live indices. InstanceReference proxies per-slot transforms; removeFromScene() or dispose() releases its slot for reuse. Removed references reject transform access, and instanceCounter counts live slots.
Asset pipeline (src/assets/, src/cache/, src/request/): assets are addressed by AssetKey {namespace, path, assetType, type, rootType, extension, root}; AssetKey.serialize() is the universal cache key and includes the explicit root or AssetLoader.ROOT, separating versions in persistent caches. AssetLoader holds an ordered AssetSource registry (HostedAssetSource for CDNs, ArchiveAssetSource + BrowserArchiveProxy for resource-pack zips — browser-only today). Added sources have highest priority. get() snapshots that order and returns the first defined result; getFirst() tries alternative paths within each source, and getAll() collects all sources. Hosted namespace/root retries remain per-source; use retryDefaults: false for strict layering. Caching layers: in-memory TTL caches (Caching, 10min/5min) over PersistentCache (node-persist in Node / localforage in browser; VERSION = 2 selects fresh directories/databases and leaves older stores untouched). BrowserCache initializes localforage on first use. If browser storage cannot be opened, it warns once and uses an in-memory Map for that cache instance's remaining lifetime; errors from operations on an opened store reach the caller. Node storage is unchanged. Requests go through jobqu queues over native Fetch (Requests.ts). RequestConfig extends Fetch options with url, baseURL, timeout, and responseType; RequestResponse contains decoded data, native Headers, and the final URL. Custom response parsers use these library-owned types. AssetLoader.setVersion(version) selects the mcassets version, exposed by AssetLoader.version, and clears in-memory caches. Persistent entries for blockstates, models, and texture metadata are keyed through AssetLoader.persistentKey, which prefixes AssetLoader.persistentScope: empty with only the default vanilla sources, otherwise every added source's cacheId (hosted root, or an archive's ArchiveProxy.id), and a per-session scope when a source has no identity. Results from resource packs therefore never persist under vanilla keys.
Registry data (src/assets/MineRenderData.ts): MineRenderData.get(dataset, root?) loads one manifest-declared dataset. It tries the exact asset version, then the nearest published patch in the same release line (older on ties), then the latest earlier release. Releases older than the published data use the oldest available release; unpublished snapshots reject. resolve(root?) exposes the requested and selected versions. Missing datasets and load errors reject; they are not empty registries. Fallback changes registry data only, not models or textures. BlockStates.get, item keys in Models.getMerged, and saved world entities replace older IDs through the optional renames dataset; IDs the selected version still registers keep their meaning, and stored world data keeps the input's ID. Published data without renames applies no renames and does not reject. Renames.resolve(kind, id, dataVersion?, root?) returns the replacement directly and accepts the input's DataVersion when the caller knows it. ItemDefaults.get, ItemTints.getPotionList, getBlockFluidState, getFluidKind, and resolveLegacyBlock are asynchronous. Loaded-table helpers keep geometry and numeric parsing synchronous. BannerPatterns.getColors returns versioned texture dyes; the deprecated DYE_COLORS and getColor retain their fixed 1.21.11 palette for compatibility.
Model pipeline (src/model/, src/UVMapper.ts): Java-edition model JSON → ModelMerger resolves the parent chain (ts-deepmerge) → UVMapper.createAtlas builds a per-model texture atlas on a compat canvas and bakes atlas UVs into element.mappedUv on the atlas's own element copies (merged models share element objects with their cached parents) before geometry creation → ModelObject.init builds cached BoxGeometrys per element, optionally merges (mergeMeshes) and instances (instanceMeshes). Blockstates (src/model/block/): BlockObject resolves variants/multipart via mapStateToVariant and creates its visuals as ModelObjects through scene.addModel (so blocks share model instances). Modern items/ definitions resolve to model files before parent merging; legacy models/item/ files remain supported. Generated item models get elements from ModelGenerator. Block models' element shade: false disables directional face shading in separate, merged, and instanced models, including world sections, while retaining tints, saved light, and ambient occlusion. Item previews keep directional shading because vanilla's item renderer ignores the flag.
Skins (src/skin/): SkinObject uses vanilla part dimensions, UVs, and joint pivots. The player faces +Z as in V1, with its own right side on -X, so a negative X rotation raises a limb forward. PNG decoding preserves hidden RGB before base-layer opacity is enforced; overlay alpha is retained. Await setSkinTexture to load and detect the layout/model; legacy skins are normalized to a square atlas. setSlim updates arms in place, preserving named parts, poses, materials, and visibility. PNG model detection is a heuristic; explicit slim/legacy options or setters override detection; pass undefined to a setter to restore it. Await setCapeTexture(src, layout) for a minecraft (default), optifine, or labymod cape; pass undefined to remove it. Skins.capeFromCapesDev resolves a name or UUID to a full static texture URL. The named cape group follows the torso; the group and mesh support posing and visibility. The opt-in ears option or setEars adds deadmau5's ears as opaque leftEar/rightEar head meshes from the skin texture at 24,0, in vanilla's 1.21.2 and later shape. The opt-in upsideDown option or setUpsideDown applies vanilla's Dinnerbone/Grumm flip to a root node that holds every part, so caller transforms and joint poses still apply. Callers decide when a player name enables either option. Geometry and prepared skin/cape materials are shared cache entries. Await setArmor(set) to load 1.21.4+ equipment assets with dye, trims, and glint, or pass undefined to remove armor; one entity object per covered skin part follows that part’s pose. Armor.getEntity(set) returns the same armor for scene.addEntity, without a player, in SkinObject’s frame: feet at the origin, facing +Z.
Entities (src/entity/): Entities.getEntity loads a versioned entity-models file. By default it selects main plus the dataset passes that have no when label or whose label is in options.when (getPassList lists them); layer/layers select exactly the named layers in order, without passes. textures overrides are keyed like EntityModel.layers, where a further draw of the same geometry is <layer>#<n> from 2. getLayerList lists available names. Each draw uses the material for its dataset render mode (Materials.createEntityCanvasMaterial), cached per texture, mode, and tint; modes that vanilla culls get no inward faces. EntityObject tints maps pass tint labels to colours. energy_swirl and breeze_wind materials are owned by their object: Ticker advances EntityObject.age and the texture offset while the object has a parent; removal pauses it and reattachment resumes it. EntityObject.getLayerGroup and layer-scoped getGroupByName/getMeshByName expose each layer’s visibility and poses. Parts form a nested Object3D hierarchy with radian poses, inherited texture dimensions, cube growth, and mirrored box UVs. The entity root applies the dataset's transform (vanilla's renderer operations for the default state: living entities face +Z with their feet at the origin); models without one get scale (-1, -1, 1). Nested child nodes hold this transform, one per operation, so every operation order is exact and caller position, rotation, and scale apply on top of it. flip: true forces that plain flip and flip: false keeps raw model space. Without an explicit texture key, use dataset textureLocation first, then the resolver (model path, a same-named texture in its directory, then variant data); getBlock and getBlockList are deprecated aliases. Entities.getDatasetRoot loads geometry, lists, animations, and the block index from the nearest published release when the exact dataset version is missing; textures keep the requested root. src/bedrock/ contains type declarations only.
World (src/world/): MineRenderWorld is an early prototype with sparse chunks keyed by signed coordinates. Cubic 16³ Chunks store ChunkData palettes with immutable states and recycled unused entries, 4,096 Uint16 state indices, and per-cell NBT. Render handles and stable BlockInfo views remain separate; .block returns detached snapshots. Block coordinates are integers; chunk-local coordinates are 0–15. World and owned-chunk setters refresh cullface masks after edits, including across chunk boundaries; only opaque full-cube models occlude neighbors. The versioned blockCulling dataset lists vanilla's further face rules, applied in both rendering modes: glass, ice, slime and similar blocks hide faces toward the same block, mangrove roots toward the same block above or below, and connected panes or bars toward each other. Leaves keep their faces, as vanilla's cutout leaves do; data without blockCulling applies no such rule. new MineRenderWorld(scene, { sectionMeshing: true }) merges models of any shape, including cutout, translucent, multipart, and animated models, plus fluid surfaces into per-section meshes with separate solid, translucent, and fluid atlas pages (at most 2048×2048 pixels by default; maxAtlasSize), with one mesh per page. Page selection uses texture alpha (hasTranslucency), so plain glass and panes use the solid/cutout page where vanilla uses the translucent layer. Transparent model and fluid pages draw their quads back to front: perspective views sort by distance from the camera and re-sort when it enters another block, as vanilla does; orthographic views sort by view depth and re-sort when the view direction changes. Sorting stays within each atlas page; intersecting surfaces can still blend incorrectly, and Three.js sorts the meshes for different pages or sections as whole objects. Per-position flower offsets remain unsupported. Only block entities keep individual objects; only opaque full cubes occlude neighbors. Placed player heads use vanilla's default skin texture; profiles in block-entity data are not read. Animated atlases occupy their own pages and redraw on frame changes. Fluid surfaces use an 18×18×18 cell neighborhood and are built in the section worker when available. The section worker entry must stay free of three, DOM, and Node imports, so pure geometry code lives in SectionGeometry.ts and FluidQuads.ts. Merged BlockInfo.object values are undefined; edit through world/chunk setters. Replacing, removing, or clearing blocks releases their instance slots. await world.unloadChunkColumn(x, z) removes every section at absolute chunk-column coordinates and refreshes neighboring faces and fluids. await world.unloadChunkColumns([{ x, z }, ...]) validates, copies, and deduplicates a group before removal, then refreshes surviving faces, fluids, lighting, AO, and biome colors once. await world.setBlockVisibleAt(position, visible) preserves block data and reveals neighboring faces in both rendering modes; replacement or removal resets visibility. Without an executor, parsed columns copy each section's palette into the chunk, resolve each palette entry once, and fill cells in one synchronous pass; placeMultiBlock groups blocks per chunk and resolves each distinct state once per group, with weighted variants chosen per cell and yields after about 8 ms of work. With sectionMeshing, placeMultiBlock starts loading every distinct state's models and atlases, excluding block entities, before it places the first chunk. Blocks that keep individual objects are created concurrently, with their position set before their models join the scene. await world.placeChunks(columns) places parsed columns in input order with one final neighbor refresh, including after partial failure. Passing a BatchedExecutor keeps the previous per-position batches, order at repeated positions, and wait-for-active-batch failure semantics. Bulk placement applies saved properties before model creation and flushes neighbor culling once, including after partial failure; single-block edits update neighbors immediately. Parsed column placement and unloading mark whole sections and neighboring sections' shared boundary cells dirty; structure placement and single edits queue chunk-local indices, and the culling drain recomputes masks in a flat pass per section and rebuilds each changed section once through SectionMesh.buildAsync, which runs the pure buildSectionGeometry (SectionGeometry.ts, typed arrays in and out) in the browser section worker when Env.provider.createWorker returns one and synchronously otherwise; a rebuild superseded by a newer rebuild or clear() is disposed. BatchedExecutor.submit returns a task promise; stop() rejects queued and future tasks while active tasks finish. Idle executors hold no timer; caller-provided executors remain caller-owned. Saved biomes supply grass, foliage, dry-foliage, and per-cell water colors in both rendering modes. Saved lighting, corner ambient occlusion, and biome blending are opt-in; light propagation, exact partial-shape ambient occlusion, and LOD remain unsupported. The demos in apps/web/ exercise MineRenderWorld.
world.raycast(raycaster) returns the nearest visible rendered block: integer position, detached block data, and scene-space point, normal, and distance. It resolves merged geometry through per-quad cell indices and individual geometry through owned model/instance references. Hidden blocks and unrelated scene objects are excluded; transparent texture pixels still participate in geometric picking.
world.raycastEntities(raycaster) returns the nearest visible rendered mob owned by that world when renderEntities is enabled: canonical type, copied saved fractional position in block units, live object, and scene-space point, normal, and distance. Hidden or detached entities are excluded. Compare block and entity hit distances to account for occlusion; the world demo selects the nearer hit and outlines the selected mob's visible bounds.
WorldStreamer loads nearby columns in distance order, retains a bounded surrounding area, and serializes placement and unloading. With placeChunks, it keeps at most four pending or decoded source columns and places the ready prefix together; a later unfinished read does not delay ready columns. Custom adapters without this method retain one-at-a-time reads and placement. Its world must provide unloadChunkColumns, as MineRenderWorld does, to group obsolete columns and disposal cleanup. Failed bulk unloads retain ownership for retry and invalidate loaded entries before any replacement loads. WorldChunkSource.getChunk(x, z, signal?) takes absolute chunk coordinates; the streamer aborts obsolete source reads and active reads during disposal. AnvilWorldSource reads regions on demand with count and byte cache limits; its AnvilRegionReader(x, z, signal?) callback takes region coordinates. The optional readExternalChunk(x, z, signal?) reads .mcc payloads at absolute chunk coordinates in the same dimension without caching them. Optional readEntityRegion reads a dimension's separate entities/r.x.z.mca files, including entity-only columns, with cache limits and cancellation. All readers must forward the optional signal to cancellable I/O to stop that work. Source failures appear in failedChunks without blocking other columns; retryFailedChunks() retries them. The world demo opens a local Java save folder or selected .mca and .mcc files, selects a dimension, and streams around the view center.
Formats: StructureParser loads .nbt; SchematicParser converts numeric .schematic blocks to modern names; parse(nbt, customMappings, options) accepts id:metadata overrides and falls back to the selected version's legacy mappings; opt-in lenient ignores Materials, tries metadata zero, and skips unmapped blocks. SpongeSchematicParser.parse(nbt) loads v2/v3 .schem files with local palettes, applies Offset to block/entity positions, and normalizes entity payloads to vanilla NBT; size retains the source dimensions. Biomes, metadata, and global block ID palettes are not imported. LitematicaParser.parse(nbt) loads v5–7 .litematic files into a combined structure and named regions, each usable with placeMultiBlock. Region position and signedSize retain the source selection; block/entity positions use the schematic origin. Combined size encloses all region bounds, and later entries in regions replace earlier cells, including with air. Metadata and scheduled ticks are not imported. AnvilParser reads numeric and paletted Java .mca chunks with gzip, zlib, LZ4, or uncompressed payloads: getChunkList/parseChunk use region-local coordinates (0–31), while placeChunk replaces the absolute chunk column. Pre-1.13 numeric chunks reuse the schematic mappings; parser and AnvilWorldSource options accept legacyMappings overrides and lenient metadata-zero fallback. Neighbor- and block-entity-derived state reconstruction is not performed. For external .mcc payloads, pass region: { x, z } and readExternalChunk in the parser options; the reader receives absolute chunk coordinates. Parsers retain DataVersion and entity NBT without migration. Modern (1.18+) section biome palettes are kept as 64 IDs per section; placeChunk copies them into the world for world.getBiomeAt(x, y, z), and uses them for block and water colors. Await chunk.setBiomes(samples) to refresh affected blocks and tall-plant halves above the section. With renderEntities, supported mobs render at their saved position and yaw; sheep wool colour and shearing and fox, axolotl and parrot variants apply; other appearance state is ignored. The option defaults to false. Column replacement, unloading, and clear() dispose the owned entity objects. Unsupported entities remain data only. AnvilParser.parseEntityChunk reads separate modern entities/*.mca records at region-local coordinates; AnvilWorldSource joins them with terrain when readEntityRegion is supplied. NBTHelper.fromBuffer also preserves NBT format, compression, and byte metadata.
World biome blending: new MineRenderWorld(scene, { biomeBlendRadius: 3 }) averages grass, foliage, dry-foliage, and water colors over a horizontal square in section meshes and requires sectionMeshing. The integer radius is 0–7 blocks and defaults to 0 (disabled). world.biomeBlendRadius reads it; await world.setBiomeBlendRadius(radius) updates loaded terrain without reloading the source. Biome edits and column loading or unloading refresh only the neighboring cells whose color changes. Explicit tints remain authoritative; missing neighbors and neighbors whose biome fails to load are excluded, and a missing center biome or definition retains preview colors. A failed color refresh after placing or unloading a column logs a warning without rejecting; setBiomeBlendRadius and setBiomes reject. Blocks that keep individual objects, such as block entities, keep unblended colors. getBiomeAt still returns the saved ID. Color caching avoids per-frame averaging; larger radii add terrain-update work. World-seed-based biome sampling remains unsupported.
Saved world lighting: new MineRenderWorld(scene, { savedLighting: true }) applies imported sky/block light to section-mesh vertex colors and requires sectionMeshing. The option defaults to false. AnvilChunk.sections[].light retains independent 2,048-byte nibble arrays, including light-only sections; lightValid records the save's validity flag. world.getLightAt returns the stored 0–15 channels even when invalid. Missing sections and invalid lighting retain preview brightness; omitted channels in known sections are zero, and sky light above a column's highest saved sky array is 15. Column replacement, unloading, and clearing remove samples and refresh neighboring surfaces. Block edits preserve samples without light propagation. ambientLight (0–1, default 0) sets a minimum brightness, as a dimension type's ambient_light does. Brightness uses a neutral daylight curve; entity and block-entity lighting and time/dimension lightmaps remain unsupported.
World ambient occlusion: new MineRenderWorld(scene, { ambientOcclusion: true }) bakes corner shading from opaque full-cube neighbors into axis-aligned boundary block faces of section meshes and requires sectionMeshing. The option defaults to false; world.ambientOcclusion reads it, and await world.setAmbientOcclusion(enabled) refreshes loaded geometry without reloading the source. Block edits, visibility changes, and column loading or unloading refresh affected neighbors, including across section boundaries. Models with ambientocclusion: false, inset and non-axis-aligned faces, fluids, entities, and block entities retain their existing shading. Partial boundary faces interpolate full-face corners; exact partial-shape parity is unsupported. Ambient occlusion multiplies existing tints and saved light.
Fluids: Water and lava use saved levels and neighboring blocks for sloped surfaces: per-block instances without section meshing and merged section surfaces with it. Both paths use still/flow textures and fixed blue water tint. Per-block fluids draw vanilla's faces: outer faces, inner side faces, and the underside of a surface below a cell that is neither the same fluid nor solid; bottoms have no inner face. Translucent water draws inner faces from their own instance pools, before the outer faces, because separate blocks are not sorted against each other; lava writes depth, so one pool holds both sides; this order is exact for a convex body of water seen from outside or inside, while other shapes and separate bodies in line can blend layers out of order. Both paths clamp texture samples to each sprite. Section meshes sort their quads and draw both sides of every face, including the bottom. Blocks with waterlogged=true, kelp, and seagrass include source water alongside their model; bubble columns render water only. Partial-shape occlusion remains unsupported.
GUIs (src/gui/): scene.addGui creates ordered texture, item-model, or text layers with top-left positions and optional output sizes. Uncropped textures read .mcmeta GUI scaling (stretch, tile, or nine-slice, including stretch_inner); metadata dimensions set the default logical size independently of PNG resolution. Explicit source-pixel crops stretch. Items use their GUI display pose, explicit tints, and gui_light, scaled from a 16×16 slot. A context.count greater than 1 draws a label; zero leaves an empty slot without loading the model. The damage and max_damage components draw a durability bar unless unbreakable is present. Vanilla items inherit durability defaults from the selected registry dataset; supplied components override them. Labels and bars scale with the slot and draw above its model and enchantment glint; decorations: false hides both. An item layer's context accepts component JSON, count, properties, and references; display-context selection remains GUI. One GUI pixel is one scene unit; positive GUI y maps to negative scene y. GuiObject.bounds includes slots and visual overhang in GUI coordinates for camera fitting. Texture layers share unlit materials and own their geometry; text and item layers own their render resources. GuiHelper.inventorySlot positions layers. GuiHelper.recipe creates shaped/shapeless crafting-table layers; callers resolve tags and alternatives to concrete items, and result stack counts are drawn. await GuiHelper.tooltip(lines, options) sizes the tooltip sprites around supplied title/body text; it does not derive item descriptions.
GUI text: Fonts.get combines resource-pack font definitions in priority order, supporting bitmap, Unihex, space, and reference providers. Unihex ZIPs supply Unicode glyphs with vanilla half-resolution metrics and size_overrides; glyph pixels are rasterized into shared atlas pages when used. A Unihex file loads only when text contains a character that the providers before it lack; fonts that reach the same file share one compact parsed copy, and a provider whose file fails to load supplies no glyphs. Text accepts literal strings or runs with text, sRGB color, bold, and italic; font, shadow, maxWidth, and lineHeight configure the layer. layoutGuiText measures the same glyph advances used for wrapping and rendering; named text layers expose groups. TrueType, translations, legacy formatting codes, and right-to-left shaping remain unsupported; missing glyphs render as outlined boxes.
GUI layouts: await GuiHelper.bossBar(options) creates a 182×5 bar with a color and clamped progress. await GuiHelper.book(options) creates a page with optional supplied text and previous/next arrows in hidden, normal, or hover states; callers handle pagination and clicks. await GuiHelper.container(type) creates a generic_54 or crafting_table background. GUI_CONTAINER_LAYOUTS exposes their slot coordinates for inventorySlot. These helpers scale source crops to the resource pack's image resolution; explicit crops bypass .mcmeta scaling.
Exports (src/export/): Renderer.toImage(trim, mime, quality) captures a fresh browser frame, including while stopped; trimming retains nonzero alpha and uses drawing-buffer pixels. NodeRenderer.renderToBuffer({ trim }) captures direct or composer output as a PNG Buffer, restoring straight alpha and the caller's active render target; empty trimmed output is one transparent pixel. await renderer.toVideo({ duration, fps, mimeType, videoBitsPerSecond, signal }) records the canvas in real time through MediaRecorder (browser only) and resolves a silent WebM or MP4 Blob; one recording per renderer, cancelled by stop(), disposal, or the signal. toObj, toPLY, and toGLTF export visible static meshes with their instance placements; SceneExporter accepts an Object3D directly. OBJ/PLY omit texture images. Static exports omit animated item glint while retaining the base geometry. Browser-only glTF/GLB retains atlas textures and vertex tints but does not bake custom shader lighting.
Animation: Renderer.onFrame(({ time, delta }) => ...) returns an unsubscribe function and runs before each FPS-limited draw while started. Subscriptions keep clean scenes drawing; stop() pauses them and dispose() clears them. Both values use seconds: time is the animation-loop timestamp, and delta is zero on the first update after subscribing or starting. renderOnce() and image capture force a fresh frame without invoking callbacks or advancing entity animation time; callers set animation time before each capture. Mcmeta frame grids, sequences, durations, and RGBA interpolation use shared atlas subscriptions driven by src/Ticker.ts. Changed frames update every consuming texture and scene; removing the last consumer stops the atlas ticker.
Entity animations (src/entity/EntityAnimation.ts, src/entity/scene/EntityAnimationPlayer.ts): Entities.getAnimations(modelKey) loads native and sampled vanilla clips from entity-models/animations/<namespace>/<id>.json. The sampler uses seconds, baked-pose offsets, scale minus one, and linear/Catmull-Rom interpolation; pre preserves jumps at keyframes. EntityObject.playAnimation(clip, { loop, speed, time }) targets the clip's layer (default main), including repeated draws; a main clip also poses selected layers without a clip of their own, such as passes. playAnimations(clips, { weights, loop, speed, time }) synchronizes clips and adds their weighted offsets within each layer. Weights default to 1, must be finite and nonnegative, and are not normalized; explicit weights require one entry per clip. setAnimationWeights(weights) reapplies the current time without restarting playback; without playback, it does nothing. activeAnimations and animationWeights retain input order; a zero-weight explicit layer clip still takes precedence over the main-layer blend. Playback uses baked part poses; stopAnimation restores the caller's previous pose. Drive the shared clock with setAnimationTime or renderer.onFrame(({ delta }) => entity.advanceAnimation(delta)), unsubscribing when playback ends. Runtime state selection, visibility changes, and animated renderer transforms remain unsupported.
- Dirty flag: anything that mutates visuals must end up calling
notifyDirty()/ settingscene.dirty, or the frame never repaints. Built-in controls (controls: { enabled: true };mode: "fly"selectsFlyControlsinstead of OrbitControls) register automatically and update before the dirty check and frame limit.renderer.setControlsMode(mode)replaces the controls object; listeners and settings on the old one do not carry over.render.fpsLimitcaps draws (default 60; nonpositive values disable the cap), retaining dirty state on skipped frames. Register external event sources, including manually created controls, viarenderer.registerEventDispatcher(...). - Logging: library code logs through
const log = logger("<Name>")fromsrc/util/log.ts, notconsole. Only warnings and errors print by default;Logging.configure({ level, namespaces })changes the threshold per logger name, andLogging.setHandler(fn)replaces console output.log.tsimports nothing. Theminerender:loglocalStorage key (browsers) orMINERENDER_LOG(Node) overridesconfigure; it is read once, on the first log call. - Renderer ownership:
stop()pauses;dispose()is final.NodeRenderer.create()owns and destroys its native context; injected surfaces retain their caller-owned canvas and context after disposal. Dispose only owned resources; shared assets, global caches, and caller-owned controls retain their owners. Callers disposeSceneStatsDisplayseparately to remove its timer and DOM. - Color pipeline: image and canvas color-texture factories set
SRGBColorSpace; the renderer uses sRGB output in both direct and composer mode. Custom model shaders apply shading in linear light and include Three's tone-mapping and output-color chunks. Keep the genericTextures.initTexturePropshelper limited to sampling settings so it preserves caller-supplied data-texture color spaces. Model atlases record partial alpha inhasTranslucency; only those materials blend, while binary-alpha textures render as cutout. - Block preview colors: The versioned
block-tints.jsondataset maps block IDs to preview rules. Grass uses the active resource pack's colormap; cauldron water defaults to blue. Explicittintsoverride automatic colors. Rules can declare abiomecolor kind; world placement supplies saved biome IDs and coordinates.Biomesloads vanilla biome definitions and resource-pack colormaps, including dark forest and swamp grass modifiers. Missing definitions retain preview colors; waterlogged blocks keep model and water tints separate. - Face order:
CUBE_FACES= east, west, up, down, south, north (three.js BoxGeometry material order). UV buffers are written atfaceIndex * 4vertices. Skins, entities, and models all rely on this ordering. - Scale: 1 block = 16 scene units (= Minecraft model space); 1 chunk = 256 units.
- Instance removal: freed slots use zero-scale matrices until reused; bulk transforms skip them. Block removal and state changes release their model references without disposing shared resources.
- Instancing gate:
addSceneObjectonly dedupes whenasset.key.assetType === "models";BlockObjects are deliberately not added to the scene graph (their ModelObjects are) — block transforms only exist through instance references. isX: truemarker + guard-function pattern (isMineRenderScene,isAssetKey, …) is used instead ofinstanceofthroughout.- The Env seam (
src/Env.ts) is the browser/Node boundary and the thing that keeps the build working.EnvProviderdeclares the four required platform-dependent capabilities —createCanvas,createImage,imageSize,openCache— plus the optionalcreateWorker("section"), which onlyBrowserEnvimplements (it loadsBrowserEnvOptions.sectionWorkerwhenregisterBrowserEnvreceived one, otherwise the file besideimport.meta.url; when that fails, including CJS and IIFE outputs withoutimport.meta.url, it logs a warning and sections build synchronously); andsrc/env/browser/+src/env/node/implement them. Nothing outsidesrc/env/node/may importcanvas,gl,node-persist,image-size, or a Node builtin as a value; type-only imports (import type) are fine because they erase. The entries (src/index.browser.ts,src/index.node.ts) eachimport "./env/<platform>/register"first, so the provider is installed before any other module body runs. Verify with:rg --no-filename -o -e 'from ?"[^."][^"]*"' -e 'import\("[^."][^"]*"\)' packages/minerender/dist/browser/esm --glob '*.mjs' | sort -u— that list of package imports, static and on first use, must stay free of Node modules. - No work at import time. Anything expensive or platform-dependent must be lazy (
Materials.MISSING_TEXTURE, thePERSISTENT_CACHEgetters,Ticker's timers andAssetLoader's default source are all deferred for this reason). A static field initializer that decodes an image or opens a cache will run before any consumer has configured anything, and in Node it will run in a process that has no DOM. Renderer,MineRenderScene,SceneObject, andSkinObjectmust not statically import the model, block, entity, or GUI object modules,UVMapper, orAssetLoader.test/Package.test.mjschecks their built static import graph.
- Idle caches and request queues let Node exit.
shutdown()clears shared caches, permanently ends request queues, and stops Ticker. - Requests require Fetch and
AbortSignal.any/timeout, with bounded GET retries and timeouts through body reading. Cancellation aborts a call; shutdown rejects waiting work and lets active calls finish. - Async list/dictionary caches evict missing and rejected loads. Texture metadata and entity texture lookups store
Caching.MISSINGfor missing files instead, and persist missing metadata. With only the vanilla source,.mcmetalookups check the hosted directory list before requesting the file. Clear in-memory caches when sources change; persistent storage must be cleared separately. - Image decode failures reject and evict the matching cache entry. Failed synchronous image placeholders are retried on the next lookup.
- Item previews accept
Models.getMerged(key, { displayContext, components, count, properties, itemReferences }). Component JSON supplies indexed custom-model-data flags, strings, and floats, named component values/presence, damage, block-state properties, and charged projectiles. Damage and count use each node'snormalizesetting; numeric dispatch then applies its scale.propertiesoverrides derived values by property ID, whiledisplayContextretains precedence for display-context selection and defaults to GUI. Count defaults to 1.await ItemDefaults.get(itemId, root?)returns the selected version's default components:max_stack_size,max_damagewithdamage: 0for damageable items, andenchantment_glint_overridefor items that glint by default; an item the dataset does not list stacks to 64. Supplied components override these defaults in model selectors, glint, and GUI durability bars. Other registry components are not loaded, andhas_componentwithignore_default: truestill requires an explicit property override. Component selectors compare JSON values without reconstructing component codecs or registry values. Component, property, and reference keys accept IDs with or without theminecraft:prefix.itemReferencessupplies item keys for dynamic nodes such asminecraft:bundle/selected_item; referenced items use their own definitions and default state, retaining the display context. Resolved caches snapshot and serialize the whole context.ItemModel.itemIdretains the original item ID through model replacements and composite children; referenced items retain their own IDs. Item tint sources readdyed_color,map_color,firework_explosion.colors(average RGB),potion_contents, and indexedcustom_model_data.colorsfrom the supplied components. Missing values use each source's declared default; explicittintsoverride automatic colors. Constant colors and grass colormap samples remain supported. Potion tints resolvepotion_contents(a potion ID, orpotionpluscustom_effects) from the selected version's potion and effect data, weighting visible effects byamplifier + 1;custom_colortakes precedence, and unknown IDs use the source's default. Releases before 1.20.5 use vanilla float32 color mixing with the legacy empty/hidden-effect colors; later releases use integer channel averages and the declared fallback for no visible effects.await ItemTints.getPotionList(root?)lists the selected registry's potion IDs. Potions with effects also inherit the pre-1.19.4 glint rule, unless an explicit component overrides it. Team colors use their declared defaults. Nested composite definitions retain orderedItemModel.parts; each child keeps its own textures, display pose,gui_light, and tint sources. Composite objects and their children do not use instancing. - Special renderers: Chests, beds, mob heads, player heads, shulker boxes, banners, shields, tridents, conduits, decorated pots, and copper-golem statues use entity geometry with the base model's display pose and
gui_light. A display node under the item holds that pose and the parts; every node uses a plain position, rotation, and scale, soBox3.setFromObjectis current without a render. Shulkers use the definition's texture, openness, and orientation (defaultup); banners use the definition's dye color; shields usebase_color; both draw the first 16 suppliedbanner_patternsin order via the versioneddata/<namespace>/banner_patternregistry, preserving uncolored banner support and shield handles; tridents select flat icons or held/throwing poses through display-context andusing_item; conduits draw static shells; pots readpot_decorationsitem IDs in back/left/right/front order, with omitted sides, bricks, and unrecognized items using plain side textures. Player heads readprofile: a name or UUID-only profile resolves a skin, while supplied texture properties ortextureresource IDs select a texture directly. Profile UUIDs use four signed integers, as in item JSON; the playground accepts conventional UUID text. Thetextureresource override takes precedence. Missing profiles use the default Steve skin; unresolved profiles use vanilla's UUID-based default. Remote skins retain the hat overlay and use the shared legacy-skin conversion. Copper-golem statues use the definition's full texture path and pose; vanilla definitions select standing, sitting, running, or star fromblock_state.copper_golem_pose. Other special renderers and automatic world/player state remain unsupported. - Item glint: Ordinary items, shields, and held or throwing tridents use supplied
enchantmentsunlessenchantment_glint_overrideoverrides them; glint preserves transparency, shares the selected asset source's texture, follows atlas attachment, and disables instancing. Ordinary items and shields use texture-local UV approximations; plain shields glint only on the plate, dyed or patterned shields also glint the handle without extra pattern passes, and other special-renderer glint remains unsupported. Normal compasses also glint whenlodestone_trackeris present; recovery compasses do not inherit that rule. Compasses, recovery compasses, and clocks use projected glint from their baked model positions, with vanilla GUI and first-person scales. Default glint comes fromItemDefaultsunless explicitly overridden. - Hosted/archive failures reject with
AssetLoadError. Only missing assets or an explicitundefinedparser result permit fallback. - Antialiasing samples pixels at triangle edges outside a face's texture region, which draws neighbouring texels as outlines. Model shaders clamp to atlas bounds; entity, skin, cape and special-item materials use
Materials.boundUvswith the per-faceuvBoundsthatGeometriesbox geometry carries.clone()drops that shader patch, so apply it again to clones. - Keep
@types/threeandthreeon the same minor version; import geometry/material types from barethree.onBeforeCompilepatches edit three's shader text by string;test/ShaderPatches.test.tsfails when an update moves a target. - Node imports require a working native
canvas3 installation;NodeRenderer.create()additionally loads optionalgllazily. Headless Linux needs Mesa and an X server such as Xvfb. Browser-only installs can skip both native builds. - Preserve V1
master, tags, bundles, and website URLs. The manually hosted V1 site and GitHub Pages docs (master:/docs) remain independent ofmain.
The union of what the three consumers (examples/vite, apps/web, examples/script-tag) actually call — treat as semi-frozen until a deliberate break:
new Renderer({camera, render: {stats, fpsLimit, antialias}, composer: {enabled}, debug: {grid, axes}}), renderer.appendTo/start/registerEventDispatcher/toImage, renderer.scene / renderer.camera / renderer.renderer; scene.addModel/addBlock/addSkin/addEntity/stats; Models.getMerged/clearCache; BlockStates.get/getList; Entities.getEntity/getBlock/getEntityList/getBlockList; Skins.fromUuidOrUsername; SkinObject.setSkinTexture; AssetKey (5–7 arg ctor, AssetKey.parse); AssetLoader.ROOT/addSource/loadOrRetryWithDefaults/NBT; HostedAssetSource, ArchiveAssetSource, BrowserArchiveProxy; Caching.clear; StructureParser.parse; MineRenderWorld(scene).setBlockAt/clear/placeMultiBlock; BatchedExecutor; Ticker.tpsOneSecond/tpsFiveSeconds; SceneInspector; re-exported OrbitControls; per-object setPosition/removeFromScene/disposeAndRemoveAllChildren/isInstanced/instanceCounter.
Delivery formats that must all keep working: ESM named imports under a bundler, CJS require, and the window.MineRender IIFE bundle (script tag / unpkg). All three are built by yarn build:lib. Check imports under Node as well as loading the consumers in a browser.
Additional public APIs: Logging.configure/setHandler, logger, Renames.get/resolve/key, shutdown(), Env/EnvProvider, BrowserEnv/NodeEnv (per-entry), Caching.end, Requests.end, Ticker.start/stop, Renderer.dispose/onFrame/renderOnce, RendererSurface, Node-only NodeRenderer.create/renderToBuffer, SceneStatsDisplay.dispose, optional controls.enabled and controls.mode, FlyControls, renderer.setControlsMode, renderer.orbitControls/flyControls/controlsMode, and renderer.controls (typed OrbitControls | FlyControls since 2.1.0-alpha.115, a deliberate break for TypeScript callers that read OrbitControls members from it; undefined unless enabled at construction, and after disposal).
Follow the ordered tasks in ROADMAP.md.