minecraft-renderer
v0.1.92
Published
The most Modular Minecraft world renderer with Three.js WebGL backend
Downloads
4,227
Maintainers
Readme
Minecraft Renderer

One of the best Minecraft world rendererers implemented from scratch. Uses Three.js WebGL 2 backend. Designed for performance testing, experimentation, and integration into Minecraft clients or other use cases for game world display.
Features:
- 💡 Full-featured: hand, third-person view, entities, debug features and even more!
- ⚡️ Leverages all available WebGL 2 and WASM world meshing for the maximum performance
- 📦 Implemented from scratch; small bundle size and runtime footprint
- ⚙️ Easily customizable: modular architecture with Three.js API
Implemented Features
- WASM mesher workers (default path) with legacy JS mesher fallback
- Instanced shader-cube rendering for full blocks (
GlobalBlockBuffer, one GPU draw) - Global legacy geometry buffer for models/stairs/slabs (merged indexed mesh, opaque + blend passes)
- Block and sky lighting, smooth lighting, and vanilla vs high-contrast (default) face shading (
vanillaLook) - Signs, banners, skulls, and other block-entity overlays
- Entities: players with skins & animations, mobs, items, armor, text/item display
- Day cycle, skybox, starfield, rain, fireworks
- Third-person camera, view bobbing, holding block / hand
- Optional off-thread graphics backend (render in a worker!)
- Smooth lighting (lighting data has to be provided)
Browser support
Requires WebGL 2.0. WebGL 1 is not supported as a full-feature path (shader cubes and several block shaders need WebGL2).
| Browser | Minimum version | | -------------------- | --------------- | | Chrome / Chromium | 56+ | | Firefox | 51+ | | Edge | 79+ (Chromium) | | Safari (macOS / iOS) | 15.3+ | | Opera | 43+ |
Not supported: Safari before 15.
Optional extensions (WEBGL_multi_draw, instanced base vertex) improve draw-call batching when present; the renderer falls back to capped multi-draw loops when they are missing.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ AppViewer │
│ - Manages graphics backend lifecycle │
│ - Handles world view and player state │
│ - Coordinates between data and rendering │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GraphicsBackend (Three.js) │
│ - WebGL rendering via Three.js │
│ - Scene, camera, and lighting management │
│ - Mesher worker coordination │
└─────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ DocumentRenderer │ │WorldGeometryHandler│ │ StarField │
│ - Render loop │ │ - Chunk meshes │ │ - Night sky │
│ - Canvas sizing │ │ - GPU memory │ │ - Twinkling │
│ - FPS tracking │ │ - Signs/banners │ │ effect │
└──────────────────┘ └──────────────────┘ └──────────────────┘Core Components
WorldView (formerly WorldDataEmitter)
Manages chunk loading/unloading and emits world events to the renderer.
import { WorldView } from 'minecraft-renderer'
// Create world view with a world provider
const worldView = new WorldView(worldProvider, renderDistance, startPosition)
// Initialize and start loading chunks
await worldView.init(playerPosition)
// Update position (loads/unloads chunks as needed)
await worldView.updatePosition(newPosition)
// Set block and emit update
worldView.setBlockStateId(position, stateId)AppViewer
Main application entry point for integrating the renderer.
import { AppViewer, createGraphicsBackend } from 'minecraft-renderer'
const viewer = new AppViewer({
config: {
sceneBackground: 'lightblue',
fpsLimit: 60
},
rendererConfig: {
enableLighting: true,
smoothLighting: true,
showChunkBorders: false
}
})
// Load backend
await viewer.loadBackend(createGraphicsBackend)
// Start rendering world
await viewer.startWorld(worldProvider, renderDistance)
// Update camera each frame
viewer.updateCamera(position, yaw, pitch)Settings flow (app integration)
Renderer-owned options live in RENDERER_DEFAULT_OPTIONS and RENDERER_OPTIONS_META (src/graphicsBackend/rendererDefaultOptions.ts).
- Defaults — spread
RENDERER_DEFAULT_OPTIONSinto your app options store (e.g. valtiooptions). - Migration — call
migrateRendererOptions(saved)when loading persisted settings (legacy mesher/GPU keys → current renderer option names). - Settings UI — merge
RENDERER_OPTIONS_METAinto your options meta; layout can stay app-owned. - Menu startup —
startMenuBackground(menuBackgroundOptionsFromStorage(options)). - Runtime sync — after
AppViewer+ backend init, call once:subscribeRendererOptions(appViewer, options, { isSafari, isCypress, onRegisterFocusHandlers }). This updatesinWorldRenderingConfig,appViewer.config(FPS/stats), and live menu-background controls whencurrentDisplay === 'menu'. - App-only — keep
volumeand bot/world hooks in the client (applyRendererEnableLighting,applyRendererWorldViewOptions, weather).
| Change | Live update | Reload required |
| --------------------------------------- | --------------------------------- | -------------------------------------- |
| Menu V2 scene / camera / speeds | Yes (backend.getMenuBackground) | Mode switch needs restart |
| rendererMesher (wasm / legacy-js) | Yes — recreates mesher workers | Chunks reload (requiresChunksReload) |
| rendererWorldPerformance | Yes — recreates mesher workers | Chunks reload (requiresChunksReload) |
| Volume | App watchValue only | No |
Sync runs on the main thread only; inWorldRenderingConfig uses existing valtio __syncToWorker for off-thread backends. Do not call subscribeRendererOptions from mesher workers.
How Block Rendering Works
1. Chunk Data Flow
World Provider → WorldView → GraphicsBackend → Mesher Workers → Three.js Scene- World Provider: Provides chunk column data (prismarine-chunk format)
- WorldView: Emits
loadChunkevents with serialized chunk data - GraphicsBackend: Receives events and dispatches to mesher workers
- Mesher Workers: Generate geometry (positions, normals, UVs, colors)
- Three.js Scene: Creates BufferGeometry meshes from worker output
2. Mesher Worker Communication
Workers receive:
- Block data (chunk JSON with block state IDs)
- Block models and textures atlas
- Lighting configuration
Workers produce:
- Float32Array of vertex positions (x, y, z per vertex)
- Float32Array of normals
- Float32Array of colors (vertex colors for lighting)
- Float32Array of UVs (texture coordinates)
- Uint16/32Array of indices
3. Geometry Structure
Each block face is a quad with 4 vertices and 6 indices:
interface MesherGeometryOutput {
positions: Float32Array // [x1,y1,z1, x2,y2,z2, ...]
normals: Float32Array // [nx,ny,nz, ...]
colors: Float32Array // [r,g,b, r,g,b, ...] (0-1 range, lighting)
uvs: Float32Array // [u1,v1, u2,v2, ...] (texture atlas coords)
indices: Uint32Array // [0,1,2, 2,3,0, ...] (triangles)
sx
sy
sz: number // Section position offset
blocksCount: number // Number of non-air blocks
signs: Record<string, SignData>
banners: Record<string, BannerData>
heads: Record<string, HeadData>
}4. Block Model Resolution
- Block state ID → Block state properties
- Block state properties → Blockstate JSON
- Blockstate JSON → Model variants
- Model JSON → Faces with texture references
- Texture references → Atlas UV coordinates
5. Lighting Calculation
Lighting uses both block light and sky light:
// Light level 0-15 for both block and sky light
const blockLight = chunk.getBlockLight(pos)
const skyLight = chunk.getSkyLight(pos)
// Combined light level
const light = Math.max(blockLight, Math.min(skyLight, skyLightCap))
// Light level to color multiplier
const brightness = lightLevelToBrightness[light]
// Applied as vertex color: [brightness, brightness, brightness]6. Ambient Occlusion
Smooth lighting uses ambient occlusion based on neighboring blocks:
// For each vertex, check 3 neighboring blocks
// AO value = (side1 + side2 + corner) / 3
// Applied as vertex color darkeningConfiguration
WorldRendererConfig
interface WorldRendererConfig {
// Performance
mesherWorkers: number // Number of worker threads (default: 4)
addChunksBatchWaitTime: number // Batch delay for chunk loading (ms)
_experimentalSmoothChunkLoading: boolean
// Rendering
enableLighting: boolean // Enable block/sky lighting
smoothLighting: boolean // Enable ambient occlusion
dayCycle: boolean // Enable time-based sky changes
starfield: boolean // Enable star field at night
fov: number // Camera field of view
// Debug
showChunkBorders: boolean // Show chunk boundary helpers
enableDebugOverlay: boolean // Show advanced stats
clipWorldBelowY: number | undefined // Don't render below Y level
}Memory Management
The renderer implements several memory optimizations:
- CPU Array Disposal: After GPU upload, CPU-side typed arrays are nulled
- Texture Caching: Signs and banners share textures via reference counting
- Section Tracking: Memory usage is tracked per section for debugging
// Get memory usage
const { bytes, readable } = worldGeometryHandler.getMemoryUsageReadable()
console.log(`GPU Memory: ${readable}`) // e.g., "45.32 MB"Performance Tips
- Mesher Workers: Increase
mesherWorkerson multi-core systems - Smooth Loading: Enable
_experimentalSmoothChunkLoadingto prevent frame drops - Clip World: Use
clipWorldBelowYto reduce geometry for surface views - Disable Lighting: Set
enableLighting: falsefor faster meshing
Development
# Install dependencies
pnpm install
# Run playground
pnpm dev
# Build library
pnpm build
# Type check
pnpm typecheckFile Structure
src/
├── index.ts # Main exports
├── types.ts # TypeScript types
├── config.ts # Default configurations
├── appViewer.ts # Main application viewer
├── worldView.ts # Chunk loading/events (WorldDataEmitter)
├── playerState.ts # Player state management
├── three/ # Three.js backend
│ ├── index.ts # Backend exports
│ ├── graphicsBackend.ts # Main backend entry
│ ├── documentRenderer.ts # Render loop management
│ ├── worldGeometryHandler.ts # Chunk geometry
│ └── starField.ts # Night sky effect
└── playground/ # Development environment
├── playground.ts # Main playground entry
└── playground.html # HTML templateIntegration Example
import { AppViewer, createGraphicsBackend, WorldView } from 'minecraft-renderer'
import ChunkLoader from 'prismarine-chunk'
import WorldLoader from 'prismarine-world'
// Setup world (using prismarine-world)
const World = WorldLoader('1.20.4')
const Chunk = ChunkLoader('1.20.4')
const world = new World().sync
// Create viewer
const viewer = new AppViewer()
// Provide resources (textures, models)
viewer.resourcesManager = {
currentConfig: { version: '1.20.4' },
currentResources: {
blocksAtlasImage: atlasImage,
blocksAtlasJson: atlasJson,
blockstatesModels: modelsData,
allReady: true
},
on: () => {}
}
// Load backend
await viewer.loadBackend(createGraphicsBackend)
// Start world
await viewer.startWorld(world, 4) // 4 chunk render distance
// Initialize world view
await viewer.worldView!.init(new Vec3(0, 64, 0))
// Game loop
function gameLoop() {
viewer.updateCamera(playerPosition, playerYaw, playerPitch)
requestAnimationFrame(gameLoop)
}
gameLoop()License
MIT
