Compare commits

...
Sign in to create a new pull request.

32 commits

Author SHA1 Message Date
Vitaly
c3aad71024
[deploy] send token channel 2025-08-06 18:02:24 +02:00
Vitaly
6324fe50b7
Merge branch 'next' into instancing 2025-08-06 17:38:45 +02:00
Vitaly Turovsky
c63b6bb536 [skip ci] test grid 2025-08-01 03:35:45 +03:00
Vitaly Turovsky
c93b7a6f8b Refactor sign and head container addition in ChunkMeshManager with error handling 2025-07-20 23:02:29 +03:00
Vitaly Turovsky
be0993a00b up all 2025-07-20 10:02:21 +03:00
Vitaly Turovsky
3e5c036512 Merge remote-tracking branch 'origin/next' into instancing 2025-07-20 09:43:22 +03:00
Vitaly Turovsky
370f07712b Merge branch 'next' into instancing 2025-07-20 09:43:03 +03:00
Vitaly Turovsky
0921b40f88 fix signs 2025-07-20 09:38:49 +03:00
Vitaly Turovsky
96c5ebb379 [to pick] fix chat crash 2025-07-20 09:38:22 +03:00
Vitaly Turovsky
2f49cbb35b finish manager! 2025-07-20 09:24:57 +03:00
Vitaly Turovsky
9ee28ef62f working geometry pool manager! 2025-07-20 08:21:15 +03:00
Vitaly Turovsky
0240a752ad box helper optim 2025-07-19 18:11:01 +03:00
Vitaly Turovsky
1c8799242a some important fixes 2025-07-19 18:10:09 +03:00
Vitaly Turovsky
b2257d8ae4 rm unused code 2025-07-16 09:16:06 +03:00
Vitaly Turovsky
5e30a4736e rm cache 2025-07-16 09:14:04 +03:00
Vitaly Turovsky
83018cd828 [before test] refactor to use state id, force! 2025-07-16 09:12:56 +03:00
Vitaly Turovsky
6868068705 fixes 2025-07-16 08:41:05 +03:00
Vitaly Turovsky
dbbe5445d8 realScene test 2025-07-16 07:55:29 +03:00
Vitaly Turovsky
336aad678b real 2025-07-16 07:52:17 +03:00
Vitaly Turovsky
3c358c9d22 all done! 2025-07-16 06:46:50 +03:00
Vitaly Turovsky
7b06561fc7 debug, try to fix growth, fix perf 2025-07-16 06:21:13 +03:00
Vitaly Turovsky
9f29491b5d maybe support rotation 2025-07-16 04:55:43 +03:00
Vitaly Turovsky
dbce9e7bec working texturing 2025-07-16 04:17:25 +03:00
Vitaly Turovsky
6083416943 utils 2025-07-16 03:15:36 +03:00
Vitaly Turovsky
102520233a code cleanup towards text 2025-07-16 03:14:08 +03:00
Vitaly Turovsky
a19d459e8a rm blocks hardcode 2025-07-16 02:16:17 +03:00
Vitaly Turovsky
561a18527f add creative server 2025-07-12 05:38:12 +03:00
Vitaly Turovsky
7ec9d10787 Merge branch 'next' into instancing 2025-07-09 17:19:31 +03:00
Vitaly Turovsky
7a692ac210 f 2025-06-29 15:22:58 +03:00
Vitaly Turovsky
52a90ce8ff Merge branch 'next' into instancing 2025-06-29 15:22:55 +03:00
Vitaly Turovsky
4aebfecf69 some progress 2025-06-29 15:22:21 +03:00
Vitaly Turovsky
97f8061b06 feat: Implement instanced rendering mode for low-end devices and performance testing 2025-06-25 17:25:33 +03:00
23 changed files with 2524 additions and 323 deletions

View file

@ -38,7 +38,6 @@ All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) ar
- Controls -> **Touch Controls Type** -> **Joystick**
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?)
- Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default)
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)

View file

@ -1,4 +1,8 @@
import * as THREE from 'three'
import globalTexture from 'mc-assets/dist/blocksAtlasLegacy.png'
// Import the renderBlockThree function
import { renderBlockThree } from '../renderer/viewer/lib/mesher/standaloneRenderer'
// Create scene, camera and renderer
const scene = new THREE.Scene()
@ -8,53 +12,292 @@ renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Position camera
camera.position.z = 5
camera.position.set(3, 3, 3)
camera.lookAt(0, 0, 0)
// Create a canvas with some content
const canvas = document.createElement('canvas')
canvas.width = 256
canvas.height = 256
const ctx = canvas.getContext('2d')
// Dark background
scene.background = new THREE.Color(0x333333)
scene.background = new THREE.Color(0x444444)
// Add some lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)
// Draw something on the canvas
ctx.fillStyle = '#444444'
// ctx.fillRect(0, 0, 256, 256)
ctx.fillStyle = 'red'
ctx.font = '48px Arial'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Hello!', 128, 128)
// Add grid helper for orientation
const gridHelper = new THREE.GridHelper(10, 10)
scene.add(gridHelper)
// Create bitmap and texture
async function createTexturedBox() {
const canvas2 = new OffscreenCanvas(256, 256)
const ctx2 = canvas2.getContext('2d')!
ctx2.drawImage(canvas, 0, 0)
const texture = new THREE.Texture(canvas2)
// Create shared material that will be used by all blocks
const sharedMaterial = new THREE.MeshLambertMaterial({
vertexColors: true,
transparent: true,
alphaTest: 0.1,
// wireframe: true // Add wireframe for debugging
})
// Create simple block models for testing
function createFullBlockModel(textureObj: any): any {
return [[{
elements: [{
from: [0, 0, 0],
to: [16, 16, 16],
faces: {
up: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
down: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
north: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
south: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
east: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
west: {
texture: textureObj,
uv: [0, 0, 16, 16]
}
}
}]
}]]
}
function createHalfBlockModel(textureObj: any): any {
return [[{
elements: [{
from: [0, 0, 0],
to: [16, 8, 16], // Half height (8 instead of 16)
faces: {
up: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
down: {
texture: textureObj,
uv: [0, 0, 16, 16]
},
north: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
},
south: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
},
east: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
},
west: {
texture: textureObj,
uv: [0, 0, 16, 8] // Half height UV
}
}
}]
}]]
}
let currentFullBlockInstancedMesh: THREE.InstancedMesh | null = null
let currentHalfBlockInstancedMesh: THREE.InstancedMesh | null = null
async function createInstancedBlock() {
try {
// Clean up previous meshes if they exist
if (currentFullBlockInstancedMesh) {
scene.remove(currentFullBlockInstancedMesh)
currentFullBlockInstancedMesh.geometry.dispose()
}
if (currentHalfBlockInstancedMesh) {
scene.remove(currentHalfBlockInstancedMesh)
currentHalfBlockInstancedMesh.geometry.dispose()
}
// Load the blocks atlas texture
const textureLoader = new THREE.TextureLoader()
const texture = await textureLoader.loadAsync(globalTexture)
// Configure texture for pixel art
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.needsUpdate = true
texture.generateMipmaps = false
texture.flipY = false
// Create box with texture
const geometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
premultipliedAlpha: false,
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// Set the texture on our shared material
sharedMaterial.map = texture
sharedMaterial.needsUpdate = true
console.log('Texture loaded:', texture.image.width, 'x', texture.image.height)
// Calculate UV coordinates for the first tile (top-left, 16x16)
const atlasWidth = texture.image.width
const atlasHeight = texture.image.height
const tileSize = 16
const textureInfo = {
u: 0 / atlasWidth, // Left edge (first column)
v: 2 * tileSize / atlasHeight, // Top edge (third row)
su: tileSize / atlasWidth, // Width of one tile
sv: tileSize / atlasHeight // Height of one tile
}
console.log('Atlas size:', atlasWidth, 'x', atlasHeight)
console.log('Calculated texture info:', textureInfo)
// Create mock texture object that matches what the renderer expects
const mockTexture = {
u: textureInfo.u,
v: textureInfo.v,
su: textureInfo.su,
sv: textureInfo.sv,
debugName: 'test_texture'
}
// Create block models with the mock texture
const fullBlockModel = createFullBlockModel(mockTexture)
const halfBlockModel = createHalfBlockModel(mockTexture)
// Mock data for the renderBlockThree function
const mockBlock = undefined // No specific block data needed for this test
const mockBiome = 'plains'
const mockMcData = {} as any
const mockVariants = []
const mockNeighbors = {}
// Render the full block
const fullBlockGeometry = renderBlockThree(
fullBlockModel,
mockBlock,
mockBiome,
mockMcData,
mockVariants,
mockNeighbors
)
// Render the half block
const halfBlockGeometry = renderBlockThree(
halfBlockModel,
mockBlock,
mockBiome,
mockMcData,
mockVariants,
mockNeighbors
)
// Create instanced mesh for full blocks
currentFullBlockInstancedMesh = new THREE.InstancedMesh(fullBlockGeometry, sharedMaterial, 2) // Support 2 instances
const matrix = new THREE.Matrix4()
// First instance (full block)
matrix.setPosition(-1.5, 0.5, 0.5)
currentFullBlockInstancedMesh.setMatrixAt(0, matrix)
// Second instance (full block)
matrix.setPosition(1.5, 0.5, 0.5)
currentFullBlockInstancedMesh.setMatrixAt(1, matrix)
currentFullBlockInstancedMesh.count = 2
currentFullBlockInstancedMesh.instanceMatrix.needsUpdate = true
scene.add(currentFullBlockInstancedMesh)
// Create instanced mesh for half blocks
currentHalfBlockInstancedMesh = new THREE.InstancedMesh(halfBlockGeometry, sharedMaterial, 1) // Support 1 instance
const halfMatrix = new THREE.Matrix4()
// Half block instance
halfMatrix.setPosition(0, 0.75, 0.5) // Positioned higher so top aligns with full blocks
currentHalfBlockInstancedMesh.setMatrixAt(0, halfMatrix)
currentHalfBlockInstancedMesh.count = 1
currentHalfBlockInstancedMesh.instanceMatrix.needsUpdate = true
scene.add(currentHalfBlockInstancedMesh)
console.log('Instanced blocks created successfully')
console.log('Full block geometry:', fullBlockGeometry)
console.log('Half block geometry:', halfBlockGeometry)
} catch (error) {
console.error('Error creating instanced blocks:', error)
// Fallback: create colored cubes
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshLambertMaterial({ color: 0xff0000, wireframe: true })
const fallbackMesh = new THREE.Mesh(geometry, material)
fallbackMesh.position.set(0, 0.5, 0.5)
scene.add(fallbackMesh)
console.log('Created fallback colored cube')
}
}
// Create the textured box
createTexturedBox()
// Create the instanced block
createInstancedBlock().then(() => {
render()
})
// Animation loop
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
// Simple render loop (no animation)
function render() {
renderer.render(scene, camera)
}
animate()
// Add mouse controls for better viewing
let mouseDown = false
let mouseX = 0
let mouseY = 0
renderer.domElement.addEventListener('mousedown', (event) => {
mouseDown = true
mouseX = event.clientX
mouseY = event.clientY
})
renderer.domElement.addEventListener('mousemove', (event) => {
if (!mouseDown) return
const deltaX = event.clientX - mouseX
const deltaY = event.clientY - mouseY
// Rotate camera around the center
const spherical = new THREE.Spherical()
spherical.setFromVector3(camera.position)
spherical.theta -= deltaX * 0.01
spherical.phi += deltaY * 0.01
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
camera.position.setFromSpherical(spherical)
camera.lookAt(0, 0, 0)
mouseX = event.clientX
mouseY = event.clientY
render()
})
renderer.domElement.addEventListener('mouseup', () => {
mouseDown = false
})
// Add button to recreate blocks (for testing)
const button = document.createElement('button')
button.textContent = 'Recreate Blocks'
button.style.position = 'fixed'
button.style.top = '10px'
button.style.left = '10px'
button.addEventListener('click', () => {
createInstancedBlock()
render()
})
document.body.appendChild(button)
// Initial render
render()

View file

@ -0,0 +1,7 @@
import { WorldBlock as Block, World } from './world'
export const isBlockInstanceable = (world: World, block: Block): boolean => {
const instancedBlocks = world?.instancedBlocks
if (!instancedBlocks) return false
return instancedBlocks.includes(block.stateId)
}

View file

@ -1,7 +1,7 @@
import { Vec3 } from 'vec3'
import { World } from './world'
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
import { BlockStateModelInfo } from './shared'
import { BlockStateModelInfo, InstancingMode } from './shared'
import { INVISIBLE_BLOCKS } from './worldConstants'
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
@ -17,7 +17,7 @@ if (module.require) {
let workerIndex = 0
let world: World
let dirtySections = new Map<string, number>()
let dirtySections = new Map<string, { key: string, instancingMode: InstancingMode, times: number }>()
let allDataReady = false
function sectionKey (x, y, z) {
@ -47,7 +47,7 @@ function drainQueue (from, to) {
queuedMessages = queuedMessages.slice(to)
}
function setSectionDirty (pos, value = true) {
function setSectionDirty (pos, value = true, instancingMode = InstancingMode.None) {
const x = Math.floor(pos.x / 16) * 16
const y = Math.floor(pos.y / 16) * 16
const z = Math.floor(pos.z / 16) * 16
@ -60,7 +60,7 @@ function setSectionDirty (pos, value = true) {
const chunk = world.getColumn(x, z)
if (chunk?.getSection(pos)) {
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
dirtySections.set(key, { key, instancingMode, times: (dirtySections.get(key)?.times || 0) + 1 })
} else {
postMessage({ type: 'sectionFinished', key, workerIndex })
}
@ -68,8 +68,7 @@ function setSectionDirty (pos, value = true) {
const softCleanup = () => {
// clean block cache and loaded chunks
world = new World(world.config.version)
globalThis.world = world
world.blockCache = {}
}
const handleMessage = data => {
@ -98,12 +97,14 @@ const handleMessage = data => {
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
allDataReady = true
workerIndex = data.workerIndex
world.instancedBlocks = data.instancedBlocks
world.instancedBlockIds = data.instancedBlockIds || {}
break
}
case 'dirty': {
const loc = new Vec3(data.x, data.y, data.z)
setSectionDirty(loc, data.value)
setSectionDirty(loc, data.value, data.instancingMode || InstancingMode.None)
break
}
@ -190,6 +191,18 @@ self.onmessage = ({ data }) => {
handleMessage(data)
}
// Debug flag to spam last geometry output
globalThis.DEBUG_GEOMETRY_SPAM = false // set to true to enable geometry spam for performance testing
globalThis.lastGeometryKey = null
// Track last 50 unique geometry objects with their respective keys for aggressive debugging
interface GeometryEntry {
key: string
geometry: any
}
const lastGeometryEntries: GeometryEntry[] = []
const MAX_GEOMETRY_ENTRIES = 50
setInterval(() => {
if (world === null || !allDataReady) return
@ -197,23 +210,37 @@ setInterval(() => {
// console.log(sections.length + ' dirty sections')
// const start = performance.now()
for (const key of dirtySections.keys()) {
for (const [key, { instancingMode }] of dirtySections.entries()) {
const [x, y, z] = key.split(',').map(v => parseInt(v, 10))
const chunk = world.getColumn(x, z)
let processTime = 0
if (chunk?.getSection(new Vec3(x, y, z))) {
const start = performance.now()
const geometry = getSectionGeometry(x, y, z, world)
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
//@ts-expect-error
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
const geometry = getSectionGeometry(x, y, z, world, instancingMode)
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) as any
postMessage({ type: 'geometry', key, geometry, workerIndex }/* , transferable */)
processTime = performance.now() - start
// Store last geometry for debug spam
globalThis.lastGeometryKey = key
// Track unique geometry entries for aggressive debugging
const existingIndex = lastGeometryEntries.findIndex(entry => entry.key === key)
if (existingIndex >= 0) {
// Update existing entry with new geometry
lastGeometryEntries[existingIndex].geometry = geometry
} else {
// Add new entry
lastGeometryEntries.push({ key, geometry })
if (lastGeometryEntries.length > MAX_GEOMETRY_ENTRIES) {
lastGeometryEntries.shift() // Remove oldest
}
}
} else {
// console.info('[mesher] Missing section', x, y, z)
}
const dirtyTimes = dirtySections.get(key)
if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy')
for (let i = 0; i < dirtyTimes; i++) {
for (let i = 0; i < dirtyTimes.times; i++) {
postMessage({ type: 'sectionFinished', key, workerIndex, processTime })
processTime = 0
}
@ -237,3 +264,24 @@ setInterval(() => {
// const time = performance.now() - start
// console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`)
}, 50)
// Debug spam: repeatedly send last geometry output every 100ms
setInterval(() => {
if (globalThis.DEBUG_GEOMETRY_SPAM) {
// Send the last geometry
// Aggressive debugging: send all tracked geometry entries with their respective geometries
// console.log(`[DEBUG] Sending ${lastGeometryEntries.length} unique geometry entries:`, lastGeometryEntries.map(e => e.key))
// Send each unique geometry entry with its respective geometry for maximum stress testing
for (const entry of lastGeometryEntries) {
postMessage({
type: 'geometry',
key: entry.key,
geometry: entry.geometry,
workerIndex,
debug: true // Mark as debug message
})
}
}
}, 20)

View file

@ -5,8 +5,8 @@ import { BlockType } from '../../../playground/shared'
import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock } from './world'
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
import { INVISIBLE_BLOCKS } from './worldConstants'
import { MesherGeometryOutput, HighestBlockInfo } from './shared'
import { MesherGeometryOutput, InstancingMode } from './shared'
import { isBlockInstanceable } from './instancingUtils'
let blockProvider: WorldBlockProvider
@ -132,7 +132,11 @@ const getVec = (v: Vec3, dir: Vec3) => {
return v.plus(dir)
}
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) {
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean, instancingEnabled: boolean) {
if (instancingEnabled) {
return // todo
}
const heights: number[] = []
for (let z = -1; z <= 1; z++) {
for (let x = -1; x <= 1; x++) {
@ -518,10 +522,64 @@ const isBlockWaterlogged = (block: Block) => {
return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name)
}
const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boolean => {
// Early return for blocks that should never be culled
if (block.transparent) return false
// Check if all 6 faces would be culled (hidden by neighbors)
const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice')
// Cache cursor offset to avoid creating new Vec3 instances
const offsetCursor = new Vec3(0, 0, 0)
// eslint-disable-next-line guard-for-in
for (const face in elemFaces) {
const { dir } = elemFaces[face]
offsetCursor.set(cursor.x + dir[0], cursor.y + dir[1], cursor.z + dir[2])
const neighbor = world.getBlock(offsetCursor, blockProvider, {})
// Face is exposed to air/void - block must be rendered
if (!neighbor) return false
// Handle special case for identical blocks (glass/ice)
if (cullIfIdentical && neighbor.stateId === block.stateId) continue
// If neighbor is not a full opaque cube, face is visible
if (neighbor.transparent || !isCube(neighbor)) return false
}
// All faces are culled, block should not be rendered
return true
}
// Add matrix calculation helper
function calculateInstanceMatrix (pos: { x: number, y: number, z: number }, offset = 0.5): number[] {
// Create a 4x4 matrix array (16 elements)
const matrix = Array.from({ length: 16 }).fill(0) as number[]
// Set identity matrix
matrix[0] = 1 // m11
matrix[5] = 1 // m22
matrix[10] = 1 // m33
matrix[15] = 1 // m44
// Set translation (position)
matrix[12] = pos.x + offset // tx
matrix[13] = pos.y + offset // ty
matrix[14] = pos.z + offset // tz
return matrix
}
let unknownBlockModel: BlockModelPartsResolved
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) {
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World, instancingMode = InstancingMode.None): MesherGeometryOutput {
let delayedRender = [] as Array<() => void>
// Check if instanced rendering is enabled for this section
const enableInstancedRendering = instancingMode !== InstancingMode.None
const forceInstancedOnly = instancingMode === InstancingMode.BlockInstancingOnly || instancingMode === InstancingMode.ColorOnly
const attr: MesherGeometryOutput = {
sx: sx + 8,
sy: sy + 8,
@ -543,7 +601,8 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
signs: {},
// isFull: true,
hadErrors: false,
blocksCount: 0
blocksCount: 0,
instancedBlocks: {}
}
const cursor = new Vec3(0, 0, 0)
@ -606,14 +665,56 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
const pos = cursor.clone()
// eslint-disable-next-line @typescript-eslint/no-loop-func
delayedRender.push(() => {
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged)
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged, forceInstancedOnly)
})
attr.blocksCount++
} else if (block.name === 'lava') {
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false)
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false, forceInstancedOnly)
attr.blocksCount++
}
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
// Check if this block can use instanced rendering
if ((enableInstancedRendering && isBlockInstanceable(world, block))/* || forceInstancedOnly */) {
// Check if block should be culled (all faces hidden by neighbors)
// TODO validate this
if (shouldCullInstancedBlock(world, cursor, block)) {
// Block is completely surrounded, skip rendering
continue
}
const blockKey = block.name
if (!attr.instancedBlocks[blockKey]) {
attr.instancedBlocks[blockKey] = {
stateId: block.stateId,
blockName: block.name,
positions: [],
matrices: [] // Add matrices array
}
}
const pos = {
x: cursor.x,
y: cursor.y,
z: cursor.z
}
// Pre-calculate transformation matrix
const offset = instancingMode === InstancingMode.ColorOnly ? 0 : 0.5
const matrix = calculateInstanceMatrix(pos, offset)
attr.instancedBlocks[blockKey].positions.push(pos)
attr.instancedBlocks[blockKey].matrices.push(matrix)
attr.blocksCount++
continue // Skip regular geometry generation for instanceable blocks
}
// Skip buffer geometry generation if force instanced only mode is enabled
if (forceInstancedOnly) {
// In force instanced only mode, skip all non-instanceable blocks
continue
}
// cache
let { models } = block

View file

@ -1,5 +1,12 @@
import { BlockType } from '../../../playground/shared'
export enum InstancingMode {
None = 'none',
ColorOnly = 'color_only',
BlockInstancing = 'block_instancing',
BlockInstancingOnly = 'block_instancing_only'
}
// only here for easier testing
export const defaultMesherConfig = {
version: '',
@ -12,7 +19,7 @@ export const defaultMesherConfig = {
// textureSize: 1024, // for testing
debugModelVariant: undefined as undefined | number[],
clipWorldBelowY: undefined as undefined | number,
disableSignsMapsSupport: false
disableSignsMapsSupport: false,
}
export type CustomBlockModels = {
@ -21,6 +28,19 @@ export type CustomBlockModels = {
export type MesherConfig = typeof defaultMesherConfig
export interface InstancedBlockEntry {
stateId: number
blockName: string
positions: Array<{ x: number, y: number, z: number }>
matrices: number[][] // Pre-calculated transformation matrices from worker
}
export type InstancingMesherData = {
blocks: {
[stateId: number]: number // instance id
}
}
export type MesherGeometryOutput = {
sx: number,
sy: number,
@ -45,6 +65,8 @@ export type MesherGeometryOutput = {
hadErrors: boolean
blocksCount: number
customBlockModels?: CustomBlockModels
// New instanced blocks data
instancedBlocks: Record<string, InstancedBlockEntry>
}
export interface MesherMainEvents {

View file

@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
import legacyJson from '../../../../src/preflatMap.json'
import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey } from './shared'
import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig } from './shared'
import { INVISIBLE_BLOCKS } from './worldConstants'
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
@ -42,12 +42,14 @@ export class World {
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels
sentBlockStateModels = new Set<string>()
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
instancedBlocks: number[] = []
instancedBlockIds = {} as Record<number, number>
constructor (version) {
constructor (version: string) {
this.Chunk = Chunks(version) as any
this.biomeCache = mcData(version).biomes
this.preflat = !mcData(version).supportFeature('blockStateId')
this.config.version = version
this.config = { ...defaultMesherConfig, version }
}
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
@ -121,7 +123,6 @@ export class World {
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
const modelOverride = this.customBlockModels.get(key)?.[blockPosKey]
const column = this.columns[key]
// null column means chunk not loaded
@ -131,6 +132,7 @@ export class World {
const locInChunk = posInChunk(loc)
const stateId = column.getBlockStateId(locInChunk)
const modelOverride = stateId ? this.customBlockModels.get(key)?.[blockPosKey] : undefined
const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride)
if (!this.blockCache[cacheKey]) {

View file

@ -20,7 +20,7 @@ export type WorldDataEmitterEvents = {
entityMoved: (data: any) => void
playerEntity: (data: any) => void
time: (data: number) => void
renderDistance: (viewDistance: number) => void
renderDistance: (viewDistance: number, keepChunksDistance: number) => void
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
markAsLoaded: (data: { x: number, z: number }) => void
unloadChunk: (data: { x: number, z: number }) => void
@ -79,7 +79,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance
this.emitter.emit('renderDistance', viewDistance)
this.emitter.emit('renderDistance', viewDistance, this.keepChunksDistance)
}
listenToBot (bot: typeof __type_bot) {

View file

@ -12,7 +12,7 @@ import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
import { SoundSystem } from '../three/threeJsSound'
import { buildCleanupDecorator } from './cleanupDecorator'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, InstancingMode } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
import { WorldDataEmitterWorker } from './worldDataEmitter'
@ -56,7 +56,16 @@ export const defaultWorldRendererConfig = {
enableDebugOverlay: false,
_experimentalSmoothChunkLoading: true,
_renderByChunks: false,
volume: 1
volume: 1,
// New instancing options
useInstancedRendering: false,
forceInstancedOnly: false,
dynamicInstancing: false,
dynamicInstancingModeDistance: 1, // chunks beyond this distance use instancing only
dynamicColorModeDistance: 1, // chunks beyond this distance use color mode only
instancedOnlyDistance: 6, // chunks beyond this distance use instancing only
enableSingleColorMode: false, // ultra-performance mode with solid colors
autoLowerRenderDistance: false,
}
export type WorldRendererConfig = typeof defaultWorldRendererConfig
@ -155,6 +164,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
abstract changeBackgroundColor (color: [number, number, number]): void
// Optional method for getting instanced blocks data (implemented by Three.js renderer)
getInstancedBlocksData? (): { instanceableBlocks?: Set<number>, allBlocksStateIdToModelIdMap?: Record<number, number> } | undefined
worldRendererConfig: WorldRendererConfig
playerStateReactive: PlayerStateReactive
playerStateUtils: PlayerStateUtils
@ -476,7 +488,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.allChunksFinished = true
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
}
this.updateChunksStats()
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
@ -591,6 +602,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
const resources = this.resourcesManager.currentResources
if (this.workers.length === 0) throw new Error('workers not initialized yet')
// Get instanceable blocks data if available (Three.js specific)
const instancedBlocksData = this.getInstancedBlocksData?.()
for (const [i, worker] of this.workers.entries()) {
const { blockstatesModels } = resources
@ -602,6 +617,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
},
blockstatesModels,
config: this.getMesherConfig(),
instancedBlocks: instancedBlocksData?.instanceableBlocks ? [...instancedBlocksData.instanceableBlocks] : [],
instancedBlockIds: instancedBlocksData?.allBlocksStateIdToModelIdMap || {}
})
}
@ -670,6 +687,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.checkAllFinished()
}
debugRemoveCurrentChunk () {
const [x, z] = chunkPos(this.viewerChunkPosition!)
this.removeColumn(x, z)
}
removeColumn (x, z) {
delete this.loadedChunks[`${x},${z}`]
for (const worker of this.workers) {
@ -915,7 +937,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
return Promise.all(data)
}
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false, instancingMode = InstancingMode.None) { // value false is used for unloading chunks
if (!this.forceCallFromMesherReplayer && this.mesherLogReader) return
if (this.viewDistance === -1) throw new Error('viewDistance not set')
@ -929,7 +951,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
// Dispatch sections to workers based on position
// This guarantees uniformity accross workers and that a given section
// is always dispatched to the same worker
const hash = this.getWorkerNumber(pos, useChangeWorker && this.mesherLogger.active)
const hash = this.getWorkerNumber(pos, this.mesherLogger.active)
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
if (this.forceCallFromMesherReplayer) {
this.workers[hash].postMessage({
@ -938,17 +960,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
y: pos.y,
z: pos.z,
value,
instancingMode,
config: this.getMesherConfig(),
})
} else {
this.toWorkerMessagesQueue[hash] ??= []
this.toWorkerMessagesQueue[hash].push({
// this.workers[hash].postMessage({
type: 'dirty',
x: pos.x,
y: pos.y,
z: pos.z,
value,
instancingMode,
config: this.getMesherConfig(),
})
this.dispatchMessages()

View file

@ -0,0 +1,743 @@
import PrismarineChatLoader from 'prismarine-chat'
import * as THREE from 'three'
import * as nbt from 'prismarine-nbt'
import { Vec3 } from 'vec3'
import { MesherGeometryOutput } from '../lib/mesher/shared'
import { chunkPos } from '../lib/simpleUtils'
import { renderSign } from '../sign-renderer'
import { getMesh } from './entity/EntityMesh'
import type { WorldRendererThree } from './worldrendererThree'
import { armorModel } from './entity/armorModels'
import { disposeObject } from './threeJsUtils'
export interface ChunkMeshPool {
mesh: THREE.Mesh
inUse: boolean
lastUsedTime: number
sectionKey?: string
}
export interface SectionObject extends THREE.Group {
mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
tilesCount?: number
blocksCount?: number
signsContainer?: THREE.Group
headsContainer?: THREE.Group
boxHelper?: THREE.BoxHelper
fountain?: boolean
}
export class ChunkMeshManager {
private readonly meshPool: ChunkMeshPool[] = []
private readonly activeSections = new Map<string, ChunkMeshPool>()
readonly sectionObjects: Record<string, SectionObject> = {}
private poolSize: number
private maxPoolSize: number
private minPoolSize: number
private readonly signHeadsRenderer: SignHeadsRenderer
// Performance tracking
private hits = 0
private misses = 0
// Debug flag to bypass pooling
public bypassPooling = false
// Performance monitoring
private readonly renderTimes: number[] = []
private readonly maxRenderTimeSamples = 30
private _performanceOverrideDistance?: number
private lastPerformanceCheck = 0
private readonly performanceCheckInterval = 2000 // Check every 2 seconds
get performanceOverrideDistance () {
return this._performanceOverrideDistance ?? 0
}
set performanceOverrideDistance (value: number | undefined) {
this._performanceOverrideDistance = value
this.updateSectionsVisibility()
}
constructor (
public worldRenderer: WorldRendererThree,
public scene: THREE.Group,
public material: THREE.Material,
public worldHeight: number,
viewDistance = 3,
) {
this.updateViewDistance(viewDistance)
this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer)
this.initializePool()
}
private initializePool () {
// Create initial pool
for (let i = 0; i < this.poolSize; i++) {
const geometry = new THREE.BufferGeometry()
const mesh = new THREE.Mesh(geometry, this.material)
mesh.visible = false
mesh.matrixAutoUpdate = false
mesh.name = 'pooled-section-mesh'
const poolEntry: ChunkMeshPool = {
mesh,
inUse: false,
lastUsedTime: 0
}
this.meshPool.push(poolEntry)
// Don't add to scene here - meshes will be added to containers
}
}
/**
* Update or create a section with new geometry data
*/
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
// Remove existing section object from scene if it exists
let sectionObject = this.sectionObjects[sectionKey]
if (sectionObject) {
this.cleanupSection(sectionKey)
}
// Get or create mesh from pool
let poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
poolEntry = this.acquireMesh()
if (!poolEntry) {
console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
return null
}
this.activeSections.set(sectionKey, poolEntry)
poolEntry.sectionKey = sectionKey
}
const { mesh } = poolEntry
// Update geometry attributes efficiently
this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
// Use direct index assignment for better performance (like before)
mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
// Set bounding box and sphere for the 16x16x16 section
mesh.geometry.boundingBox = new THREE.Box3(
new THREE.Vector3(-8, -8, -8),
new THREE.Vector3(8, 8, 8)
)
mesh.geometry.boundingSphere = new THREE.Sphere(
new THREE.Vector3(0, 0, 0),
Math.sqrt(3 * 8 ** 2)
)
// Position the mesh
mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
mesh.updateMatrix()
mesh.visible = true
mesh.name = 'mesh'
poolEntry.lastUsedTime = performance.now()
// Create or update the section object container
sectionObject = new THREE.Group() as SectionObject
sectionObject.add(mesh)
sectionObject.mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
// Store metadata
sectionObject.tilesCount = geometryData.positions.length / 3 / 4
sectionObject.blocksCount = geometryData.blocksCount
try {
// Add signs container
if (Object.keys(geometryData.signs).length > 0) {
const signsContainer = new THREE.Group()
signsContainer.name = 'signs'
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometryData.signs)) {
const signBlockEntity = this.worldRenderer.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
const sign = this.signHeadsRenderer.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
if (!sign) continue
signsContainer.add(sign)
}
sectionObject.add(signsContainer)
sectionObject.signsContainer = signsContainer
}
// Add heads container
if (Object.keys(geometryData.heads).length > 0) {
const headsContainer = new THREE.Group()
headsContainer.name = 'heads'
for (const [posKey, { isWall, rotation }] of Object.entries(geometryData.heads)) {
const headBlockEntity = this.worldRenderer.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
const head = this.signHeadsRenderer.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
if (!head) continue
headsContainer.add(head)
}
sectionObject.add(headsContainer)
sectionObject.headsContainer = headsContainer
}
} catch (err) {
console.error('ChunkMeshManager: Error adding signs or heads to section', err)
}
// Store and add to scene
this.sectionObjects[sectionKey] = sectionObject
this.scene.add(sectionObject)
sectionObject.matrixAutoUpdate = false
return sectionObject
}
cleanupSection (sectionKey: string) {
// Remove section object from scene
const sectionObject = this.sectionObjects[sectionKey]
if (sectionObject) {
this.scene.remove(sectionObject)
// Dispose signs and heads containers
if (sectionObject.signsContainer) {
this.disposeContainer(sectionObject.signsContainer)
}
if (sectionObject.headsContainer) {
this.disposeContainer(sectionObject.headsContainer)
}
delete this.sectionObjects[sectionKey]
}
}
/**
* Release a section and return its mesh to the pool
*/
releaseSection (sectionKey: string): boolean {
this.cleanupSection(sectionKey)
const poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
return false
}
// Hide mesh and mark as available
poolEntry.mesh.visible = false
poolEntry.inUse = false
poolEntry.sectionKey = undefined
poolEntry.lastUsedTime = 0
// Clear geometry to free memory
this.clearGeometry(poolEntry.mesh.geometry)
this.activeSections.delete(sectionKey)
// Memory cleanup: if pool exceeds max size and we have free meshes, remove one
this.cleanupExcessMeshes()
return true
}
/**
* Get section object if it exists
*/
getSectionObject (sectionKey: string): SectionObject | undefined {
return this.sectionObjects[sectionKey]
}
/**
* Update box helper for a section
*/
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
const sectionObject = this.sectionObjects[sectionKey]
if (!sectionObject?.mesh) return
if (showChunkBorders) {
if (!sectionObject.boxHelper) {
// mesh with static dimensions: 16x16x16
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
staticChunkMesh.position.copy(sectionObject.mesh.position)
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
boxHelper.name = 'helper'
sectionObject.add(boxHelper)
sectionObject.name = 'chunk'
sectionObject.boxHelper = boxHelper
}
sectionObject.boxHelper.visible = true
} else if (sectionObject.boxHelper) {
sectionObject.boxHelper.visible = false
}
}
/**
* Get mesh for section if it exists
*/
getSectionMesh (sectionKey: string): THREE.Mesh | undefined {
return this.activeSections.get(sectionKey)?.mesh
}
/**
* Check if section is managed by this pool
*/
hasSection (sectionKey: string): boolean {
return this.activeSections.has(sectionKey)
}
/**
* Update pool size based on new view distance
*/
updateViewDistance (maxViewDistance: number) {
// Calculate dynamic pool size based on view distance
const chunksInView = (maxViewDistance * 2 + 1) ** 2
const maxSectionsPerChunk = this.worldHeight / 16
const avgSectionsPerChunk = 5
this.minPoolSize = Math.floor(chunksInView * avgSectionsPerChunk)
this.maxPoolSize = Math.floor(chunksInView * maxSectionsPerChunk) + 1
this.poolSize ??= this.minPoolSize
// Expand pool if needed to reach optimal size
if (this.minPoolSize > this.poolSize) {
const targetSize = Math.min(this.minPoolSize, this.maxPoolSize)
this.expandPool(targetSize)
}
console.log(`ChunkMeshManager: Updated view max distance to ${maxViewDistance}, pool: ${this.poolSize}/${this.maxPoolSize}, optimal: ${this.minPoolSize}`)
}
/**
* Get pool statistics
*/
getStats () {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
const memoryUsage = this.getEstimatedMemoryUsage()
return {
poolSize: this.poolSize,
activeCount: this.activeSections.size,
freeCount,
hitRate: `${hitRate}%`,
hits: this.hits,
misses: this.misses,
memoryUsage
}
}
/**
* Get total tiles rendered
*/
getTotalTiles (): number {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.tilesCount || 0), 0)
}
/**
* Get total blocks rendered
*/
getTotalBlocks (): number {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.blocksCount || 0), 0)
}
/**
* Estimate memory usage in MB
*/
getEstimatedMemoryUsage (): { total: string, breakdown: any } {
let totalBytes = 0
let positionBytes = 0
let normalBytes = 0
let colorBytes = 0
let uvBytes = 0
let indexBytes = 0
for (const poolEntry of this.meshPool) {
if (poolEntry.inUse && poolEntry.mesh.geometry) {
const { geometry } = poolEntry.mesh
const position = geometry.getAttribute('position')
if (position) {
const bytes = position.array.byteLength
positionBytes += bytes
totalBytes += bytes
}
const normal = geometry.getAttribute('normal')
if (normal) {
const bytes = normal.array.byteLength
normalBytes += bytes
totalBytes += bytes
}
const color = geometry.getAttribute('color')
if (color) {
const bytes = color.array.byteLength
colorBytes += bytes
totalBytes += bytes
}
const uv = geometry.getAttribute('uv')
if (uv) {
const bytes = uv.array.byteLength
uvBytes += bytes
totalBytes += bytes
}
if (geometry.index) {
const bytes = geometry.index.array.byteLength
indexBytes += bytes
totalBytes += bytes
}
}
}
const totalMB = (totalBytes / (1024 * 1024)).toFixed(2)
return {
total: `${totalMB} MB`,
breakdown: {
position: `${(positionBytes / (1024 * 1024)).toFixed(2)} MB`,
normal: `${(normalBytes / (1024 * 1024)).toFixed(2)} MB`,
color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
}
}
}
/**
* Cleanup and dispose resources
*/
dispose () {
// Release all active sections
for (const [sectionKey] of this.activeSections) {
this.releaseSection(sectionKey)
}
// Dispose all meshes and geometries
for (const poolEntry of this.meshPool) {
// Meshes will be removed from scene when their parent containers are removed
poolEntry.mesh.geometry.dispose()
}
this.meshPool.length = 0
this.activeSections.clear()
}
// Private helper methods
private acquireMesh (): ChunkMeshPool | undefined {
if (this.bypassPooling) {
return {
mesh: new THREE.Mesh(new THREE.BufferGeometry(), this.material),
inUse: true,
lastUsedTime: performance.now()
}
}
// Find first available mesh
const availableMesh = this.meshPool.find(entry => !entry.inUse)
if (availableMesh) {
availableMesh.inUse = true
this.hits++
return availableMesh
}
// No available mesh, expand pool to accommodate new sections
let newPoolSize = Math.min(this.poolSize + 16, this.maxPoolSize)
if (newPoolSize === this.poolSize) {
newPoolSize = this.poolSize + 8
this.maxPoolSize = newPoolSize
console.warn(`ChunkMeshManager: Pool exhausted (${this.poolSize}/${this.maxPoolSize}). Emergency expansion to ${newPoolSize}`)
}
this.misses++
this.expandPool(newPoolSize)
return this.acquireMesh()
}
private expandPool (newSize: number) {
const oldSize = this.poolSize
this.poolSize = newSize
// console.log(`ChunkMeshManager: Expanding pool from ${oldSize} to ${newSize}`)
// Add new meshes to pool
for (let i = oldSize; i < newSize; i++) {
const geometry = new THREE.BufferGeometry()
const mesh = new THREE.Mesh(geometry, this.material)
mesh.visible = false
mesh.matrixAutoUpdate = false
mesh.name = 'pooled-section-mesh'
const poolEntry: ChunkMeshPool = {
mesh,
inUse: false,
lastUsedTime: 0
}
this.meshPool.push(poolEntry)
// Don't add to scene here - meshes will be added to containers
}
}
private updateGeometryAttribute (
geometry: THREE.BufferGeometry,
name: string,
array: Float32Array,
itemSize: number
) {
const attribute = geometry.getAttribute(name)
if (attribute && attribute.count === array.length / itemSize) {
// Reuse existing attribute
;(attribute.array as Float32Array).set(array)
attribute.needsUpdate = true
} else {
// Create new attribute (this will dispose the old one automatically)
geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize))
}
}
private clearGeometry (geometry: THREE.BufferGeometry) {
// Clear attributes but keep the attribute objects for reuse
const attributes = ['position', 'normal', 'color', 'uv']
for (const name of attributes) {
const attr = geometry.getAttribute(name)
if (attr) {
// Just mark as needing update but don't dispose to avoid recreation costs
attr.needsUpdate = true
}
}
if (geometry.index) {
geometry.index.needsUpdate = true
}
}
private cleanupExcessMeshes () {
// If pool size exceeds max and we have free meshes, remove some
if (this.poolSize > this.maxPoolSize) {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
if (freeCount > 0) {
const excessCount = Math.min(this.poolSize - this.maxPoolSize, freeCount)
for (let i = 0; i < excessCount; i++) {
const freeIndex = this.meshPool.findIndex(entry => !entry.inUse)
if (freeIndex !== -1) {
const poolEntry = this.meshPool[freeIndex]
poolEntry.mesh.geometry.dispose()
this.meshPool.splice(freeIndex, 1)
this.poolSize--
}
}
// console.log(`ChunkMeshManager: Cleaned up ${excessCount} excess meshes. Pool size: ${this.poolSize}/${this.maxPoolSize}`)
}
}
}
private disposeContainer (container: THREE.Group) {
disposeObject(container, true)
}
/**
* Record render time for performance monitoring
*/
recordRenderTime (renderTime: number): void {
this.renderTimes.push(renderTime)
if (this.renderTimes.length > this.maxRenderTimeSamples) {
this.renderTimes.shift()
}
// Check performance periodically
const now = performance.now()
if (now - this.lastPerformanceCheck > this.performanceCheckInterval) {
this.checkPerformance()
this.lastPerformanceCheck = now
}
}
/**
* Get current effective render distance
*/
getEffectiveRenderDistance (): number {
return this.performanceOverrideDistance || this.worldRenderer.viewDistance
}
/**
* Force reset performance override
*/
resetPerformanceOverride (): void {
this.performanceOverrideDistance = undefined
this.renderTimes.length = 0
console.log('ChunkMeshManager: Performance override reset')
}
/**
* Get average render time
*/
getAverageRenderTime (): number {
if (this.renderTimes.length === 0) return 0
return this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length
}
/**
* Check if performance is degraded and adjust render distance
*/
private checkPerformance (): void {
if (this.renderTimes.length < this.maxRenderTimeSamples) return
const avgRenderTime = this.getAverageRenderTime()
const targetRenderTime = 16.67 // 60 FPS target (16.67ms per frame)
const performanceThreshold = targetRenderTime * 1.5 // 25ms threshold
if (avgRenderTime > performanceThreshold) {
// Performance is bad, reduce render distance
const currentViewDistance = this.worldRenderer.viewDistance
const newDistance = Math.max(1, Math.floor(currentViewDistance * 0.8))
if (!this.performanceOverrideDistance || newDistance < this.performanceOverrideDistance) {
this.performanceOverrideDistance = newDistance
console.warn(`ChunkMeshManager: Performance degraded (${avgRenderTime.toFixed(2)}ms avg). Reducing effective render distance to ${newDistance}`)
}
} else if (this.performanceOverrideDistance && avgRenderTime < targetRenderTime * 1.1) {
// Performance is good, gradually restore render distance
const currentViewDistance = this.worldRenderer.viewDistance
const newDistance = Math.min(currentViewDistance, this.performanceOverrideDistance + 1)
if (newDistance !== this.performanceOverrideDistance) {
this.performanceOverrideDistance = newDistance >= currentViewDistance ? undefined : newDistance
console.log(`ChunkMeshManager: Performance improved. Restoring render distance to ${newDistance}`)
}
}
}
/**
* Hide sections beyond performance override distance
*/
updateSectionsVisibility (): void {
const cameraPos = this.worldRenderer.cameraSectionPos
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
if (!this.performanceOverrideDistance) {
sectionObject.visible = true
continue
}
const [x, y, z] = sectionKey.split(',').map(Number)
const sectionPos = { x: x / 16, y: y / 16, z: z / 16 }
// Calculate distance using hypot (same as render distance calculation)
const dx = sectionPos.x - cameraPos.x
const dz = sectionPos.z - cameraPos.z
const distance = Math.floor(Math.hypot(dx, dz))
sectionObject.visible = distance <= this.performanceOverrideDistance
}
}
}
class SignHeadsRenderer {
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
constructor (public worldRendererThree: WorldRendererThree) {
}
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
if (!textures) return
try {
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
let skinUrl = textureData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererThree.worldRendererConfig
if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
.replace('https://textures.minecraft.net/', skinTexturesProxy)
}
const mesh = getMesh(this.worldRendererThree, skinUrl, armorModel.head)
const group = new THREE.Group()
if (isWall) {
mesh.position.set(0, 0.3125, 0.3125)
}
// move head model down as armor have a different offset than blocks
mesh.position.y -= 23 / 16
group.add(mesh)
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.scale.set(0.8, 0.8, 0.8)
return group
} catch (err) {
console.error('Error decoding player texture:', err)
}
}
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
const tex = this.getSignTexture(position, blockEntity, isHanging)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
mesh.renderOrder = 999
const lineHeight = 7 / 16
const scaleFactor = isHanging ? 1.3 : 1
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
const thickness = (isHanging ? 2 : 1.5) / 16
const wallSpacing = 0.25 / 16
if (isWall && !isHanging) {
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
} else {
mesh.position.set(0, 0, thickness / 2 + 0.0001)
}
const group = new THREE.Group()
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.add(mesh)
const height = (isHanging ? 10 : 8) / 16
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
const textPosition = height / 2 + heightOffset
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
return group
}
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version)
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
}

View file

@ -735,6 +735,7 @@ export class Entities {
outerGroup.add(mesh)
return {
mesh: outerGroup,
meshGeometry: mesh.children.find(child => child instanceof THREE.Mesh)?.geometry,
isBlock: true,
itemsTexture: null,
itemsTextureFlipped: null,
@ -1345,12 +1346,12 @@ export class Entities {
}
}
raycastSceneDebug () {
debugRaycastScene () {
// return any object from scene. raycast from camera
const raycaster = new THREE.Raycaster()
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
return intersects[0]?.object
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
return intersects.find(intersect => intersect.object.visible)?.object
}
private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {

View file

@ -0,0 +1,30 @@
import legacyJson from '../../../src/preflatMap.json'
export const getPreflatBlock = (block, reportIssue?: () => void) => {
const b = block
b._properties = {}
const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, reportIssue)
if (namePropsStr) {
b.name = namePropsStr.split('[')[0]
const propsStr = namePropsStr.split('[')?.[1]?.split(']')
if (propsStr) {
const newProperties = Object.fromEntries(propsStr.join('').split(',').map(x => {
let [key, val] = x.split('=')
if (!isNaN(val)) val = parseInt(val, 10)
return [key, val]
}))
b._properties = newProperties
}
}
return b
}
const findClosestLegacyBlockFallback = (id, metadata, reportIssue) => {
reportIssue?.()
for (const [key, value] of Object.entries(legacyJson.blocks)) {
const [idKey, meta] = key.split(':')
if (idKey === id) return value
}
return null
}

View file

@ -189,7 +189,7 @@ export default class HoldingBlock {
this.swingAnimator?.stopSwing()
}
render (originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) {
render (renderer: THREE.WebGLRenderer) {
if (!this.lastHeldItem) return
const now = performance.now()
if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick
@ -205,15 +205,13 @@ export default class HoldingBlock {
this.blockSwapAnimation?.switcher.update()
const scene = new THREE.Scene()
const scene = this.worldRenderer.templateScene
scene.add(this.cameraGroup)
// if (this.camera.aspect !== originalCamera.aspect) {
// this.camera.aspect = originalCamera.aspect
// this.camera.updateProjectionMatrix()
// }
this.updateCameraGroup()
scene.add(ambientLight.clone())
scene.add(directionalLight.clone())
const viewerSize = renderer.getSize(new THREE.Vector2())
const minSize = Math.min(viewerSize.width, viewerSize.height)
@ -241,6 +239,8 @@ export default class HoldingBlock {
if (offHandDisplay) {
this.cameraGroup.scale.x = 1
}
scene.remove(this.cameraGroup)
}
// worldTest () {

View file

@ -0,0 +1,860 @@
import * as THREE from 'three'
import { Vec3 } from 'vec3'
import { versionToNumber } from 'flying-squid/dist/utils'
import PrismarineBlock from 'prismarine-block'
import { IndexedBlock } from 'minecraft-data'
import moreBlockData from '../lib/moreBlockDataGenerated.json'
import { InstancingMode, MesherGeometryOutput } from '../lib/mesher/shared'
import { getPreflatBlock } from './getPreflatBlock'
import { WorldRendererThree } from './worldrendererThree'
// Helper function to parse RGB color strings from moreBlockDataGenerated.json
function parseRgbColor (rgbString: string): number {
const match = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(rgbString)
if (!match) return 0x99_99_99 // Default gray
const r = parseInt(match[1], 10)
const g = parseInt(match[2], 10)
const b = parseInt(match[3], 10)
return (r << 16) | (g << 8) | b
}
export interface InstancedBlockData {
stateId: number
positions: Vec3[]
blockName: string
}
export interface InstancedSectionData {
sectionKey: string
instancedBlocks: Map<number, InstancedBlockData>
shouldUseInstancedOnly: boolean
}
export interface InstancedBlockModelData {
stateId: number
// textures: number[]
rotation: number[]
transparent?: boolean
emitLight?: number
filterLight?: number
textureInfos?: Array<{ u: number, v: number, su: number, sv: number }>
}
export interface InstancedBlocksConfig {
instanceableBlocks: Set<number>
blocksDataModel: Record<number, InstancedBlockModelData>
blockNameToStateIdMap: Record<string, number>
interestedTextureTiles: Set<string>
}
export class InstancedRenderer {
isPreflat: boolean
USE_APP_GEOMETRY = true
private readonly instancedMeshes = new Map<number, THREE.InstancedMesh>()
private readonly sceneUsedMeshes = new Map<string, THREE.InstancedMesh>()
private readonly blockCounts = new Map<number, number>()
private readonly sectionInstances = new Map<string, Map<number, number[]>>()
private readonly cubeGeometry: THREE.BoxGeometry
private readonly tempMatrix = new THREE.Matrix4()
private readonly stateIdToName = new Map<number, string>()
// Cache for single color materials
private readonly colorMaterials = new Map<number, THREE.MeshBasicMaterial>()
// Dynamic instance management
private readonly initialInstancesPerBlock = 2000
private readonly maxInstancesPerBlock = 100_000
private readonly maxTotalInstances = 10_000_000
private currentTotalInstances = 0
private readonly growthFactor = 1.5 // How much to grow when needed
// Visibility control
private _instancedMeshesVisible = true
// Memory tracking
private totalAllocatedInstances = 0
private instancedBlocksConfig: InstancedBlocksConfig | null = null
private sharedSolidMaterial: THREE.MeshLambertMaterial | null = null
constructor (private readonly worldRenderer: WorldRendererThree) {
this.cubeGeometry = this.createCubeGeometry()
this.isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13')
// Create shared solid material with no transparency
this.sharedSolidMaterial = new THREE.MeshLambertMaterial({
transparent: false,
alphaTest: 0.1
})
}
private getStateId (blockName: string): number {
if (!this.instancedBlocksConfig) {
throw new Error('Instanced blocks config not prepared')
}
const stateId = this.instancedBlocksConfig.blockNameToStateIdMap[blockName]
if (stateId === undefined) {
throw new Error(`Block ${blockName} not found in blockNameToStateIdMap`)
}
return stateId
}
// Add getter/setter for visibility
get instancedMeshesVisible (): boolean {
return this._instancedMeshesVisible
}
set instancedMeshesVisible (visible: boolean) {
this._instancedMeshesVisible = visible
// Update all instanced meshes visibility
for (const mesh of this.instancedMeshes.values()) {
mesh.visible = visible
}
}
private getInitialInstanceCount (blockName: string): number {
// Start with small allocation, can grow later if needed
return Math.min(this.initialInstancesPerBlock, this.maxInstancesPerBlock)
}
debugResizeMesh () {
// Debug helper to test resize operation
const blockName = 'grass_block'
const stateId = this.getStateId(blockName)
const mesh = this.instancedMeshes.get(stateId)
this.resizeInstancedMesh(stateId, mesh!.instanceMatrix.count * this.growthFactor)
}
private resizeInstancedMesh (stateId: number, newSize: number): boolean {
const mesh = this.instancedMeshes.get(stateId)
if (!mesh) return false
const blockName = this.stateIdToName.get(stateId) || 'unknown'
const oldSize = mesh.instanceMatrix.count
const actualInstanceCount = this.blockCounts.get(stateId) || 0
// console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`)
const { geometry } = mesh
const { material } = mesh
// Create new mesh with increased capacity
const newMesh = new THREE.InstancedMesh(
geometry,
material,
newSize
)
newMesh.name = mesh.name
newMesh.frustumCulled = false
newMesh.visible = this._instancedMeshesVisible
// Copy ALL existing instances using our tracked count
for (let i = 0; i < actualInstanceCount; i++) {
this.tempMatrix.identity()
mesh.getMatrixAt(i, this.tempMatrix)
newMesh.setMatrixAt(i, this.tempMatrix)
}
newMesh.count = actualInstanceCount
newMesh.instanceMatrix.needsUpdate = true
this.totalAllocatedInstances += (newSize - oldSize)
this.worldRenderer.scene.add(newMesh)
this.instancedMeshes.set(stateId, newMesh)
this.worldRenderer.scene.remove(mesh)
// Clean up old mesh
mesh.geometry.dispose()
if (Array.isArray(mesh.material)) {
for (const m of mesh.material) m.dispose()
} else {
mesh.material.dispose()
}
// Verify instance count matches
// console.log(`Finished growing ${blockName}. Actual instances: ${actualInstanceCount}, New capacity: ${newSize}, Mesh count: ${newMesh.count}`)
return true
}
private canAddMoreInstances (stateId: number, count: number): boolean {
const currentForBlock = this.blockCounts.get(stateId) || 0
const mesh = this.instancedMeshes.get(stateId)
if (!mesh) return false
const blockName = this.stateIdToName.get(stateId) || 'unknown'
// If we would exceed current capacity, try to grow
if (currentForBlock + count > mesh.instanceMatrix.count) {
const currentCapacity = mesh.instanceMatrix.count
const neededCapacity = currentForBlock + count
const newSize = Math.min(
this.maxInstancesPerBlock,
Math.ceil(Math.max(
neededCapacity,
currentCapacity * this.growthFactor
))
)
// console.log(`Need to grow ${blockName}: current ${currentForBlock}/${currentCapacity}, need ${neededCapacity}, growing to ${newSize}`)
// Check if growth would exceed total budget
const growthAmount = newSize - currentCapacity
if (this.totalAllocatedInstances + growthAmount > this.maxTotalInstances) {
console.warn(`Cannot grow instances for ${blockName}: would exceed total budget`)
return false
}
// Try to grow
if (!this.resizeInstancedMesh(stateId, newSize)) {
console.warn(`Failed to grow instances for ${blockName}`)
return false
}
}
// Check total instance budget
if (this.currentTotalInstances + count > this.maxTotalInstances) {
console.warn(`Total instance limit reached (${this.currentTotalInstances}/${this.maxTotalInstances})`)
return false
}
return true
}
prepareInstancedBlock (stateId: number, name: string, props: Record<string, any>, mcBlockData?: IndexedBlock, defaultState = false) {
const config = this.instancedBlocksConfig!
const possibleIssues = [] as string[]
const { currentResources } = this.worldRenderer.resourcesManager
if (!currentResources?.worldBlockProvider) return
const models = currentResources.worldBlockProvider.getAllResolvedModels0_1({
name,
properties: props
}, this.isPreflat, possibleIssues, [], [], true)
// skipping composite blocks
if (models.length !== 1 || !models[0]![0].elements) {
return
}
const elements = models[0]![0]?.elements
if (!elements || (elements.length !== 1 && name !== 'grass_block')) {
return
}
const elem = elements[0]
if (elem.from[0] !== 0 || elem.from[1] !== 0 || elem.from[2] !== 0 || elem.to[0] !== 16 || elem.to[1] !== 16 || elem.to[2] !== 16) {
// not full block
return
}
const facesMapping = [
['front', 'south'],
['bottom', 'down'],
['top', 'up'],
['right', 'east'],
['left', 'west'],
['back', 'north'],
]
const blockData: InstancedBlockModelData = {
stateId,
rotation: [0, 0, 0, 0, 0, 0],
textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 }))
}
for (const [face, { texture, cullface, rotation = 0 }] of Object.entries(elem.faces)) {
const faceIndex = facesMapping.findIndex(x => x.includes(face))
if (faceIndex === -1) {
throw new Error(`Unknown face ${face}`)
}
blockData.rotation[faceIndex] = rotation / 90
if (Math.floor(blockData.rotation[faceIndex]) !== blockData.rotation[faceIndex]) {
throw new Error(`Invalid rotation ${rotation} ${name}`)
}
config.interestedTextureTiles.add(texture.debugName)
// Store texture info for this face
blockData.textureInfos![faceIndex] = {
u: texture.u,
v: texture.v,
su: texture.su,
sv: texture.sv
}
}
config.blocksDataModel[stateId] = blockData
config.instanceableBlocks.add(stateId)
config.blockNameToStateIdMap[name] = stateId
if (mcBlockData) {
blockData.transparent = mcBlockData.transparent
blockData.emitLight = mcBlockData.emitLight
blockData.filterLight = mcBlockData.filterLight
}
}
prepareInstancedBlocksData () {
if (this.sharedSolidMaterial) {
this.sharedSolidMaterial.dispose()
this.sharedSolidMaterial = null
}
this.sharedSolidMaterial = new THREE.MeshLambertMaterial({
transparent: true,
// depthWrite: true,
alphaTest: 0.1
})
this.sharedSolidMaterial.map = this.worldRenderer.material.map
// this.sharedTransparentMaterial = new THREE.MeshLambertMaterial({
// transparent: true,
// // depthWrite: false,
// alphaTest: 0.1
// })
// this.sharedTransparentMaterial.map = this.worldRenderer.material.map
const { forceInstancedOnly } = this.worldRenderer.worldRendererConfig
const debugBlocksMap = forceInstancedOnly ? {
'double_stone_slab': 'stone',
'stone_slab': 'stone',
'oak_stairs': 'planks',
'stone_stairs': 'stone',
'glass_pane': 'stained_glass',
'brick_stairs': 'brick_block',
'stone_brick_stairs': 'stonebrick',
'nether_brick_stairs': 'nether_brick',
'double_wooden_slab': 'planks',
'wooden_slab': 'planks',
'sandstone_stairs': 'sandstone',
'cobblestone_wall': 'cobblestone',
'quartz_stairs': 'quartz_block',
'stained_glass_pane': 'stained_glass',
'red_sandstone_stairs': 'red_sandstone',
'stone_slab2': 'stone_slab',
'purpur_stairs': 'purpur_block',
'purpur_slab': 'purpur_block',
} : {}
const PBlockOriginal = PrismarineBlock(this.worldRenderer.version)
this.instancedBlocksConfig = {
instanceableBlocks: new Set(),
blocksDataModel: {},
blockNameToStateIdMap: {},
interestedTextureTiles: new Set(),
} satisfies InstancedBlocksConfig
// Add unknown block model
this.prepareInstancedBlock(-1, 'unknown', {})
// Handle texture overrides for special blocks
const textureOverrideFullBlocks = {
water: 'water_still',
lava: 'lava_still',
}
// Process all blocks to find instanceable ones
for (const b of loadedData.blocksArray) {
for (let stateId = b.minStateId; stateId <= b.maxStateId; stateId++) {
const config = this.instancedBlocksConfig
const mapping = debugBlocksMap[b.name]
const block = PBlockOriginal.fromStateId(mapping && loadedData.blocksByName[mapping] ? loadedData.blocksByName[mapping].defaultState : stateId, 0)
if (this.isPreflat) {
getPreflatBlock(block)
}
const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined
if (textureOverride) {
const { currentResources } = this.worldRenderer.resourcesManager
if (!currentResources?.worldBlockProvider) continue
const texture = currentResources.worldBlockProvider.getTextureInfo(textureOverride)
if (!texture) {
console.warn('Missing texture override for', block.name)
continue
}
const texIndex = texture.tileIndex
config.blocksDataModel[stateId] = {
stateId,
rotation: [0, 0, 0, 0, 0, 0],
filterLight: b.filterLight,
textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({
u: texture.u,
v: texture.v,
su: texture.su,
sv: texture.sv
}))
}
config.instanceableBlocks.add(block.stateId)
config.interestedTextureTiles.add(textureOverride)
config.blockNameToStateIdMap[block.name] = stateId
continue
}
// Check if block is a full cube
if (block.shapes.length === 0 || !block.shapes.every(shape => {
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
})) {
continue
}
this.prepareInstancedBlock(stateId, block.name, block.getProperties(), b, stateId === b.defaultState)
}
}
}
private getOrCreateColorMaterial (blockName: string): THREE.Material {
const color = this.getBlockColor(blockName)
const materialKey = color
let material = this.colorMaterials.get(materialKey)
if (!material) {
material = new THREE.MeshBasicMaterial({
color,
transparent: false
})
material.name = `instanced_color_${blockName}`
this.colorMaterials.set(materialKey, material)
}
return material
}
private createBlockMaterial (blockName: string, instancingMode: InstancingMode): THREE.Material {
if (instancingMode === InstancingMode.ColorOnly) {
return this.getOrCreateColorMaterial(blockName)
} else {
return this.sharedSolidMaterial!
}
}
// Update initializeInstancedMeshes to respect visibility setting
initializeInstancedMeshes () {
if (!this.instancedBlocksConfig) {
console.warn('Instanced blocks config not prepared')
return
}
// Create InstancedMesh for each instanceable block type
for (const stateId of this.instancedBlocksConfig.instanceableBlocks) {
const blockName = this.stateIdToName.get(stateId)
if (blockName) {
this.initializeInstancedMesh(stateId, blockName, InstancingMode.ColorOnly)
}
}
}
initializeInstancedMesh (stateId: number, blockName: string, instancingMode: InstancingMode) {
if (this.instancedMeshes.has(stateId)) return // Skip if already exists
if (!this.instancedBlocksConfig!.blocksDataModel) {
this.prepareInstancedBlock(stateId, blockName, {})
}
const blockModelData = this.instancedBlocksConfig!.blocksDataModel[stateId]
const isTransparent = blockModelData?.transparent ?? false
const initialCount = this.getInitialInstanceCount(blockName)
const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry
const material = this.createBlockMaterial(blockName, instancingMode)
const mesh = new THREE.InstancedMesh(
geometry,
material,
initialCount
)
mesh.name = `instanced_${blockName}`
mesh.frustumCulled = false
mesh.count = 0
mesh.visible = this._instancedMeshesVisible // Set initial visibility
// mesh.renderOrder = isTransparent ? 1 : 0
this.instancedMeshes.set(stateId, mesh)
// Don't add to scene until actually used
this.totalAllocatedInstances += initialCount
if (!blockModelData) {
console.warn(`No block model data found for block ${blockName}`)
}
}
private debugRaycast () {
// get instanced block name
const raycaster = new THREE.Raycaster()
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
for (const intersect of intersects) {
const mesh = intersect.object as THREE.Mesh
if (mesh.name.startsWith('instanced_')) {
console.log(`Instanced block name: ${mesh.name}`)
}
}
}
private createCubeGeometry (): THREE.BoxGeometry {
// Create a basic cube geometry
// For proper texturing, we would need to modify UV coordinates per block type
// For now, use default BoxGeometry which works with the texture atlas
const geometry = new THREE.BoxGeometry(1, 1, 1)
return geometry
}
private createCustomGeometry (stateId: number, blockModelData: InstancedBlockModelData): THREE.BufferGeometry {
if (this.USE_APP_GEOMETRY) {
const itemMesh = this.worldRenderer.entities.getItemMesh(stateId === -1 ? {
name: 'unknown'
} : {
blockState: stateId
}, {})
return itemMesh?.meshGeometry
}
// Create custom geometry with specific UV coordinates per face
const geometry = new THREE.BoxGeometry(1, 1, 1)
// Get UV attribute
const uvAttribute = geometry.getAttribute('uv') as THREE.BufferAttribute
const uvs = uvAttribute.array as Float32Array
if (!blockModelData.textureInfos) {
console.warn('No texture infos available for block model')
return geometry
}
// BoxGeometry has 6 faces, each with 4 vertices (8 UV values)
// Three.js BoxGeometry face order: +X, -X, +Y, -Y, +Z, -Z
// Our face mapping: [front, bottom, top, right, left, back] = [south, down, up, east, west, north]
// Map to Three.js indices: [+Z, -Y, +Y, +X, -X, -Z] = [4, 3, 2, 0, 1, 5]
interface UVVertex {
u: number
v: number
}
for (let faceIndex = 0; faceIndex < 6; faceIndex++) {
// Map Three.js face index to our face index
let ourFaceIndex: number
switch (faceIndex) {
case 0: ourFaceIndex = 3; break // +X -> right (east)
case 1: ourFaceIndex = 4; break // -X -> left (west)
case 2: ourFaceIndex = 2; break // +Y -> top (up)
case 3: ourFaceIndex = 1; break // -Y -> bottom (down)
case 4: ourFaceIndex = 0; break // +Z -> front (south)
case 5: ourFaceIndex = 5; break // -Z -> back (north)
default: continue
}
const textureInfo = blockModelData.textureInfos[ourFaceIndex]
const rotation = blockModelData.rotation[ourFaceIndex]
if (!textureInfo) {
console.warn(`No texture info found for face ${ourFaceIndex}`)
continue
}
const { u, v, su, sv } = textureInfo
const faceUvStart = faceIndex * 8
// Get original UVs for this face
const faceUVs = uvs.slice(faceUvStart, faceUvStart + 8)
// Apply rotation if needed (0=0°, 1=90°, 2=180°, 3=270°)
// Add base 180° rotation (2) to all faces
const totalRotation = (rotation + 2) % 4
if (totalRotation > 0) {
// Each vertex has 2 UV coordinates (u,v)
// We need to rotate the 4 vertices as a group
const vertices: UVVertex[] = []
for (let i = 0; i < 8; i += 2) {
vertices.push({
u: faceUVs[i],
v: faceUVs[i + 1]
})
}
// Rotate vertices
const rotatedVertices: UVVertex[] = []
for (let i = 0; i < 4; i++) {
const srcIndex = (i + totalRotation) % 4
rotatedVertices.push(vertices[srcIndex])
}
// Write back rotated coordinates
for (let i = 0; i < 4; i++) {
faceUVs[i * 2] = rotatedVertices[i].u
faceUVs[i * 2 + 1] = rotatedVertices[i].v
}
}
// Apply texture atlas coordinates to the potentially rotated UVs
for (let i = 0; i < 8; i += 2) {
uvs[faceUvStart + i] = u + faceUVs[i] * su
uvs[faceUvStart + i + 1] = v + faceUVs[i + 1] * sv
}
}
uvAttribute.needsUpdate = true
return geometry
}
private getBlockColor (blockName: string): number {
// Get color from moreBlockDataGenerated.json
const rgbString = moreBlockData.colors[blockName]
if (rgbString) {
return parseRgbColor(rgbString)
}
// Debug: Log when color is not found
console.warn(`No color found for block: ${blockName}, using default gray`)
// Fallback to default gray if color not found
return 0x99_99_99
}
handleInstancedBlocksFromWorker (instancedBlocks: MesherGeometryOutput['instancedBlocks'], sectionKey: string, instancingMode: InstancingMode) {
// Initialize section tracking if not exists
if (!this.sectionInstances.has(sectionKey)) {
this.sectionInstances.set(sectionKey, new Map())
}
const sectionMap = this.sectionInstances.get(sectionKey)!
// Remove old instances for blocks that are being updated
const previousStateIds = [...sectionMap.keys()]
for (const stateId of previousStateIds) {
const instanceIndices = sectionMap.get(stateId)
if (instanceIndices) {
this.removeInstancesFromBlock(stateId, instanceIndices)
sectionMap.delete(stateId)
}
}
// Keep track of blocks that were updated this frame
for (const [blockName, blockData] of Object.entries(instancedBlocks)) {
const { stateId, positions, matrices } = blockData
this.stateIdToName.set(stateId, blockName)
if (this.USE_APP_GEOMETRY) {
this.initializeInstancedMesh(stateId, blockName, instancingMode)
}
const instanceIndices: number[] = []
const currentCount = this.blockCounts.get(stateId) || 0
// Check if we can add all positions at once
const neededInstances = positions.length
if (!this.canAddMoreInstances(stateId, neededInstances)) {
console.warn(`Cannot add ${neededInstances} instances for block ${blockName} (current: ${currentCount}, max: ${this.maxInstancesPerBlock})`)
continue
}
const mesh = this.instancedMeshes.get(stateId)!
// Add new instances for this section using pre-calculated matrices from worker
for (let i = 0; i < positions.length; i++) {
const instanceIndex = currentCount + instanceIndices.length
mesh.setMatrixAt(instanceIndex, new THREE.Matrix4().fromArray(matrices[i]))
instanceIndices.push(instanceIndex)
}
// Update tracking
if (instanceIndices.length > 0) {
sectionMap.set(stateId, instanceIndices)
const newCount = currentCount + instanceIndices.length
this.blockCounts.set(stateId, newCount)
this.currentTotalInstances += instanceIndices.length
mesh.count = newCount
mesh.instanceMatrix.needsUpdate = true
// Only add mesh to scene when it's first used
if (newCount === instanceIndices.length) {
this.worldRenderer.scene.add(mesh)
}
this.sceneUsedMeshes.set(blockName, mesh)
}
}
}
removeSectionInstances (sectionKey: string) {
const sectionMap = this.sectionInstances.get(sectionKey)
if (!sectionMap) return // Section not tracked
// Remove instances for each block type in this section
for (const [stateId, instanceIndices] of sectionMap) {
this.removeInstancesFromBlock(stateId, instanceIndices)
// Remove from sceneUsedMeshes if no instances left
const blockName = this.stateIdToName.get(stateId)
if (blockName && (this.blockCounts.get(stateId) || 0) === 0) {
this.sceneUsedMeshes.delete(blockName)
}
}
// Remove section from tracking
this.sectionInstances.delete(sectionKey)
}
private removeInstancesFromBlock (stateId: number, indicesToRemove: number[]) {
const mesh = this.instancedMeshes.get(stateId)
if (!mesh || indicesToRemove.length === 0) return
const currentCount = this.blockCounts.get(stateId) || 0
const removeSet = new Set(indicesToRemove)
// Update total instance count
this.currentTotalInstances -= indicesToRemove.length
// Create mapping from old indices to new indices
const indexMapping = new Map<number, number>()
let writeIndex = 0
const tempMatrix = new THREE.Matrix4()
// Compact the instance matrix by removing gaps
for (let readIndex = 0; readIndex < currentCount; readIndex++) {
if (!removeSet.has(readIndex)) {
indexMapping.set(readIndex, writeIndex)
if (writeIndex !== readIndex) {
mesh.getMatrixAt(readIndex, tempMatrix)
mesh.setMatrixAt(writeIndex, tempMatrix)
}
writeIndex++
}
}
// Update count
const newCount = writeIndex
this.blockCounts.set(stateId, newCount)
mesh.count = newCount
mesh.instanceMatrix.needsUpdate = true
// Update all section tracking to reflect new indices
for (const [sectionKey, sectionMap] of this.sectionInstances) {
const sectionIndices = sectionMap.get(stateId)
if (sectionIndices) {
const updatedIndices = sectionIndices
.map(index => indexMapping.get(index))
.filter(index => index !== undefined)
if (updatedIndices.length > 0) {
sectionMap.set(stateId, updatedIndices)
} else {
sectionMap.delete(stateId)
}
}
}
// Update sceneUsedMeshes if no instances left
if (newCount === 0) {
const blockName = this.stateIdToName.get(stateId)
if (blockName) {
this.sceneUsedMeshes.delete(blockName)
}
}
}
disposeOldMeshes () {
// Reset total instance count since we're clearing everything
this.currentTotalInstances = 0
for (const [stateId, mesh] of this.instancedMeshes) {
if (mesh.material instanceof THREE.Material && mesh.material.name.startsWith('instanced_color_')) {
mesh.material.dispose()
}
mesh.geometry.dispose()
this.instancedMeshes.delete(stateId)
this.worldRenderer.scene.remove(mesh)
}
// Clear counts
this.blockCounts.clear()
}
destroy () {
// Clean up resources
for (const [stateId, mesh] of this.instancedMeshes) {
this.worldRenderer.scene.remove(mesh)
mesh.geometry.dispose()
if (mesh.material instanceof THREE.Material) {
mesh.material.dispose()
}
}
// Clean up materials
if (this.sharedSolidMaterial) {
this.sharedSolidMaterial.dispose()
this.sharedSolidMaterial = null
}
for (const material of this.colorMaterials.values()) {
material.dispose()
}
this.colorMaterials.clear()
this.instancedMeshes.clear()
this.blockCounts.clear()
this.sectionInstances.clear()
this.stateIdToName.clear()
this.sceneUsedMeshes.clear()
this.cubeGeometry.dispose()
}
// Add visibility info to stats
getStats () {
let totalInstances = 0
let activeBlockTypes = 0
let totalWastedMemory = 0
for (const [stateId, mesh] of this.instancedMeshes) {
const allocated = mesh.instanceMatrix.count
const used = mesh.count
totalWastedMemory += (allocated - used) * 64 // 64 bytes per instance (approximate)
if (used > 0) {
totalInstances += used
activeBlockTypes++
}
}
const maxPerBlock = this.maxInstancesPerBlock
const renderDistance = this.worldRenderer.viewDistance
return {
totalInstances,
activeBlockTypes,
drawCalls: this._instancedMeshesVisible ? activeBlockTypes : 0,
memoryStats: {
totalAllocatedInstances: this.totalAllocatedInstances,
usedInstances: totalInstances,
wastedInstances: this.totalAllocatedInstances - totalInstances,
estimatedMemoryUsage: this.totalAllocatedInstances * 64,
estimatedWastedMemory: totalWastedMemory,
utilizationPercent: ((totalInstances / this.totalAllocatedInstances) * 100).toFixed(1) + '%'
},
maxInstancesPerBlock: maxPerBlock,
totalInstanceBudget: this.maxTotalInstances,
renderDistance,
instanceUtilization: totalInstances / this.maxTotalInstances,
instancedMeshesVisible: this._instancedMeshesVisible
}
}
// New method to prepare and initialize everything
prepareAndInitialize () {
console.log('Preparing instanced blocks data...')
this.prepareInstancedBlocksData()
const config = this.instancedBlocksConfig!
console.log(`Found ${config.instanceableBlocks.size} instanceable blocks`)
this.disposeOldMeshes()
this.initializeInstancedMeshes()
}
// Method to get the current configuration
getInstancedBlocksConfig (): InstancedBlocksConfig | null {
return this.instancedBlocksConfig
}
}

View file

@ -204,7 +204,7 @@ export class PanoramaRenderer {
}
)
if (this.worldRenderer instanceof WorldRendererThree) {
this.scene = this.worldRenderer.scene
this.scene = this.worldRenderer.realScene
}
void worldView.init(initPos)

View file

@ -565,7 +565,7 @@ export class ThreeJsMedia {
raycaster.setFromCamera(mouse, camera)
// Check intersection with all objects in scene
const intersects = raycaster.intersectObjects(scene.children, true)
const intersects = raycaster.intersectObjects(scene.children.filter(child => child.visible), true)
if (intersects.length > 0) {
const intersection = intersects[0]
const intersectedObject = intersection.object

View file

@ -1,21 +1,17 @@
import * as THREE from 'three'
import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js'
import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils'
import { sectionPos } from '../lib/simpleUtils'
import { WorldRendererCommon } from '../lib/worldrendererCommon'
import { WorldDataEmitterWorker } from '../lib/worldDataEmitter'
import { addNewStat } from '../lib/ui/newStats'
import { MesherGeometryOutput } from '../lib/mesher/shared'
import { MesherGeometryOutput, InstancingMode } from '../lib/mesher/shared'
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
import { getMyHand } from './hand'
import HoldingBlock from './holdingBlock'
import { getMesh } from './entity/EntityMesh'
import { armorModel } from './entity/armorModels'
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
import { loadThreeJsTextureFromBitmap } from './threeJsUtils'
import { CursorBlock } from './world/cursorBlock'
import { getItemUv } from './appShared'
import { Entities } from './entities'
@ -23,19 +19,23 @@ import { ThreeJsSound } from './threeJsSound'
import { CameraShake } from './cameraShake'
import { ThreeJsMedia } from './threeJsMedia'
import { Fountain } from './threeJsParticles'
import { InstancedRenderer } from './instancedRenderer'
import { ChunkMeshManager } from './chunkMeshManager'
type SectionKey = string
export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {}
sectionInstancingMode: Record<string, InstancingMode> = {}
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
signsCache = new Map<string, any>()
starField: StarField
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
holdingBlock: HoldingBlock
holdingBlockLeft: HoldingBlock
scene = new THREE.Scene()
holdingBlock: HoldingBlock | undefined
holdingBlockLeft: HoldingBlock | undefined
realScene = new THREE.Scene()
scene = new THREE.Group()
templateScene = new THREE.Scene()
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
entities = new Entities(this)
@ -47,9 +47,12 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraShake: CameraShake
cameraContainer: THREE.Object3D
media: ThreeJsMedia
instancedRenderer: InstancedRenderer | undefined
chunkMeshManager: ChunkMeshManager
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
camera: THREE.PerspectiveCamera
renderTimeAvg = 0
chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
sectionsOffsetsAnimations = {} as {
[chunkKey: string]: {
time: number,
@ -72,13 +75,14 @@ export class WorldRendererThree extends WorldRendererCommon {
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
private readonly worldOffset = new THREE.Vector3()
get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
return this.chunkMeshManager.getTotalTiles()
}
get blocksRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
return this.chunkMeshManager.getTotalBlocks()
}
constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
@ -94,11 +98,18 @@ export class WorldRendererThree extends WorldRendererCommon {
this.addDebugOverlay()
this.resetScene()
void this.init()
this.soundSystem = new ThreeJsSound(this)
this.cameraShake = new CameraShake(this, this.onRender)
this.media = new ThreeJsMedia(this)
this.instancedRenderer = new InstancedRenderer(this)
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
// Enable bypass pooling for debugging if URL param is present
if (new URLSearchParams(location.search).get('bypassMeshPooling') === 'true') {
this.chunkMeshManager.bypassPooling = true
console.log('ChunkMeshManager: Bypassing pooling for debugging')
}
// this.fountain = new Fountain(this.scene, this.scene, {
// position: new THREE.Vector3(0, 10, 0),
// })
@ -107,6 +118,14 @@ export class WorldRendererThree extends WorldRendererCommon {
this.finishChunk(chunkKey)
})
this.worldSwitchActions()
void this.init()
}
// Add this method to update world origin
private updateWorldOrigin (pos: THREE.Vector3) {
// this.worldOffset.copy(pos)
// this.scene.position.copy(this.worldOffset).multiplyScalar(-1)
}
get cameraObject () {
@ -122,6 +141,15 @@ export class WorldRendererThree extends WorldRendererCommon {
})
}
override connect (worldView: WorldDataEmitterWorker) {
super.connect(worldView)
// Add additional renderDistance handling for mesh pool updates
worldView.on('renderDistance', (viewDistance) => {
this.chunkMeshManager.updateViewDistance(viewDistance)
})
}
updateEntity (e, isPosUpdate = false) {
const overrides = {
rotation: {
@ -143,33 +171,44 @@ export class WorldRendererThree extends WorldRendererCommon {
this.entities.handlePlayerEntity(e)
}
resetTemplateScene () {
this.templateScene = new THREE.Scene()
this.templateScene.add(this.ambientLight.clone())
this.templateScene.add(this.directionalLight.clone())
}
resetScene () {
this.scene.matrixAutoUpdate = false // for perf
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
this.scene.add(this.ambientLight)
this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
this.realScene.add(this.ambientLight)
this.directionalLight.position.set(1, 1, 0.5).normalize()
this.directionalLight.castShadow = true
this.scene.add(this.directionalLight)
this.realScene.add(this.directionalLight)
const size = this.renderer.getSize(new THREE.Vector2())
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
this.cameraContainer = new THREE.Object3D()
this.cameraContainer.add(this.camera)
this.scene.add(this.cameraContainer)
this.realScene.add(this.cameraContainer)
this.realScene.add(this.scene)
this.resetTemplateScene()
}
override watchReactivePlayerState () {
super.watchReactivePlayerState()
this.onReactivePlayerStateUpdated('inWater', (value) => {
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
this.realScene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
})
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
if (!value) return
this.ambientLight.intensity = value
this.resetTemplateScene()
})
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
if (!value) return
this.directionalLight.intensity = value
this.resetTemplateScene()
})
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
@ -185,19 +224,34 @@ export class WorldRendererThree extends WorldRendererCommon {
})
}
getInstancedBlocksData () {
const config = this.instancedRenderer?.getInstancedBlocksConfig()
if (!config) return undefined
return {
instanceableBlocks: config.instanceableBlocks,
}
}
override watchReactiveConfig () {
super.watchReactiveConfig()
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
this.updateShowChunksBorder(value)
this.updateShowChunksBorder()
})
this.onReactiveConfigUpdated('enableDebugOverlay', (value) => {
if (!value) {
// restore visibility
this.chunkMeshManager.updateSectionsVisibility()
}
})
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
if (isAnimationPlaying) {
holdingBlock.startSwing()
holdingBlock?.startSwing()
} else {
holdingBlock.stopSwing()
holdingBlock?.stopSwing()
}
}
@ -224,8 +278,12 @@ export class WorldRendererThree extends WorldRendererCommon {
oldItemsTexture.dispose()
}
// Prepare and initialize instanced renderer with dynamic block detection
this.instancedRenderer?.prepareAndInitialize()
await super.updateAssetsData()
this.onAllTexturesLoaded()
if (Object.keys(this.loadedChunks).length > 0) {
console.log('rerendering chunks because of texture update')
this.rerenderAllChunks()
@ -233,14 +291,18 @@ export class WorldRendererThree extends WorldRendererCommon {
}
onAllTexturesLoaded () {
this.holdingBlock.ready = true
this.holdingBlock.updateItem()
this.holdingBlockLeft.ready = true
this.holdingBlockLeft.updateItem()
if (this.holdingBlock) {
this.holdingBlock.ready = true
this.holdingBlock.updateItem()
}
if (this.holdingBlockLeft) {
this.holdingBlockLeft.ready = true
this.holdingBlockLeft.updateItem()
}
}
changeBackgroundColor (color: [number, number, number]): void {
this.scene.background = new THREE.Color(color[0], color[1], color[2])
this.realScene.background = new THREE.Color(color[0], color[1], color[2])
}
timeUpdated (newTime: number): void {
@ -294,12 +356,20 @@ export class WorldRendererThree extends WorldRendererCommon {
const formatBigNumber = (num: number) => {
return new Intl.NumberFormat('en-US', {}).format(num)
}
const instancedStats = this.instancedRenderer?.getStats()
let text = ''
text += `C: ${formatBigNumber(this.renderer.info.render.calls)} `
text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} `
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
text += `F: ${formatBigNumber(this.tilesRendered)} `
text += `B: ${formatBigNumber(this.blocksRendered)}`
text += `B: ${formatBigNumber(this.blocksRendered)} `
if (instancedStats) {
text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t `
text += `DC: ${formatBigNumber(instancedStats.drawCalls)} `
}
const poolStats = this.chunkMeshManager.getStats()
const poolMode = this.chunkMeshManager.bypassPooling ? 'BYPASS' : poolStats.hitRate
text += `MP: ${poolStats.activeCount}/${poolStats.poolSize} ${poolMode}`
pane.updateText(text)
this.backendInfoReport = text
}
@ -313,8 +383,8 @@ export class WorldRendererThree extends WorldRendererCommon {
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
// sum of distances: x + y + z
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
section.renderOrder = 500 - chunkDistance
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
sectionObject.mesh!.renderOrder = 500 - chunkDistance
}
override updateViewerPosition (pos: Vec3): void {
@ -323,10 +393,22 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraSectionPositionUpdate () {
// eslint-disable-next-line guard-for-in
for (const key in this.sectionObjects) {
const value = this.sectionObjects[key]
if (!value) continue
this.updatePosDataChunk(key)
for (const key in this.sectionInstancingMode) {
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
if (sectionObject) {
this.updatePosDataChunk(key)
}
if (this.worldRendererConfig.dynamicInstancing) {
const [x, y, z] = key.split(',').map(x => +x)
const pos = new Vec3(x, y, z)
const instancingMode = this.getInstancingMode(pos)
if (instancingMode !== this.sectionInstancingMode[key]) {
// console.log('update section', key, this.sectionInstancingMode[key], '->', instancingMode)
// update section
this.setSectionDirty(pos)
}
}
}
}
@ -337,123 +419,68 @@ export class WorldRendererThree extends WorldRendererCommon {
finishChunk (chunkKey: string) {
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
this.sectionObjects[sectionKey].visible = true
const sectionObject = this.chunkMeshManager.getSectionObject(sectionKey)
if (sectionObject) {
sectionObject.visible = true
}
}
delete this.waitingChunksToDisplay[chunkKey]
}
// debugRecomputedDeletedObjects = 0
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
if (data.type !== 'geometry') return
let object: THREE.Object3D = this.sectionObjects[data.key]
if (object) {
this.scene.remove(object)
disposeObject(object)
delete this.sectionObjects[data.key]
}
const chunkCoords = data.key.split(',')
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
const chunkKey = chunkCoords[0] + ',' + chunkCoords[2]
// if (object) {
// this.debugRecomputedDeletedObjects++
// }
const hasInstancedBlocks = data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
geometry.index = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1)
this.instancedRenderer?.removeSectionInstances(data.key)
const mesh = new THREE.Mesh(geometry, this.material)
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
mesh.name = 'mesh'
object = new THREE.Group()
object.add(mesh)
// mesh with static dimensions: 16x16x16
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 }))
staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
boxHelper.name = 'helper'
object.add(boxHelper)
object.name = 'chunk';
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
(object as any).blocksCount = data.geometry.blocksCount
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
boxHelper.visible = false
// Handle instanced blocks data from worker
if (hasInstancedBlocks) {
this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key, this.getInstancingMode(new Vec3(chunkCoords[0], chunkCoords[1], chunkCoords[2])))
}
// should not compute it once
if (Object.keys(data.geometry.signs).length) {
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
const signBlockEntity = this.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
if (!sign) continue
object.add(sign)
}
// Check if chunk should be loaded and has geometry
if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) {
// Release any existing section from the pool
this.chunkMeshManager.releaseSection(data.key)
return
}
if (Object.keys(data.geometry.heads).length) {
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
const headBlockEntity = this.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
if (!head) continue
object.add(head)
}
// Use ChunkMeshManager for optimized mesh handling
const sectionObject = this.chunkMeshManager.updateSection(data.key, data.geometry)
if (!sectionObject) {
return
}
this.sectionObjects[data.key] = object
this.updateBoxHelper(data.key)
// Handle chunk-based rendering
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false
sectionObject.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
this.waitingChunksToDisplay[chunkKey] ??= []
this.waitingChunksToDisplay[chunkKey].push(data.key)
if (this.finishedChunks[chunkKey]) {
// todo it might happen even when it was not an update
this.finishChunk(chunkKey)
}
}
this.updatePosDataChunk(data.key)
object.matrixAutoUpdate = false
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
// mesh.matrixAutoUpdate = false
}
this.scene.add(object)
}
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.version)
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
getCameraPosition () {
const worldPos = new THREE.Vector3()
this.camera.getWorldPosition(worldPos)
return worldPos
// Add world offset to get true world position
return worldPos.add(this.worldOffset)
}
getWorldCameraPosition () {
getSectionCameraPosition () {
const pos = this.getCameraPosition()
return new Vec3(
Math.floor(pos.x / 16),
@ -463,7 +490,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
updateCameraSectionPos () {
const newSectionPos = this.getWorldCameraPosition()
const newSectionPos = this.getSectionCameraPosition()
if (!this.cameraSectionPos.equals(newSectionPos)) {
this.cameraSectionPos = newSectionPos
this.cameraSectionPositionUpdate()
@ -473,6 +500,17 @@ export class WorldRendererThree extends WorldRendererCommon {
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
const yOffset = this.playerStateReactive.eyeHeight
if (pos) {
// Convert Vec3 to THREE.Vector3
const worldPos = new THREE.Vector3(pos.x, pos.y + yOffset, pos.z)
// Update world origin before updating camera
this.updateWorldOrigin(worldPos)
// Keep camera at origin and move world instead
// this.cameraObject.position.set(pos.x, pos.y + yOffset, pos.z)
}
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
this.media.tryIntersectMedia()
this.updateCameraSectionPos()
@ -517,7 +555,7 @@ export class WorldRendererThree extends WorldRendererCommon {
raycaster.far = distance // Limit raycast distance
// Filter to only nearby chunks for performance
const nearbyChunks = Object.values(this.sectionObjects)
const nearbyChunks = Object.values(this.chunkMeshManager.sectionObjects)
.filter(obj => obj.name === 'chunk' && obj.visible)
.filter(obj => {
// Get the mesh child which has the actual geometry
@ -675,7 +713,7 @@ export class WorldRendererThree extends WorldRendererCommon {
chunksRenderBelowOverride !== undefined ||
chunksRenderDistanceOverride !== undefined
) {
for (const [key, object] of Object.entries(this.sectionObjects)) {
for (const [key, object] of Object.entries(this.chunkMeshManager.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
const isVisible =
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
@ -687,10 +725,6 @@ export class WorldRendererThree extends WorldRendererCommon {
object.visible = isVisible
}
} else {
for (const object of Object.values(this.sectionObjects)) {
object.visible = true
}
}
}
@ -716,7 +750,7 @@ export class WorldRendererThree extends WorldRendererCommon {
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
this.renderer.render(this.scene, cam)
this.renderer.render(this.realScene, cam)
if (
this.displayOptions.inWorldRenderingConfig.showHand &&
@ -725,14 +759,15 @@ export class WorldRendererThree extends WorldRendererCommon {
// !this.freeFlyMode &&
!this.renderer.xr.isPresenting
) {
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
this.holdingBlock?.render(this.renderer)
this.holdingBlockLeft?.render(this.renderer)
}
for (const fountain of this.fountains) {
if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) {
fountain.createParticles(this.sectionObjects[fountain.sectionId])
this.sectionObjects[fountain.sectionId].foutain = true
const sectionObject = this.chunkMeshManager.getSectionObject(fountain.sectionId)
if (sectionObject && !sectionObject.fountain) {
fountain.createParticles(sectionObject)
sectionObject.fountain = true
}
fountain.render()
}
@ -742,87 +777,18 @@ export class WorldRendererThree extends WorldRendererCommon {
}
const end = performance.now()
const totalTime = end - start
if (this.worldRendererConfig.autoLowerRenderDistance) {
// Record render time for performance monitoring
this.chunkMeshManager.recordRenderTime(totalTime)
}
this.renderTimeAvgCount++
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
this.currentRenderedFrames++
}
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
if (!textures) return
try {
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
let skinUrl = textureData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererConfig
if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
.replace('https://textures.minecraft.net/', skinTexturesProxy)
}
const mesh = getMesh(this, skinUrl, armorModel.head)
const group = new THREE.Group()
if (isWall) {
mesh.position.set(0, 0.3125, 0.3125)
}
// move head model down as armor have a different offset than blocks
mesh.position.y -= 23 / 16
group.add(mesh)
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.scale.set(0.8, 0.8, 0.8)
return group
} catch (err) {
console.error('Error decoding player texture:', err)
}
}
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
const tex = this.getSignTexture(position, blockEntity, isHanging)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
mesh.renderOrder = 999
const lineHeight = 7 / 16
const scaleFactor = isHanging ? 1.3 : 1
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
const thickness = (isHanging ? 2 : 1.5) / 16
const wallSpacing = 0.25 / 16
if (isWall && !isHanging) {
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
} else {
mesh.position.set(0, 0, thickness / 2 + 0.0001)
}
const group = new THREE.Group()
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.add(mesh)
const height = (isHanging ? 10 : 8) / 16
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
const textPosition = height / 2 + heightOffset
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
return group
}
lightUpdate (chunkX: number, chunkZ: number) {
// set all sections in the chunk dirty
@ -832,26 +798,27 @@ export class WorldRendererThree extends WorldRendererCommon {
}
rerenderAllChunks () { // todo not clear what to do with loading chunks
for (const key of Object.keys(this.sectionObjects)) {
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
this.setSectionDirty(new Vec3(x, y, z))
}
}
updateShowChunksBorder (value: boolean) {
for (const object of Object.values(this.sectionObjects)) {
for (const child of object.children) {
if (child.name === 'helper') {
child.visible = value
}
}
updateShowChunksBorder () {
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
this.updateBoxHelper(key)
}
}
updateBoxHelper (key: string) {
const { showChunkBorders } = this.worldRendererConfig
this.chunkMeshManager.updateBoxHelper(key, showChunkBorders, this.chunkBoxMaterial)
}
resetWorld () {
super.resetWorld()
for (const mesh of Object.values(this.sectionObjects)) {
for (const mesh of Object.values(this.chunkMeshManager.sectionObjects)) {
this.scene.remove(mesh)
}
@ -868,7 +835,7 @@ export class WorldRendererThree extends WorldRendererCommon {
getLoadedChunksRelative (pos: Vec3, includeY = false) {
const [currentX, currentY, currentZ] = sectionPos(pos)
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
return Object.fromEntries(Object.entries(this.chunkMeshManager.sectionObjects).map(([key, o]) => {
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
@ -885,12 +852,13 @@ export class WorldRendererThree extends WorldRendererCommon {
}
readdChunks () {
for (const key of Object.keys(this.sectionObjects)) {
this.scene.remove(this.sectionObjects[key])
const { sectionObjects } = this.chunkMeshManager
for (const key of Object.keys(sectionObjects)) {
this.scene.remove(sectionObjects[key])
}
setTimeout(() => {
for (const key of Object.keys(this.sectionObjects)) {
this.scene.add(this.sectionObjects[key])
for (const key of Object.keys(sectionObjects)) {
this.scene.add(sectionObjects[key])
}
}, 500)
}
@ -902,6 +870,11 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}
removeCurrentChunk () {
const currentChunk = this.cameraSectionPos
this.removeColumn(currentChunk.x * 16, currentChunk.z * 16)
}
removeColumn (x, z) {
super.removeColumn(x, z)
@ -909,19 +882,47 @@ export class WorldRendererThree extends WorldRendererCommon {
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
this.setSectionDirty(new Vec3(x, y, z), false)
const key = `${x},${y},${z}`
const mesh = this.sectionObjects[key]
if (mesh) {
this.scene.remove(mesh)
disposeObject(mesh)
}
delete this.sectionObjects[key]
// Remove instanced blocks for this section
this.instancedRenderer?.removeSectionInstances(key)
// Release section from mesh pool (this will also remove from scene)
this.chunkMeshManager.releaseSection(key)
}
}
setSectionDirty (...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
const [pos] = args
getInstancingMode (pos: Vec3) {
const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly, dynamicInstancing, dynamicInstancingModeDistance, dynamicColorModeDistance } = this.worldRendererConfig
let instancingMode = InstancingMode.None
if (useInstancedRendering || enableSingleColorMode) {
instancingMode = enableSingleColorMode
? InstancingMode.ColorOnly
: forceInstancedOnly
? InstancingMode.BlockInstancingOnly
: InstancingMode.BlockInstancing
} else if (dynamicInstancing) {
const dx = pos.x / 16 - this.cameraSectionPos.x
const dz = pos.z / 16 - this.cameraSectionPos.z
const distance = Math.floor(Math.hypot(dx, dz))
// console.log('distance', distance, `${pos.x},${pos.y},${pos.z}`)
if (distance > dynamicColorModeDistance) {
instancingMode = InstancingMode.ColorOnly
} else if (distance > dynamicInstancingModeDistance) {
instancingMode = InstancingMode.BlockInstancingOnly
}
}
return instancingMode
}
setSectionDirty (pos: Vec3, value = true) {
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
super.setSectionDirty(...args)
const instancingMode = this.getInstancingMode(pos)
super.setSectionDirty(pos, value, undefined, instancingMode)
if (value) {
this.sectionInstancingMode[`${pos.x},${pos.y},${pos.z}`] = instancingMode
}
}
static getRendererInfo (renderer: THREE.WebGLRenderer) {
@ -938,6 +939,8 @@ export class WorldRendererThree extends WorldRendererCommon {
}
destroy (): void {
this.instancedRenderer?.destroy()
this.chunkMeshManager.dispose()
super.destroy()
}
@ -950,7 +953,7 @@ export class WorldRendererThree extends WorldRendererCommon {
const chunkKey = `${chunkX},${chunkZ}`
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
return !!this.finishedChunks[chunkKey] || !!this.chunkMeshManager.sectionObjects[sectionKey]
}
updateSectionOffsets () {
@ -988,7 +991,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
// Apply the offset to the section object
const section = this.sectionObjects[key]
const section = this.chunkMeshManager.sectionObjects[key]
if (section) {
section.position.set(
anim.currentOffsetX,

View file

@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
lockUrl()
}
if (command === 'communication.toggleMicrophone') {
toggleMicrophoneMuted?.()
if (typeof toggleMicrophoneMuted === 'function') {
toggleMicrophoneMuted()
}
}
}

View file

@ -15,6 +15,28 @@ export default () => {
registerMediaChannels()
registerSectionAnimationChannels()
registeredJeiChannel()
sendTokenChannel()
})
}
const sendTokenChannel = () => {
const CHANNEL_NAME = 'minecraft-web-client:token'
bot._client.registerChannel(CHANNEL_NAME, [
'container',
[
{
name: 'token',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'userId',
type: ['pstring', { countType: 'i16' }]
},
]
], true)
bot._client.writeChannel(CHANNEL_NAME, {
token: new URLSearchParams(window.location.search).get('token') ?? '',
userId: new URLSearchParams(window.location.search).get('userId') ?? ''
})
}

View file

@ -43,6 +43,12 @@ export const defaultOptions = {
starfieldRendering: true,
enabledResourcepack: null as string | null,
useVersionsTextures: 'latest',
// Instanced rendering options
useInstancedRendering: false,
autoLowerRenderDistance: false,
forceInstancedOnly: false,
instancedOnlyDistance: 6,
enableSingleColorMode: false,
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
showHand: true,
viewBobbing: true,

View file

@ -6,7 +6,7 @@ import { versionToNumber } from 'mc-assets/dist/utils'
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
import Button from './react/Button'
import { OptionMeta, OptionSlider } from './react/OptionsItems'
import { OptionButton, OptionMeta, OptionSlider } from './react/OptionsItems'
import Slider from './react/Slider'
import { getScreenRefreshRate } from './utils'
import { setLoadingScreenStatus } from './appStatus'
@ -114,6 +114,52 @@ export const guiOptionsScheme: {
text: 'Performance Debug',
}
},
{
custom () {
let status = 'OFF'
const { useInstancedRendering, forceInstancedOnly, enableSingleColorMode } = useSnapshot(options)
if (useInstancedRendering) {
status = 'ON'
if (enableSingleColorMode) {
status = 'ON (single color)'
} else if (forceInstancedOnly) {
status = 'ON (force)'
}
}
return <OptionButton
item={{
type: 'toggle',
text: 'Instacing',
requiresChunksReload: true,
}}
cacheKey='instacing'
valueText={status}
onClick={() => {
// cycle
if (useInstancedRendering) {
if (enableSingleColorMode) {
options.useInstancedRendering = false
options.enableSingleColorMode = false
options.forceInstancedOnly = false
} else if (forceInstancedOnly) {
options.useInstancedRendering = true
options.enableSingleColorMode = true
options.forceInstancedOnly = false
} else {
options.useInstancedRendering = true
options.enableSingleColorMode = false
options.forceInstancedOnly = true
}
} else {
options.useInstancedRendering = true
options.enableSingleColorMode = false
options.forceInstancedOnly = false
}
}}
/>
},
},
{
custom () {
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions

View file

@ -16,7 +16,7 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
const { reactiveDebugParams } = worldRenderer
const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams)
const { rendererPerfDebugOverlay } = useSnapshot(options)
const { rendererPerfDebugOverlay, useInstancedRendering, forceInstancedOnly, instancedOnlyDistance, enableSingleColorMode } = useSnapshot(options)
// Helper to round values to nearest step
const roundToStep = (value: number, step: number) => Math.round(value / step) * step
@ -115,5 +115,36 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
/>
</div> */}
</div>
<div className={styles.column}>
<h3>Instanced Rendering</h3>
<Button
label={useInstancedRendering ? 'Disable Instanced Rendering' : 'Enable Instanced Rendering'}
onClick={() => { options.useInstancedRendering = !options.useInstancedRendering }}
overlayColor={useInstancedRendering ? 'green' : undefined}
/>
<Button
label={forceInstancedOnly ? 'Disable Force Instanced Only' : 'Enable Force Instanced Only'}
onClick={() => { options.forceInstancedOnly = !options.forceInstancedOnly }}
overlayColor={forceInstancedOnly ? 'orange' : undefined}
/>
<Button
label={enableSingleColorMode ? 'Disable Single Color Mode' : 'Enable Single Color Mode'}
onClick={() => { options.enableSingleColorMode = !options.enableSingleColorMode }}
overlayColor={enableSingleColorMode ? 'yellow' : undefined}
/>
<Slider
label="Instanced Distance"
min={1}
max={16}
style={{ width: '100%', }}
value={instancedOnlyDistance}
updateValue={(value) => {
options.instancedOnlyDistance = Math.round(value)
}}
unit=""
valueDisplay={instancedOnlyDistance}
/>
</div>
</div>
}

View file

@ -115,6 +115,18 @@ export const watchOptionsAfterViewerInit = () => {
watchValue(options, o => {
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
})
watchValue(options, o => {
appViewer.inWorldRenderingConfig.autoLowerRenderDistance = o.autoLowerRenderDistance
})
// Instanced rendering options
watchValue(options, o => {
appViewer.inWorldRenderingConfig.useInstancedRendering = o.useInstancedRendering
appViewer.inWorldRenderingConfig.forceInstancedOnly = o.forceInstancedOnly
appViewer.inWorldRenderingConfig.instancedOnlyDistance = o.instancedOnlyDistance
appViewer.inWorldRenderingConfig.enableSingleColorMode = o.enableSingleColorMode
})
}
export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {