minecraft-renderer
v0.1.29
Published
The most Modular Minecraft world renderer with Three.js WebGL backend
Maintainers
Readme
Minecraft Renderer

A modular Minecraft world renderer with Three.js WebGL backend. Designed for performance testing, experimentation, and integration into Minecraft clients.
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)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, skyLight * skyLightMultiplier)
// 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
