minecraft-renderer
v0.1.99
Published
The most Modular Minecraft world renderer with Three.js WebGL backend
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
