From 97f8061b06c42be258327c5f839413c156c162fd Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 25 Jun 2025 17:25:33 +0300 Subject: [PATCH 01/27] feat: Implement instanced rendering mode for low-end devices and performance testing --- renderer/viewer/lib/mesher/models.ts | 248 +++++++++- renderer/viewer/lib/mesher/shared.ts | 20 +- renderer/viewer/lib/worldrendererCommon.ts | 11 +- renderer/viewer/three/instancedRenderer.ts | 494 ++++++++++++++++++++ renderer/viewer/three/worldrendererThree.ts | 31 +- src/optionsStorage.ts | 5 + src/react/RendererDebugMenu.tsx | 33 +- src/watchOptions.ts | 8 + 8 files changed, 842 insertions(+), 8 deletions(-) create mode 100644 renderer/viewer/three/instancedRenderer.ts diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index a69d3e9a..9e928ccb 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -5,8 +5,141 @@ 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, HighestBlockInfo, InstancedBlockEntry } from './shared' +// Hardcoded list of full blocks that can use instancing +export const INSTANCEABLE_BLOCKS = new Set([ + 'grass_block', + 'dirt', + 'stone', + 'cobblestone', + 'mossy_cobblestone', + 'clay', + 'moss_block', + 'spawner', + 'sand', + 'gravel', + 'oak_planks', + 'birch_planks', + 'spruce_planks', + 'jungle_planks', + 'acacia_planks', + 'dark_oak_planks', + 'mangrove_planks', + 'cherry_planks', + 'bamboo_planks', + 'crimson_planks', + 'warped_planks', + 'iron_block', + 'gold_block', + 'diamond_block', + 'emerald_block', + 'netherite_block', + 'coal_block', + 'redstone_block', + 'lapis_block', + 'copper_block', + 'exposed_copper', + 'weathered_copper', + 'oxidized_copper', + 'cut_copper', + 'exposed_cut_copper', + 'weathered_cut_copper', + 'oxidized_cut_copper', + 'waxed_copper_block', + 'waxed_exposed_copper', + 'waxed_weathered_copper', + 'waxed_oxidized_copper', + 'raw_iron_block', + 'raw_copper_block', + 'raw_gold_block', + 'smooth_stone', + 'cobbled_deepslate', + 'deepslate', + 'calcite', + 'tuff', + 'dripstone_block', + 'amethyst_block', + 'budding_amethyst', + 'obsidian', + 'crying_obsidian', + 'bedrock', + 'end_stone', + 'purpur_block', + 'quartz_block', + 'smooth_quartz', + 'nether_bricks', + 'red_nether_bricks', + 'blackstone', + 'gilded_blackstone', + 'polished_blackstone', + 'chiseled_nether_bricks', + 'cracked_nether_bricks', + 'basalt', + 'smooth_basalt', + 'polished_basalt', + 'netherrack', + 'magma_block', + 'soul_sand', + 'soul_soil', + 'ancient_debris', + 'bone_block', + 'packed_ice', + 'blue_ice', + 'ice', + 'snow_block', + 'powder_snow', + 'white_wool', + 'orange_wool', + 'magenta_wool', + 'light_blue_wool', + 'yellow_wool', + 'lime_wool', + 'pink_wool', + 'gray_wool', + 'light_gray_wool', + 'cyan_wool', + 'purple_wool', + 'blue_wool', + 'brown_wool', + 'green_wool', + 'red_wool', + 'black_wool', + 'white_concrete', + 'orange_concrete', + 'magenta_concrete', + 'light_blue_concrete', + 'yellow_concrete', + 'lime_concrete', + 'pink_concrete', + 'gray_concrete', + 'light_gray_concrete', + 'cyan_concrete', + 'purple_concrete', + 'blue_concrete', + 'brown_concrete', + 'green_concrete', + 'red_concrete', + 'black_concrete', + 'white_terracotta', + 'orange_terracotta', + 'magenta_terracotta', + 'light_blue_terracotta', + 'yellow_terracotta', + 'lime_terracotta', + 'pink_terracotta', + 'gray_terracotta', + 'light_gray_terracotta', + 'cyan_terracotta', + 'purple_terracotta', + 'blue_terracotta', + 'brown_terracotta', + 'green_terracotta', + 'red_terracotta', + 'black_terracotta', + 'terracotta', + 'glazed_terracotta', +]) let blockProvider: WorldBlockProvider @@ -518,10 +651,87 @@ 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 => { + // Check if all 6 faces would be culled (hidden by neighbors) + const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice') + + // eslint-disable-next-line guard-for-in + for (const face in elemFaces) { + const { dir } = elemFaces[face] + const neighbor = world.getBlock(cursor.plus(new Vec3(dir[0], dir[1], dir[2])), blockProvider, {}) + + if (!neighbor) { + // Face is exposed to air/void + return false + } + + if (cullIfIdentical && neighbor.stateId === block.stateId) { + // Same block type, face should be culled + continue + } + + if (!neighbor.transparent && isCube(neighbor)) { + // Neighbor is opaque and full cube, face should be culled + continue + } + + // Face is not culled, block should be rendered + return false + } + + // All faces are culled, block should not be rendered + return true +} + +const getInstancedBlockTextureInfo = (block: Block) => { + // Get texture info from the first available face + const model = block.models?.[0]?.[0] + if (!model?.elements?.[0]) return undefined + + const element = model.elements[0] + + // Try to find a face with texture - prefer visible faces + const faceOrder = ['up', 'north', 'east', 'south', 'west', 'down'] + for (const faceName of faceOrder) { + const face = element.faces[faceName] + if (face?.texture) { + const texture = face.texture as any + return { + u: texture.u || 0, + v: texture.v || 0, + su: texture.su || 1, + sv: texture.sv || 1 + } + } + } + + return undefined +} + +const isBlockInstanceable = (block: Block): boolean => { + // Only instanceable if it's a full cube block without complex geometry + if (!INSTANCEABLE_BLOCKS.has(block.name)) return false + + // Check if it's actually a full cube (no rotations, no complex models) + if (!block.models || block.models.length !== 1) return false + + const model = block.models[0][0] // First variant of first model + if (!model || model.x || model.y || model.z) return false // No rotations + + // Check if all elements are full cubes + return (model.elements ?? []).every(element => { + return element.from[0] === 0 && element.from[1] === 0 && element.from[2] === 0 && + element.to[0] === 16 && element.to[1] === 16 && element.to[2] === 16 + }) +} + let unknownBlockModel: BlockModelPartsResolved export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) { let delayedRender = [] as Array<() => void> + // Check if instanced rendering is enabled + const enableInstancedRendering = world.config.useInstancedRendering + const attr: MesherGeometryOutput = { sx: sx + 8, sy: sy + 8, @@ -544,7 +754,8 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W // isFull: true, highestBlocks: new Map(), hadErrors: false, - blocksCount: 0 + blocksCount: 0, + instancedBlocks: {} } const cursor = new Vec3(0, 0, 0) @@ -621,6 +832,39 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W 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(block)) { + // Check if block should be culled (all faces hidden by neighbors) + if (shouldCullInstancedBlock(world, cursor, block)) { + // Block is completely surrounded, skip rendering + continue + } + + const blockKey = block.name + if (!attr.instancedBlocks[blockKey]) { + const textureInfo = getInstancedBlockTextureInfo(block) + attr.instancedBlocks[blockKey] = { + blockName: block.name, + stateId: block.stateId, + textureInfo, + positions: [] + } + } + attr.instancedBlocks[blockKey].positions.push({ + x: cursor.x, + y: cursor.y, + z: cursor.z + }) + attr.blocksCount++ + continue // Skip regular geometry generation for instanceable blocks + } + + // Skip buffer geometry generation if force instanced only mode is enabled + if (world.config.forceInstancedOnly) { + // In force instanced only mode, skip all non-instanceable blocks + continue + } + // cache let { models } = block diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 53e0c534..debf27a6 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -12,7 +12,11 @@ export const defaultMesherConfig = { // textureSize: 1024, // for testing debugModelVariant: undefined as undefined | number[], clipWorldBelowY: undefined as undefined | number, - disableSignsMapsSupport: false + disableSignsMapsSupport: false, + // Instanced rendering options + useInstancedRendering: false, + forceInstancedOnly: false, + enableSingleColorMode: false, } export type CustomBlockModels = { @@ -21,6 +25,18 @@ export type CustomBlockModels = { export type MesherConfig = typeof defaultMesherConfig +export type InstancedBlockEntry = { + blockName: string + stateId: number + textureInfo?: { + u: number + v: number + su: number + sv: number + } + positions: Array<{ x: number, y: number, z: number }> +} + export type MesherGeometryOutput = { sx: number, sy: number, @@ -46,6 +62,8 @@ export type MesherGeometryOutput = { hadErrors: boolean blocksCount: number customBlockModels?: CustomBlockModels + // New instanced blocks data + instancedBlocks: Record } export interface MesherMainEvents { diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index dfa4f43b..372d8211 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -55,7 +55,12 @@ export const defaultWorldRendererConfig = { enableDebugOverlay: false, _experimentalSmoothChunkLoading: true, _renderByChunks: false, - volume: 1 + volume: 1, + // New instancing options + useInstancedRendering: false, + forceInstancedOnly: false, + instancedOnlyDistance: 6, // chunks beyond this distance use instancing only + enableSingleColorMode: false, // ultra-performance mode with solid colors } export type WorldRendererConfig = typeof defaultWorldRendererConfig @@ -571,6 +576,10 @@ export abstract class WorldRendererCommon disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, worldMinY: this.worldMinYRender, worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight, + // Instanced rendering options + useInstancedRendering: this.worldRendererConfig.useInstancedRendering, + forceInstancedOnly: this.worldRendererConfig.forceInstancedOnly, + enableSingleColorMode: this.worldRendererConfig.enableSingleColorMode, } } diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts new file mode 100644 index 00000000..50ecb1c3 --- /dev/null +++ b/renderer/viewer/three/instancedRenderer.ts @@ -0,0 +1,494 @@ +import * as THREE from 'three' +import { Vec3 } from 'vec3' +import { WorldRendererThree } from './worldrendererThree' + +// Hardcoded list of full blocks that can use instancing +export const INSTANCEABLE_BLOCKS = new Set([ + 'grass_block', + 'dirt', + 'stone', + 'cobblestone', + 'mossy_cobblestone', + 'clay', + 'moss_block', + 'spawner', + 'sand', + 'gravel', + 'oak_planks', + 'birch_planks', + 'spruce_planks', + 'jungle_planks', + 'acacia_planks', + 'dark_oak_planks', + 'mangrove_planks', + 'cherry_planks', + 'bamboo_planks', + 'crimson_planks', + 'warped_planks', + 'iron_block', + 'gold_block', + 'diamond_block', + 'emerald_block', + 'netherite_block', + 'coal_block', + 'redstone_block', + 'lapis_block', + 'copper_block', + 'exposed_copper', + 'weathered_copper', + 'oxidized_copper', + 'cut_copper', + 'exposed_cut_copper', + 'weathered_cut_copper', + 'oxidized_cut_copper', + 'waxed_copper_block', + 'waxed_exposed_copper', + 'waxed_weathered_copper', + 'waxed_oxidized_copper', + 'raw_iron_block', + 'raw_copper_block', + 'raw_gold_block', + 'smooth_stone', + 'cobbled_deepslate', + 'deepslate', + 'calcite', + 'tuff', + 'dripstone_block', + 'amethyst_block', + 'budding_amethyst', + 'obsidian', + 'crying_obsidian', + 'bedrock', + 'end_stone', + 'purpur_block', + 'quartz_block', + 'smooth_quartz', + 'nether_bricks', + 'red_nether_bricks', + 'blackstone', + 'gilded_blackstone', + 'polished_blackstone', + 'chiseled_nether_bricks', + 'cracked_nether_bricks', + 'basalt', + 'smooth_basalt', + 'polished_basalt', + 'netherrack', + 'magma_block', + 'soul_sand', + 'soul_soil', + 'ancient_debris', + 'bone_block', + 'packed_ice', + 'blue_ice', + 'ice', + 'snow_block', + 'powder_snow', + 'white_wool', + 'orange_wool', + 'magenta_wool', + 'light_blue_wool', + 'yellow_wool', + 'lime_wool', + 'pink_wool', + 'gray_wool', + 'light_gray_wool', + 'cyan_wool', + 'purple_wool', + 'blue_wool', + 'brown_wool', + 'green_wool', + 'red_wool', + 'black_wool', + 'white_concrete', + 'orange_concrete', + 'magenta_concrete', + 'light_blue_concrete', + 'yellow_concrete', + 'lime_concrete', + 'pink_concrete', + 'gray_concrete', + 'light_gray_concrete', + 'cyan_concrete', + 'purple_concrete', + 'blue_concrete', + 'brown_concrete', + 'green_concrete', + 'red_concrete', + 'black_concrete', + 'white_terracotta', + 'orange_terracotta', + 'magenta_terracotta', + 'light_blue_terracotta', + 'yellow_terracotta', + 'lime_terracotta', + 'pink_terracotta', + 'gray_terracotta', + 'light_gray_terracotta', + 'cyan_terracotta', + 'purple_terracotta', + 'blue_terracotta', + 'brown_terracotta', + 'green_terracotta', + 'red_terracotta', + 'black_terracotta', + 'terracotta', + 'glazed_terracotta', +]) + +export interface InstancedBlockData { + positions: Vec3[] + blockName: string + stateId: number +} + +export interface InstancedSectionData { + sectionKey: string + instancedBlocks: Map + shouldUseInstancedOnly: boolean +} + +export class InstancedRenderer { + private readonly instancedMeshes = new Map() + private readonly blockCounts = new Map() + private readonly sectionInstances = new Map>() // sectionKey -> blockName -> instanceIndices + private readonly maxInstancesPerBlock = 100_000 // Reasonable limit + private readonly cubeGeometry: THREE.BoxGeometry + private readonly tempMatrix = new THREE.Matrix4() + + constructor (private readonly worldRenderer: WorldRendererThree) { + this.cubeGeometry = this.createCubeGeometry() + this.initInstancedMeshes() + } + + 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 (textureInfo: { u: number, v: number, su: number, sv: number }): THREE.BufferGeometry { + // Create custom geometry with specific UV coordinates for this block type + // This would be more complex but would give perfect texture mapping + 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 + + // Modify UV coordinates to use the specific texture region + // This is a simplified version - real implementation would need to handle all 6 faces properly + for (let i = 0; i < uvs.length; i += 2) { + const u = uvs[i] + const v = uvs[i + 1] + + // Map from 0-1 to the specific texture region + uvs[i] = textureInfo.u + u * textureInfo.su + uvs[i + 1] = textureInfo.v + v * textureInfo.sv + } + + uvAttribute.needsUpdate = true + return geometry + } + + private initInstancedMeshes () { + // Create InstancedMesh for each instanceable block type + for (const blockName of INSTANCEABLE_BLOCKS) { + const material = this.createBlockMaterial(blockName) + const mesh = new THREE.InstancedMesh( + this.cubeGeometry, + material, + this.maxInstancesPerBlock + ) + mesh.name = `instanced_${blockName}` + mesh.frustumCulled = false // Important for performance + mesh.count = 0 // Start with 0 instances + + this.instancedMeshes.set(blockName, mesh) + this.worldRenderer.scene.add(mesh) + } + } + + private createBlockMaterial (blockName: string, textureInfo?: { u: number, v: number, su: number, sv: number }): THREE.Material { + const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig + + if (enableSingleColorMode) { + // Ultra-performance mode: solid colors only + const color = this.getBlockColor(blockName) + return new THREE.MeshBasicMaterial({ color }) + } else { + // Use texture from the blocks atlas + if (this.worldRenderer.material.map) { + const material = this.worldRenderer.material.clone() + // The texture is already the correct blocks atlas texture + // Individual block textures are handled by UV coordinates in the geometry + return material + } else { + // Fallback to colored material + const color = this.getBlockColor(blockName) + return new THREE.MeshLambertMaterial({ color }) + } + } + } + + private getBlockColor (blockName: string): number { + // Simple color mapping for blocks + const colorMap: Record = { + 'grass_block': 0x7c_bd_3b, + 'dirt': 0x8b_45_13, + 'stone': 0x7d_7d_7d, + 'cobblestone': 0x7a_7a_7a, + 'mossy_cobblestone': 0x65_97_65, + 'clay': 0xa0_a5_a6, + 'moss_block': 0x58_7d_3c, + 'spawner': 0x1a_1a_1a, + 'sand': 0xf7_e9_a3, + 'gravel': 0x8a_8a_8a, + 'iron_block': 0xd8_d8_d8, + 'gold_block': 0xfa_ee_4d, + 'diamond_block': 0x5c_db_d5, + 'emerald_block': 0x17_dd_62, + 'coal_block': 0x34_34_34, + 'redstone_block': 0xa1_27_22, + 'lapis_block': 0x22_4d_bb, + 'netherrack': 0x72_3a_32, + 'obsidian': 0x0f_0f_23, + 'bedrock': 0x56_56_56, + 'end_stone': 0xda_dc_a6, + 'quartz_block': 0xec_e4_d6, + 'snow_block': 0xff_ff_ff, + 'ice': 0x9c_f4_ff, + 'packed_ice': 0x74_c7_ec, + 'blue_ice': 0x3e_5f_bc, + } + return colorMap[blockName] || 0x99_99_99 // Default gray + } + + handleInstancedGeometry (data: InstancedSectionData) { + const { sectionKey, instancedBlocks } = data + + // Clear previous instances for this section + this.clearSectionInstances(sectionKey) + + // Add new instances + for (const [blockName, blockData] of instancedBlocks) { + if (!INSTANCEABLE_BLOCKS.has(blockName)) continue + + const mesh = this.instancedMeshes.get(blockName) + if (!mesh) continue + + const currentCount = this.blockCounts.get(blockName) || 0 + let instanceIndex = currentCount + + for (const pos of blockData.positions) { + if (instanceIndex >= this.maxInstancesPerBlock) { + console.warn(`Exceeded max instances for block ${blockName} (${instanceIndex}/${this.maxInstancesPerBlock})`) + break + } + + this.tempMatrix.setPosition(pos.x, pos.y, pos.z) + mesh.setMatrixAt(instanceIndex, this.tempMatrix) + instanceIndex++ + } + + mesh.count = instanceIndex + mesh.instanceMatrix.needsUpdate = true + this.blockCounts.set(blockName, instanceIndex) + } + } + + handleInstancedBlocksFromWorker (instancedBlocks: Record, sectionKey: string) { + // Clear existing instances for this section first + this.removeSectionInstances(sectionKey) + + // Initialize section tracking if not exists + if (!this.sectionInstances.has(sectionKey)) { + this.sectionInstances.set(sectionKey, new Map()) + } + const sectionMap = this.sectionInstances.get(sectionKey)! + + for (const [blockName, blockData] of Object.entries(instancedBlocks)) { + if (!this.isBlockInstanceable(blockName)) continue + + let mesh = this.instancedMeshes.get(blockName) + if (!mesh) { + // Create new mesh if it doesn't exist + const material = this.createBlockMaterial(blockName, blockData.textureInfo) + mesh = new THREE.InstancedMesh( + this.cubeGeometry, + material, + this.maxInstancesPerBlock + ) + mesh.name = `instanced_${blockName}` + mesh.frustumCulled = false + mesh.count = 0 + this.instancedMeshes.set(blockName, mesh) + this.worldRenderer.scene.add(mesh) + } + + const instanceIndices: number[] = [] + const currentCount = this.blockCounts.get(blockName) || 0 + + // Add new instances for this section + for (const pos of blockData.positions) { + if (currentCount + instanceIndices.length >= this.maxInstancesPerBlock) { + console.warn(`Exceeded max instances for block ${blockName} (${currentCount + instanceIndices.length}/${this.maxInstancesPerBlock})`) + break + } + + const instanceIndex = currentCount + instanceIndices.length + this.tempMatrix.setPosition(pos.x, pos.y, pos.z) + mesh.setMatrixAt(instanceIndex, this.tempMatrix) + instanceIndices.push(instanceIndex) + } + + // Update tracking + if (instanceIndices.length > 0) { + sectionMap.set(blockName, instanceIndices) + this.blockCounts.set(blockName, currentCount + instanceIndices.length) + mesh.count = this.blockCounts.get(blockName) || 0 + mesh.instanceMatrix.needsUpdate = true + } + } + } + + private clearSectionInstances (sectionKey: string) { + // For now, we'll rebuild all instances each time + // This could be optimized to track instances per section + for (const [blockName, mesh] of this.instancedMeshes) { + mesh.count = 0 + mesh.instanceMatrix.needsUpdate = true + this.blockCounts.set(blockName, 0) + } + } + + 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 [blockName, instanceIndices] of sectionMap) { + const mesh = this.instancedMeshes.get(blockName) + if (!mesh) continue + + // For efficiency, we'll rebuild the entire instance array by compacting it + // This removes gaps left by deleted instances + this.compactInstancesForBlock(blockName, instanceIndices) + } + + // Remove section from tracking + this.sectionInstances.delete(sectionKey) + } + + private compactInstancesForBlock (blockName: string, indicesToRemove: number[]) { + const mesh = this.instancedMeshes.get(blockName) + if (!mesh) return + + const currentCount = this.blockCounts.get(blockName) || 0 + const removeSet = new Set(indicesToRemove) + + 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)) { + if (writeIndex !== readIndex) { + mesh.getMatrixAt(readIndex, tempMatrix) + mesh.setMatrixAt(writeIndex, tempMatrix) + } + writeIndex++ + } + } + + // Update count and indices in section tracking + const newCount = writeIndex + this.blockCounts.set(blockName, newCount) + mesh.count = newCount + mesh.instanceMatrix.needsUpdate = true + + // Update all section tracking to reflect compacted indices + const offset = 0 + for (const [sectionKey, sectionMap] of this.sectionInstances) { + const sectionIndices = sectionMap.get(blockName) + if (sectionIndices) { + const compactedIndices = sectionIndices + .filter(index => !removeSet.has(index)) + .map(index => index - removeSet.size + offset) + + if (compactedIndices.length > 0) { + sectionMap.set(blockName, compactedIndices) + } else { + sectionMap.delete(blockName) + } + } + } + } + + shouldUseInstancedRendering (chunkKey: string): boolean { + const { useInstancedRendering, forceInstancedOnly, instancedOnlyDistance } = this.worldRenderer.worldRendererConfig + + if (!useInstancedRendering) return false + if (forceInstancedOnly) return true + + // Check distance for automatic switching + const [x, z] = chunkKey.split(',').map(Number) + const chunkPos = new Vec3(x * 16, 0, z * 16) + const [dx, dz] = this.worldRenderer.getDistance(chunkPos) + const maxDistance = Math.max(dx, dz) + + return maxDistance >= instancedOnlyDistance + } + + isBlockInstanceable (blockName: string): boolean { + return INSTANCEABLE_BLOCKS.has(blockName) + } + + updateMaterials () { + // Update materials when texture atlas changes + for (const [blockName, mesh] of this.instancedMeshes) { + const newMaterial = this.createBlockMaterial(blockName) + const oldMaterial = mesh.material + mesh.material = newMaterial + if (oldMaterial instanceof THREE.Material) { + oldMaterial.dispose() + } + } + } + + destroy () { + // Clean up resources + for (const [blockName, mesh] of this.instancedMeshes) { + this.worldRenderer.scene.remove(mesh) + mesh.geometry.dispose() + if (mesh.material instanceof THREE.Material) { + mesh.material.dispose() + } + } + this.instancedMeshes.clear() + this.blockCounts.clear() + this.sectionInstances.clear() + this.cubeGeometry.dispose() + } + + getStats () { + let totalInstances = 0 + let activeBlockTypes = 0 + + for (const [blockName, mesh] of this.instancedMeshes) { + if (mesh.count > 0) { + totalInstances += mesh.count + activeBlockTypes++ + } + } + + return { + totalInstances, + activeBlockTypes, + drawCalls: activeBlockTypes, // One draw call per active block type + memoryEstimate: totalInstances * 64 // Rough estimate in bytes + } + } +} diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index b4ae4961..552aa2c1 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -24,6 +24,7 @@ import { ThreeJsSound } from './threeJsSound' import { CameraShake } from './cameraShake' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' +import { InstancedRenderer, INSTANCEABLE_BLOCKS } from './instancedRenderer' type SectionKey = string @@ -48,6 +49,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraShake: CameraShake cameraContainer: THREE.Object3D media: ThreeJsMedia + instancedRenderer: InstancedRenderer waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } camera: THREE.PerspectiveCamera renderTimeAvg = 0 @@ -100,6 +102,7 @@ export class WorldRendererThree extends WorldRendererCommon { this.soundSystem = new ThreeJsSound(this) this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) + this.instancedRenderer = new InstancedRenderer(this) // this.fountain = new Fountain(this.scene, this.scene, { // position: new THREE.Vector3(0, 10, 0), // }) @@ -227,6 +230,8 @@ export class WorldRendererThree extends WorldRendererCommon { await super.updateAssetsData() this.onAllTexturesLoaded() + // Update instanced renderer materials when textures change + this.instancedRenderer.updateMaterials() if (Object.keys(this.loadedChunks).length > 0) { console.log('rerendering chunks because of texture update') this.rerenderAllChunks() @@ -295,12 +300,15 @@ 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)} ` + text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t ` + text += `DC: ${formatBigNumber(instancedStats.drawCalls)}` pane.updateText(text) this.backendInfoReport = text } @@ -343,8 +351,21 @@ export class WorldRendererThree extends WorldRendererCommon { } // debugRecomputedDeletedObjects = 0 + handleInstancedBlocks (instancedBlocks: Record, sectionKey: string): void { + this.instancedRenderer.handleInstancedBlocksFromWorker(instancedBlocks, sectionKey) + } + handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void { if (data.type !== 'geometry') return + + const chunkCoords = data.key.split(',') + const chunkKey = chunkCoords[0] + ',' + chunkCoords[2] + + // Handle instanced blocks data from worker + if (data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0) { + this.handleInstancedBlocks(data.geometry.instancedBlocks, data.key) + } + let object: THREE.Object3D = this.sectionObjects[data.key] if (object) { this.scene.remove(object) @@ -352,8 +373,7 @@ export class WorldRendererThree extends WorldRendererCommon { delete this.sectionObjects[data.key] } - const chunkCoords = data.key.split(',') - if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return + if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) return // if (object) { // this.debugRecomputedDeletedObjects++ @@ -878,6 +898,10 @@ 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}` + + // Remove instanced blocks for this section + this.instancedRenderer.removeSectionInstances(key) + const mesh = this.sectionObjects[key] if (mesh) { this.scene.remove(mesh) @@ -907,6 +931,7 @@ export class WorldRendererThree extends WorldRendererCommon { } destroy (): void { + this.instancedRenderer.destroy() super.destroy() } diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 882610f8..b61cb1b6 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -52,6 +52,11 @@ const defaultOptions = { starfieldRendering: true, enabledResourcepack: null as string | null, useVersionsTextures: 'latest', + // Instanced rendering options + useInstancedRendering: false, + forceInstancedOnly: false, + instancedOnlyDistance: 6, + enableSingleColorMode: false, serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', showHand: true, viewBobbing: true, diff --git a/src/react/RendererDebugMenu.tsx b/src/react/RendererDebugMenu.tsx index f4bf7876..b116bca0 100644 --- a/src/react/RendererDebugMenu.tsx +++ b/src/react/RendererDebugMenu.tsx @@ -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 /> */} + +
+

Instanced Rendering

+
} diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 478da4fb..8d1e46b9 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -115,6 +115,14 @@ export const watchOptionsAfterViewerInit = () => { watchValue(options, o => { // appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates }) + + // 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) => { From 4aebfecf69cde48a76e331b9ac33791ebb985cfe Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 29 Jun 2025 15:22:21 +0300 Subject: [PATCH 02/27] some progress --- renderer/viewer/lib/mesher/mesher.ts | 18 +-- renderer/viewer/lib/mesher/models.ts | 67 +++++---- renderer/viewer/lib/mesher/shared.ts | 17 ++- renderer/viewer/lib/mesher/world.ts | 8 +- renderer/viewer/lib/worldrendererCommon.ts | 13 +- renderer/viewer/three/instancedRenderer.ts | 143 +++++++++++--------- renderer/viewer/three/worldrendererThree.ts | 14 +- 7 files changed, 167 insertions(+), 113 deletions(-) diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index a063d77f..444093f2 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -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() +let dirtySections = new Map() 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 }) } @@ -98,12 +98,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 || new Map() 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 } @@ -197,13 +199,13 @@ 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 geometry = getSectionGeometry(x, y, z, world, instancingMode) 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) @@ -213,7 +215,7 @@ setInterval(() => { } 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 } diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index 9e928ccb..f83f8c25 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -5,7 +5,7 @@ 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, InstancedBlockEntry } from './shared' +import { MesherGeometryOutput, HighestBlockInfo, InstancedBlockEntry, InstancingMode } from './shared' // Hardcoded list of full blocks that can use instancing export const INSTANCEABLE_BLOCKS = new Set([ @@ -146,6 +146,9 @@ let blockProvider: WorldBlockProvider const tints: any = {} let needTiles = false +// Cache for texture info to avoid repeated calculations +const textureInfoCache = new Map() + let tintsData try { tintsData = require('esbuild-data').tints @@ -652,31 +655,29 @@ const isBlockWaterlogged = (block: Block) => { } 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] - const neighbor = world.getBlock(cursor.plus(new Vec3(dir[0], dir[1], dir[2])), blockProvider, {}) + offsetCursor.set(cursor.x + dir[0], cursor.y + dir[1], cursor.z + dir[2]) + const neighbor = world.getBlock(offsetCursor, blockProvider, {}) - if (!neighbor) { - // Face is exposed to air/void - return false - } + // Face is exposed to air/void - block must be rendered + if (!neighbor) return false - if (cullIfIdentical && neighbor.stateId === block.stateId) { - // Same block type, face should be culled - continue - } + // Handle special case for identical blocks (glass/ice) + if (cullIfIdentical && neighbor.stateId === block.stateId) continue - if (!neighbor.transparent && isCube(neighbor)) { - // Neighbor is opaque and full cube, face should be culled - continue - } - - // Face is not culled, block should be rendered - return false + // 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 @@ -684,6 +685,12 @@ const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boo } const getInstancedBlockTextureInfo = (block: Block) => { + // Cache texture info by block state ID + const cacheKey = block.stateId + if (textureInfoCache.has(cacheKey)) { + return textureInfoCache.get(cacheKey) + } + // Get texture info from the first available face const model = block.models?.[0]?.[0] if (!model?.elements?.[0]) return undefined @@ -692,20 +699,25 @@ const getInstancedBlockTextureInfo = (block: Block) => { // Try to find a face with texture - prefer visible faces const faceOrder = ['up', 'north', 'east', 'south', 'west', 'down'] + let textureInfo: any + for (const faceName of faceOrder) { const face = element.faces[faceName] if (face?.texture) { const texture = face.texture as any - return { + textureInfo = { u: texture.u || 0, v: texture.v || 0, su: texture.su || 1, sv: texture.sv || 1 } + break } } - return undefined + // Cache the result + textureInfoCache.set(cacheKey, textureInfo) + return textureInfo } const isBlockInstanceable = (block: Block): boolean => { @@ -726,11 +738,12 @@ const isBlockInstanceable = (block: Block): boolean => { } 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 - const enableInstancedRendering = world.config.useInstancedRendering + // Check if instanced rendering is enabled for this section + const enableInstancedRendering = instancingMode !== InstancingMode.None + const forceInstancedOnly = instancingMode === InstancingMode.TexturedInstancing // Only force when using full instancing const attr: MesherGeometryOutput = { sx: sx + 8, @@ -843,7 +856,15 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W const blockKey = block.name if (!attr.instancedBlocks[blockKey]) { const textureInfo = getInstancedBlockTextureInfo(block) + // Get or create block ID + let blockId = world.instancedBlockIds.get(block.name) + if (blockId === undefined) { + blockId = world.instancedBlockIds.size + world.instancedBlockIds.set(block.name, blockId) + } + attr.instancedBlocks[blockKey] = { + blockId, blockName: block.name, stateId: block.stateId, textureInfo, @@ -860,7 +881,7 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W } // Skip buffer geometry generation if force instanced only mode is enabled - if (world.config.forceInstancedOnly) { + if (forceInstancedOnly) { // In force instanced only mode, skip all non-instanceable blocks continue } diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index debf27a6..b2881e8d 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -1,5 +1,11 @@ import { BlockType } from '../../../playground/shared' +export enum InstancingMode { + None = 'none', + ColorOnly = 'color_only', + TexturedInstancing = 'textured_instancing' +} + // only here for easier testing export const defaultMesherConfig = { version: '', @@ -13,10 +19,6 @@ export const defaultMesherConfig = { debugModelVariant: undefined as undefined | number[], clipWorldBelowY: undefined as undefined | number, disableSignsMapsSupport: false, - // Instanced rendering options - useInstancedRendering: false, - forceInstancedOnly: false, - enableSingleColorMode: false, } export type CustomBlockModels = { @@ -26,6 +28,7 @@ export type CustomBlockModels = { export type MesherConfig = typeof defaultMesherConfig export type InstancedBlockEntry = { + blockId: number // Unique ID for this block type blockName: string stateId: number textureInfo?: { @@ -37,6 +40,12 @@ export type InstancedBlockEntry = { positions: Array<{ x: number, y: number, z: number }> } +export type InstancingMesherData = { + blocks: { + [stateId: number]: number // instance id + } +} + export type MesherGeometryOutput = { sx: number, sy: number, diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts index f2757ae6..110fb93c 100644 --- a/renderer/viewer/lib/mesher/world.ts +++ b/renderer/viewer/lib/mesher/world.ts @@ -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() // chunkKey -> blockModels sentBlockStateModels = new Set() blockStateModelInfo = new Map() + instancedBlocks: Record = {} + instancedBlockIds = new Map() - 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 = '') { diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 372d8211..30761aaf 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -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' @@ -576,10 +576,6 @@ export abstract class WorldRendererCommon disableSignsMapsSupport: !this.worldRendererConfig.extraBlockRenderers, worldMinY: this.worldMinYRender, worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight, - // Instanced rendering options - useInstancedRendering: this.worldRendererConfig.useInstancedRendering, - forceInstancedOnly: this.worldRendererConfig.forceInstancedOnly, - enableSingleColorMode: this.worldRendererConfig.enableSingleColorMode, } } @@ -927,7 +923,7 @@ export abstract class WorldRendererCommon 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') @@ -941,7 +937,7 @@ export abstract class WorldRendererCommon // 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({ @@ -950,17 +946,18 @@ export abstract class WorldRendererCommon 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() diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 50ecb1c3..105cd3bb 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -1,5 +1,6 @@ import * as THREE from 'three' import { Vec3 } from 'vec3' +import moreBlockData from '../lib/moreBlockDataGenerated.json' import { WorldRendererThree } from './worldrendererThree' // Hardcoded list of full blocks that can use instancing @@ -136,7 +137,20 @@ export const INSTANCEABLE_BLOCKS = new Set([ 'glazed_terracotta', ]) +// 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 { + blockId: number positions: Vec3[] blockName: string stateId: number @@ -149,12 +163,15 @@ export interface InstancedSectionData { } export class InstancedRenderer { - private readonly instancedMeshes = new Map() - private readonly blockCounts = new Map() - private readonly sectionInstances = new Map>() // sectionKey -> blockName -> instanceIndices - private readonly maxInstancesPerBlock = 100_000 // Reasonable limit + private readonly instancedMeshes = new Map() + private readonly blockCounts = new Map() + private readonly sectionInstances = new Map>() + private readonly maxInstancesPerBlock = 100_000 private readonly cubeGeometry: THREE.BoxGeometry private readonly tempMatrix = new THREE.Matrix4() + private readonly blockIdToName = new Map() + private readonly blockNameToId = new Map() + private nextBlockId = 0 constructor (private readonly worldRenderer: WorldRendererThree) { this.cubeGeometry = this.createCubeGeometry() @@ -206,7 +223,7 @@ export class InstancedRenderer { mesh.frustumCulled = false // Important for performance mesh.count = 0 // Start with 0 instances - this.instancedMeshes.set(blockName, mesh) + this.instancedMeshes.set(this.getBlockId(blockName), mesh) this.worldRenderer.scene.add(mesh) } } @@ -220,6 +237,7 @@ export class InstancedRenderer { return new THREE.MeshBasicMaterial({ color }) } else { // Use texture from the blocks atlas + // eslint-disable-next-line no-lonely-if if (this.worldRenderer.material.map) { const material = this.worldRenderer.material.clone() // The texture is already the correct blocks atlas texture @@ -234,36 +252,14 @@ export class InstancedRenderer { } private getBlockColor (blockName: string): number { - // Simple color mapping for blocks - const colorMap: Record = { - 'grass_block': 0x7c_bd_3b, - 'dirt': 0x8b_45_13, - 'stone': 0x7d_7d_7d, - 'cobblestone': 0x7a_7a_7a, - 'mossy_cobblestone': 0x65_97_65, - 'clay': 0xa0_a5_a6, - 'moss_block': 0x58_7d_3c, - 'spawner': 0x1a_1a_1a, - 'sand': 0xf7_e9_a3, - 'gravel': 0x8a_8a_8a, - 'iron_block': 0xd8_d8_d8, - 'gold_block': 0xfa_ee_4d, - 'diamond_block': 0x5c_db_d5, - 'emerald_block': 0x17_dd_62, - 'coal_block': 0x34_34_34, - 'redstone_block': 0xa1_27_22, - 'lapis_block': 0x22_4d_bb, - 'netherrack': 0x72_3a_32, - 'obsidian': 0x0f_0f_23, - 'bedrock': 0x56_56_56, - 'end_stone': 0xda_dc_a6, - 'quartz_block': 0xec_e4_d6, - 'snow_block': 0xff_ff_ff, - 'ice': 0x9c_f4_ff, - 'packed_ice': 0x74_c7_ec, - 'blue_ice': 0x3e_5f_bc, + // Get color from moreBlockDataGenerated.json + const rgbString = moreBlockData.colors[blockName] + if (rgbString) { + return parseRgbColor(rgbString) } - return colorMap[blockName] || 0x99_99_99 // Default gray + + // Fallback to default gray if color not found + return 0x99_99_99 } handleInstancedGeometry (data: InstancedSectionData) { @@ -276,10 +272,10 @@ export class InstancedRenderer { for (const [blockName, blockData] of instancedBlocks) { if (!INSTANCEABLE_BLOCKS.has(blockName)) continue - const mesh = this.instancedMeshes.get(blockName) + const mesh = this.instancedMeshes.get(this.getBlockId(blockName)) if (!mesh) continue - const currentCount = this.blockCounts.get(blockName) || 0 + const currentCount = this.blockCounts.get(this.getBlockId(blockName)) || 0 let instanceIndex = currentCount for (const pos of blockData.positions) { @@ -295,7 +291,7 @@ export class InstancedRenderer { mesh.count = instanceIndex mesh.instanceMatrix.needsUpdate = true - this.blockCounts.set(blockName, instanceIndex) + this.blockCounts.set(this.getBlockId(blockName), instanceIndex) } } @@ -312,7 +308,10 @@ export class InstancedRenderer { for (const [blockName, blockData] of Object.entries(instancedBlocks)) { if (!this.isBlockInstanceable(blockName)) continue - let mesh = this.instancedMeshes.get(blockName) + const { blockId } = blockData + this.blockIdToName.set(blockId, blockName) + + let mesh = this.instancedMeshes.get(blockId) if (!mesh) { // Create new mesh if it doesn't exist const material = this.createBlockMaterial(blockName, blockData.textureInfo) @@ -324,12 +323,12 @@ export class InstancedRenderer { mesh.name = `instanced_${blockName}` mesh.frustumCulled = false mesh.count = 0 - this.instancedMeshes.set(blockName, mesh) + this.instancedMeshes.set(blockId, mesh) this.worldRenderer.scene.add(mesh) } const instanceIndices: number[] = [] - const currentCount = this.blockCounts.get(blockName) || 0 + const currentCount = this.blockCounts.get(blockId) || 0 // Add new instances for this section for (const pos of blockData.positions) { @@ -346,9 +345,9 @@ export class InstancedRenderer { // Update tracking if (instanceIndices.length > 0) { - sectionMap.set(blockName, instanceIndices) - this.blockCounts.set(blockName, currentCount + instanceIndices.length) - mesh.count = this.blockCounts.get(blockName) || 0 + sectionMap.set(blockId, instanceIndices) + this.blockCounts.set(blockId, currentCount + instanceIndices.length) + mesh.count = this.blockCounts.get(blockId) || 0 mesh.instanceMatrix.needsUpdate = true } } @@ -357,10 +356,10 @@ export class InstancedRenderer { private clearSectionInstances (sectionKey: string) { // For now, we'll rebuild all instances each time // This could be optimized to track instances per section - for (const [blockName, mesh] of this.instancedMeshes) { + for (const [blockId, mesh] of this.instancedMeshes) { mesh.count = 0 mesh.instanceMatrix.needsUpdate = true - this.blockCounts.set(blockName, 0) + this.blockCounts.set(blockId, 0) } } @@ -369,24 +368,24 @@ export class InstancedRenderer { if (!sectionMap) return // Section not tracked // Remove instances for each block type in this section - for (const [blockName, instanceIndices] of sectionMap) { - const mesh = this.instancedMeshes.get(blockName) + for (const [blockId, instanceIndices] of sectionMap) { + const mesh = this.instancedMeshes.get(blockId) if (!mesh) continue // For efficiency, we'll rebuild the entire instance array by compacting it // This removes gaps left by deleted instances - this.compactInstancesForBlock(blockName, instanceIndices) + this.compactInstancesForBlock(blockId, instanceIndices) } // Remove section from tracking this.sectionInstances.delete(sectionKey) } - private compactInstancesForBlock (blockName: string, indicesToRemove: number[]) { - const mesh = this.instancedMeshes.get(blockName) + private compactInstancesForBlock (blockId: number, indicesToRemove: number[]) { + const mesh = this.instancedMeshes.get(blockId) if (!mesh) return - const currentCount = this.blockCounts.get(blockName) || 0 + const currentCount = this.blockCounts.get(blockId) || 0 const removeSet = new Set(indicesToRemove) let writeIndex = 0 @@ -405,23 +404,23 @@ export class InstancedRenderer { // Update count and indices in section tracking const newCount = writeIndex - this.blockCounts.set(blockName, newCount) + this.blockCounts.set(blockId, newCount) mesh.count = newCount mesh.instanceMatrix.needsUpdate = true // Update all section tracking to reflect compacted indices const offset = 0 for (const [sectionKey, sectionMap] of this.sectionInstances) { - const sectionIndices = sectionMap.get(blockName) + const sectionIndices = sectionMap.get(blockId) if (sectionIndices) { const compactedIndices = sectionIndices .filter(index => !removeSet.has(index)) .map(index => index - removeSet.size + offset) if (compactedIndices.length > 0) { - sectionMap.set(blockName, compactedIndices) + sectionMap.set(blockId, compactedIndices) } else { - sectionMap.delete(blockName) + sectionMap.delete(blockId) } } } @@ -448,19 +447,22 @@ export class InstancedRenderer { updateMaterials () { // Update materials when texture atlas changes - for (const [blockName, mesh] of this.instancedMeshes) { - const newMaterial = this.createBlockMaterial(blockName) - const oldMaterial = mesh.material - mesh.material = newMaterial - if (oldMaterial instanceof THREE.Material) { - oldMaterial.dispose() + for (const [blockId, mesh] of this.instancedMeshes) { + const blockName = this.blockIdToName.get(blockId) + if (blockName) { + const newMaterial = this.createBlockMaterial(blockName) + const oldMaterial = mesh.material + mesh.material = newMaterial + if (oldMaterial instanceof THREE.Material) { + oldMaterial.dispose() + } } } } destroy () { // Clean up resources - for (const [blockName, mesh] of this.instancedMeshes) { + for (const [blockId, mesh] of this.instancedMeshes) { this.worldRenderer.scene.remove(mesh) mesh.geometry.dispose() if (mesh.material instanceof THREE.Material) { @@ -470,6 +472,9 @@ export class InstancedRenderer { this.instancedMeshes.clear() this.blockCounts.clear() this.sectionInstances.clear() + this.blockIdToName.clear() + this.blockNameToId.clear() + this.nextBlockId = 0 this.cubeGeometry.dispose() } @@ -477,7 +482,7 @@ export class InstancedRenderer { let totalInstances = 0 let activeBlockTypes = 0 - for (const [blockName, mesh] of this.instancedMeshes) { + for (const [blockId, mesh] of this.instancedMeshes) { if (mesh.count > 0) { totalInstances += mesh.count activeBlockTypes++ @@ -491,4 +496,16 @@ export class InstancedRenderer { memoryEstimate: totalInstances * 64 // Rough estimate in bytes } } + + private getBlockId (blockName: string): number { + // Get the block ID from our local map + let blockId = this.blockNameToId.get(blockName) + if (blockId === undefined) { + // If the block ID doesn't exist, create a new one + blockId = this.nextBlockId++ + this.blockNameToId.set(blockName, blockId) + this.blockIdToName.set(blockId, blockName) + } + return blockId + } } diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 552aa2c1..a989c5d2 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -8,7 +8,7 @@ import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer import { chunkPos, sectionPos } from '../lib/simpleUtils' import { WorldRendererCommon } from '../lib/worldrendererCommon' 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 { getMyHand } from '../lib/hand' import { setBlockPosition } from '../lib/mesher/standaloneRenderer' @@ -911,10 +911,16 @@ export class WorldRendererThree extends WorldRendererCommon { } } - setSectionDirty (...args: Parameters) { - const [pos] = args + setSectionDirty (pos: Vec3, value = true) { + const { useInstancedRendering, enableSingleColorMode } = this.worldRendererConfig + let instancingMode = InstancingMode.None + + if (useInstancedRendering) { + instancingMode = enableSingleColorMode ? InstancingMode.ColorOnly : InstancingMode.TexturedInstancing + } + this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! - super.setSectionDirty(...args) + super.setSectionDirty(pos, value, undefined, instancingMode) } static getRendererInfo (renderer: THREE.WebGLRenderer) { From 7a692ac210e53eedb3a6de19b61ed55fcf14264e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 29 Jun 2025 15:22:58 +0300 Subject: [PATCH 03/27] f --- src/defaultOptions.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index d2d510ec..be5851b2 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -42,6 +42,11 @@ export const defaultOptions = { starfieldRendering: true, enabledResourcepack: null as string | null, useVersionsTextures: 'latest', + // Instanced rendering options + useInstancedRendering: false, + forceInstancedOnly: false, + instancedOnlyDistance: 6, + enableSingleColorMode: false, serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never', showHand: true, viewBobbing: true, From 561a18527fe4cbb5dff904ee11d3ed460f7558e4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 12 Jul 2025 05:38:12 +0300 Subject: [PATCH 04/27] add creative server --- config.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config.json b/config.json index 57b1c207..940fb738 100644 --- a/config.json +++ b/config.json @@ -16,6 +16,10 @@ { "ip": "wss://play2.mcraft.fun" }, + { + "ip": "wss://play-creative.mcraft.fun", + "description": "Might be available soon, stay tuned!" + }, { "ip": "kaboom.pw", "version": "1.20.3", From a19d459e8a7057e91b48b42a1ea7ebaebcb52d24 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 02:16:17 +0300 Subject: [PATCH 05/27] rm blocks hardcode --- renderer/viewer/lib/mesher/mesher.ts | 2 +- renderer/viewer/lib/mesher/models.ts | 167 +-------- renderer/viewer/lib/mesher/world.ts | 4 +- renderer/viewer/lib/worldrendererCommon.ts | 9 + renderer/viewer/three/getPreflatBlock.ts | 30 ++ renderer/viewer/three/instancedRenderer.ts | 375 ++++++++++++-------- renderer/viewer/three/worldrendererThree.ts | 20 +- 7 files changed, 297 insertions(+), 310 deletions(-) create mode 100644 renderer/viewer/three/getPreflatBlock.ts diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index 444093f2..f781275c 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -99,7 +99,7 @@ const handleMessage = data => { allDataReady = true workerIndex = data.workerIndex world.instancedBlocks = data.instancedBlocks - world.instancedBlockIds = data.instancedBlockIds || new Map() + world.instancedBlockIds = data.instancedBlockIds || {} break } diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index e2333a03..cb07d01a 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -7,140 +7,6 @@ import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, veca import { INVISIBLE_BLOCKS } from './worldConstants' import { MesherGeometryOutput, HighestBlockInfo, InstancedBlockEntry, InstancingMode } from './shared' -// Hardcoded list of full blocks that can use instancing -export const INSTANCEABLE_BLOCKS = new Set([ - 'grass_block', - 'dirt', - 'stone', - 'cobblestone', - 'mossy_cobblestone', - 'clay', - 'moss_block', - 'spawner', - 'sand', - 'gravel', - 'oak_planks', - 'birch_planks', - 'spruce_planks', - 'jungle_planks', - 'acacia_planks', - 'dark_oak_planks', - 'mangrove_planks', - 'cherry_planks', - 'bamboo_planks', - 'crimson_planks', - 'warped_planks', - 'iron_block', - 'gold_block', - 'diamond_block', - 'emerald_block', - 'netherite_block', - 'coal_block', - 'redstone_block', - 'lapis_block', - 'copper_block', - 'exposed_copper', - 'weathered_copper', - 'oxidized_copper', - 'cut_copper', - 'exposed_cut_copper', - 'weathered_cut_copper', - 'oxidized_cut_copper', - 'waxed_copper_block', - 'waxed_exposed_copper', - 'waxed_weathered_copper', - 'waxed_oxidized_copper', - 'raw_iron_block', - 'raw_copper_block', - 'raw_gold_block', - 'smooth_stone', - 'cobbled_deepslate', - 'deepslate', - 'calcite', - 'tuff', - 'dripstone_block', - 'amethyst_block', - 'budding_amethyst', - 'obsidian', - 'crying_obsidian', - 'bedrock', - 'end_stone', - 'purpur_block', - 'quartz_block', - 'smooth_quartz', - 'nether_bricks', - 'red_nether_bricks', - 'blackstone', - 'gilded_blackstone', - 'polished_blackstone', - 'chiseled_nether_bricks', - 'cracked_nether_bricks', - 'basalt', - 'smooth_basalt', - 'polished_basalt', - 'netherrack', - 'magma_block', - 'soul_sand', - 'soul_soil', - 'ancient_debris', - 'bone_block', - 'packed_ice', - 'blue_ice', - 'ice', - 'snow_block', - 'powder_snow', - 'white_wool', - 'orange_wool', - 'magenta_wool', - 'light_blue_wool', - 'yellow_wool', - 'lime_wool', - 'pink_wool', - 'gray_wool', - 'light_gray_wool', - 'cyan_wool', - 'purple_wool', - 'blue_wool', - 'brown_wool', - 'green_wool', - 'red_wool', - 'black_wool', - 'white_concrete', - 'orange_concrete', - 'magenta_concrete', - 'light_blue_concrete', - 'yellow_concrete', - 'lime_concrete', - 'pink_concrete', - 'gray_concrete', - 'light_gray_concrete', - 'cyan_concrete', - 'purple_concrete', - 'blue_concrete', - 'brown_concrete', - 'green_concrete', - 'red_concrete', - 'black_concrete', - 'white_terracotta', - 'orange_terracotta', - 'magenta_terracotta', - 'light_blue_terracotta', - 'yellow_terracotta', - 'lime_terracotta', - 'pink_terracotta', - 'gray_terracotta', - 'light_gray_terracotta', - 'cyan_terracotta', - 'purple_terracotta', - 'blue_terracotta', - 'brown_terracotta', - 'green_terracotta', - 'red_terracotta', - 'black_terracotta', - 'terracotta', - 'glazed_terracotta', -]) - let blockProvider: WorldBlockProvider const tints: any = {} @@ -720,9 +586,14 @@ const getInstancedBlockTextureInfo = (block: Block) => { return textureInfo } -const isBlockInstanceable = (block: Block): boolean => { - // Only instanceable if it's a full cube block without complex geometry - if (!INSTANCEABLE_BLOCKS.has(block.name)) return false +const isBlockInstanceable = (world: World, block: Block): boolean => { + // Use dynamic instanceable blocks data if available + const instancedBlocks = world?.instancedBlocks + if (Array.isArray(instancedBlocks)) { + if (!instancedBlocks.includes(block.name)) return false + } else { + return false + } // Check if it's actually a full cube (no rotations, no complex models) if (!block.models || block.models.length !== 1) return false @@ -839,7 +710,7 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W } if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) { // Check if this block can use instanced rendering - if (enableInstancedRendering && isBlockInstanceable(block)) { + if (enableInstancedRendering && isBlockInstanceable(world, block)) { // Check if block should be culled (all faces hidden by neighbors) if (shouldCullInstancedBlock(world, cursor, block)) { // Block is completely surrounded, skip rendering @@ -850,18 +721,16 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W if (!attr.instancedBlocks[blockKey]) { const textureInfo = getInstancedBlockTextureInfo(block) // Get or create block ID - let blockId = world.instancedBlockIds.get(block.name) - if (blockId === undefined) { - blockId = world.instancedBlockIds.size - world.instancedBlockIds.set(block.name, blockId) - } + const blockId = world.instancedBlockIds[block.stateId] - attr.instancedBlocks[blockKey] = { - blockId, - blockName: block.name, - stateId: block.stateId, - textureInfo, - positions: [] + if (blockId !== undefined) { + attr.instancedBlocks[blockKey] = { + blockId, + blockName: block.name, + stateId: block.stateId, + textureInfo, + positions: [] + } } } attr.instancedBlocks[blockKey].positions.push({ diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts index 110fb93c..bcff11b7 100644 --- a/renderer/viewer/lib/mesher/world.ts +++ b/renderer/viewer/lib/mesher/world.ts @@ -43,7 +43,7 @@ export class World { sentBlockStateModels = new Set() blockStateModelInfo = new Map() instancedBlocks: Record = {} - instancedBlockIds = new Map() + instancedBlockIds = {} as Record constructor (version: string) { this.Chunk = Chunks(version) as any @@ -123,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 @@ -133,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]) { diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index daaf6bfd..b176deea 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -160,6 +160,9 @@ export abstract class WorldRendererCommon abstract changeBackgroundColor (color: [number, number, number]): void + // Optional method for getting instanced blocks data (implemented by Three.js renderer) + getInstancedBlocksData? (): { instanceableBlocks: Set, allBlocksStateIdToModelIdMap: Record } | undefined + worldRendererConfig: WorldRendererConfig playerStateReactive: PlayerStateReactive playerStateUtils: PlayerStateUtils @@ -596,6 +599,10 @@ export abstract class WorldRendererCommon 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 @@ -607,6 +614,8 @@ export abstract class WorldRendererCommon }, blockstatesModels, config: this.getMesherConfig(), + instancedBlocks: instancedBlocksData?.instanceableBlocks ? [...instancedBlocksData.instanceableBlocks] : [], + instancedBlockIds: instancedBlocksData?.allBlocksStateIdToModelIdMap || {} }) } diff --git a/renderer/viewer/three/getPreflatBlock.ts b/renderer/viewer/three/getPreflatBlock.ts new file mode 100644 index 00000000..2c4e36a6 --- /dev/null +++ b/renderer/viewer/three/getPreflatBlock.ts @@ -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 +} diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 105cd3bb..f1306156 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -1,142 +1,12 @@ import * as THREE from 'three' import { Vec3 } from 'vec3' +import { versionToNumber } from 'flying-squid/dist/utils' +import PrismarineBlock, { Block } from 'prismarine-block' +import { IndexedBlock } from 'minecraft-data' import moreBlockData from '../lib/moreBlockDataGenerated.json' +import { getPreflatBlock } from './getPreflatBlock' import { WorldRendererThree } from './worldrendererThree' -// Hardcoded list of full blocks that can use instancing -export const INSTANCEABLE_BLOCKS = new Set([ - 'grass_block', - 'dirt', - 'stone', - 'cobblestone', - 'mossy_cobblestone', - 'clay', - 'moss_block', - 'spawner', - 'sand', - 'gravel', - 'oak_planks', - 'birch_planks', - 'spruce_planks', - 'jungle_planks', - 'acacia_planks', - 'dark_oak_planks', - 'mangrove_planks', - 'cherry_planks', - 'bamboo_planks', - 'crimson_planks', - 'warped_planks', - 'iron_block', - 'gold_block', - 'diamond_block', - 'emerald_block', - 'netherite_block', - 'coal_block', - 'redstone_block', - 'lapis_block', - 'copper_block', - 'exposed_copper', - 'weathered_copper', - 'oxidized_copper', - 'cut_copper', - 'exposed_cut_copper', - 'weathered_cut_copper', - 'oxidized_cut_copper', - 'waxed_copper_block', - 'waxed_exposed_copper', - 'waxed_weathered_copper', - 'waxed_oxidized_copper', - 'raw_iron_block', - 'raw_copper_block', - 'raw_gold_block', - 'smooth_stone', - 'cobbled_deepslate', - 'deepslate', - 'calcite', - 'tuff', - 'dripstone_block', - 'amethyst_block', - 'budding_amethyst', - 'obsidian', - 'crying_obsidian', - 'bedrock', - 'end_stone', - 'purpur_block', - 'quartz_block', - 'smooth_quartz', - 'nether_bricks', - 'red_nether_bricks', - 'blackstone', - 'gilded_blackstone', - 'polished_blackstone', - 'chiseled_nether_bricks', - 'cracked_nether_bricks', - 'basalt', - 'smooth_basalt', - 'polished_basalt', - 'netherrack', - 'magma_block', - 'soul_sand', - 'soul_soil', - 'ancient_debris', - 'bone_block', - 'packed_ice', - 'blue_ice', - 'ice', - 'snow_block', - 'powder_snow', - 'white_wool', - 'orange_wool', - 'magenta_wool', - 'light_blue_wool', - 'yellow_wool', - 'lime_wool', - 'pink_wool', - 'gray_wool', - 'light_gray_wool', - 'cyan_wool', - 'purple_wool', - 'blue_wool', - 'brown_wool', - 'green_wool', - 'red_wool', - 'black_wool', - 'white_concrete', - 'orange_concrete', - 'magenta_concrete', - 'light_blue_concrete', - 'yellow_concrete', - 'lime_concrete', - 'pink_concrete', - 'gray_concrete', - 'light_gray_concrete', - 'cyan_concrete', - 'purple_concrete', - 'blue_concrete', - 'brown_concrete', - 'green_concrete', - 'red_concrete', - 'black_concrete', - 'white_terracotta', - 'orange_terracotta', - 'magenta_terracotta', - 'light_blue_terracotta', - 'yellow_terracotta', - 'lime_terracotta', - 'pink_terracotta', - 'gray_terracotta', - 'light_gray_terracotta', - 'cyan_terracotta', - 'purple_terracotta', - 'blue_terracotta', - 'brown_terracotta', - 'green_terracotta', - 'red_terracotta', - 'black_terracotta', - 'terracotta', - 'glazed_terracotta', -]) - // 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) @@ -162,6 +32,21 @@ export interface InstancedSectionData { shouldUseInstancedOnly: boolean } +export interface InstancedBlockModelData { + textures: number[] + rotation: number[] + transparent?: boolean + emitLight?: number + filterLight?: number +} + +export interface InstancedBlocksConfig { + instanceableBlocks: Set + blocksDataModel: Record + allBlocksStateIdToModelIdMap: Record + interestedTextureTiles: Set +} + export class InstancedRenderer { private readonly instancedMeshes = new Map() private readonly blockCounts = new Map() @@ -173,9 +58,191 @@ export class InstancedRenderer { private readonly blockNameToId = new Map() private nextBlockId = 0 + // New properties for dynamic block detection + private instancedBlocksConfig: InstancedBlocksConfig | null = null + constructor (private readonly worldRenderer: WorldRendererThree) { this.cubeGeometry = this.createCubeGeometry() - this.initInstancedMeshes() + } + + prepareInstancedBlocksData (): InstancedBlocksConfig { + const blocksMap = { + '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 isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13') + const PBlockOriginal = PrismarineBlock(this.worldRenderer.version) + + const instanceableBlocks = new Set() + const blocksDataModel = {} as Record + const interestedTextureTiles = new Set() + const blocksProcessed = {} as Record + let i = 0 + const allBlocksStateIdToModelIdMap = {} as Record + + const addBlockModel = (state: number, name: string, props: Record, mcBlockData?: IndexedBlock, defaultState = false) => { + const possibleIssues = [] as string[] + const { currentResources } = this.worldRenderer.resourcesManager + if (!currentResources?.worldBlockProvider) return + + const models = currentResources.worldBlockProvider.getAllResolvedModels0_1({ + name, + properties: props + }, 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 = { + textures: [0, 0, 0, 0, 0, 0], + rotation: [0, 0, 0, 0, 0, 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.textures[faceIndex] = texture.tileIndex + blockData.rotation[faceIndex] = rotation / 90 + if (Math.floor(blockData.rotation[faceIndex]) !== blockData.rotation[faceIndex]) { + throw new Error(`Invalid rotation ${rotation} ${name}`) + } + interestedTextureTiles.add(texture.debugName) + } + + const k = i++ + allBlocksStateIdToModelIdMap[state] = k + blocksDataModel[k] = blockData + instanceableBlocks.add(name) + blocksProcessed[name] = true + + if (mcBlockData) { + blockData.transparent = mcBlockData.transparent + blockData.emitLight = mcBlockData.emitLight + blockData.filterLight = mcBlockData.filterLight + } + } + + // Add unknown block model + addBlockModel(-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 (globalThis as any).loadedData.blocksArray) { + for (let state = b.minStateId; state <= b.maxStateId; state++) { + const mapping = blocksMap[b.name] + const block = PBlockOriginal.fromStateId(mapping && (globalThis as any).loadedData.blocksByName[mapping] ? (globalThis as any).loadedData.blocksByName[mapping].defaultState : state, 0) + if (isPreflat) { + getPreflatBlock(block) + } + + const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined + if (textureOverride) { + const k = i++ + 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 + allBlocksStateIdToModelIdMap[state] = k + const blockData: InstancedBlockModelData = { + textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], + rotation: [0, 0, 0, 0, 0, 0], + filterLight: b.filterLight + } + blocksDataModel[k] = blockData + instanceableBlocks.add(block.name) + interestedTextureTiles.add(textureOverride) + 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 + } + + addBlockModel(state, block.name, block.getProperties(), b, state === b.defaultState) + } + } + + return { + instanceableBlocks, + blocksDataModel, + allBlocksStateIdToModelIdMap, + interestedTextureTiles + } + } + + initializeInstancedMeshes () { + if (!this.instancedBlocksConfig) { + console.warn('Instanced blocks config not prepared') + return + } + + // Create InstancedMesh for each instanceable block type + for (const blockName of this.instancedBlocksConfig.instanceableBlocks) { + const material = this.createBlockMaterial(blockName) + const mesh = new THREE.InstancedMesh( + this.cubeGeometry, + material, + this.maxInstancesPerBlock + ) + mesh.name = `instanced_${blockName}` + mesh.frustumCulled = false // Important for performance + mesh.count = 0 // Start with 0 instances + + this.instancedMeshes.set(this.getBlockId(blockName), mesh) + this.worldRenderer.scene.add(mesh) + } } private createCubeGeometry (): THREE.BoxGeometry { @@ -210,24 +277,6 @@ export class InstancedRenderer { return geometry } - private initInstancedMeshes () { - // Create InstancedMesh for each instanceable block type - for (const blockName of INSTANCEABLE_BLOCKS) { - const material = this.createBlockMaterial(blockName) - const mesh = new THREE.InstancedMesh( - this.cubeGeometry, - material, - this.maxInstancesPerBlock - ) - mesh.name = `instanced_${blockName}` - mesh.frustumCulled = false // Important for performance - mesh.count = 0 // Start with 0 instances - - this.instancedMeshes.set(this.getBlockId(blockName), mesh) - this.worldRenderer.scene.add(mesh) - } - } - private createBlockMaterial (blockName: string, textureInfo?: { u: number, v: number, su: number, sv: number }): THREE.Material { const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig @@ -270,7 +319,7 @@ export class InstancedRenderer { // Add new instances for (const [blockName, blockData] of instancedBlocks) { - if (!INSTANCEABLE_BLOCKS.has(blockName)) continue + if (!this.isBlockInstanceable(blockName)) continue const mesh = this.instancedMeshes.get(this.getBlockId(blockName)) if (!mesh) continue @@ -442,7 +491,7 @@ export class InstancedRenderer { } isBlockInstanceable (blockName: string): boolean { - return INSTANCEABLE_BLOCKS.has(blockName) + return this.instancedBlocksConfig?.instanceableBlocks.has(blockName) ?? false } updateMaterials () { @@ -508,4 +557,20 @@ export class InstancedRenderer { } return blockId } + + // New method to prepare and initialize everything + prepareAndInitialize () { + console.log('Preparing instanced blocks data...') + this.instancedBlocksConfig = this.prepareInstancedBlocksData() + console.log(`Found ${this.instancedBlocksConfig.instanceableBlocks.size} instanceable blocks:`, + [...this.instancedBlocksConfig.instanceableBlocks].slice(0, 10).join(', '), + this.instancedBlocksConfig.instanceableBlocks.size > 10 ? '...' : '') + + this.initializeInstancedMeshes() + } + + // Method to get the current configuration + getInstancedBlocksConfig (): InstancedBlocksConfig | null { + return this.instancedBlocksConfig + } } diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 82080b3f..21f6f68f 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -24,7 +24,7 @@ import { ThreeJsSound } from './threeJsSound' import { CameraShake } from './cameraShake' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' -import { InstancedRenderer, INSTANCEABLE_BLOCKS } from './instancedRenderer' +import { InstancedRenderer } from './instancedRenderer' type SectionKey = string @@ -97,8 +97,6 @@ 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) @@ -111,6 +109,8 @@ export class WorldRendererThree extends WorldRendererCommon { this.finishChunk(chunkKey) }) this.worldSwitchActions() + + void this.init() } get cameraObject () { @@ -143,6 +143,16 @@ export class WorldRendererThree extends WorldRendererCommon { } } + getInstancedBlocksData () { + const config = this.instancedRenderer.getInstancedBlocksConfig() + if (!config) return undefined + + return { + instanceableBlocks: config.instanceableBlocks, + allBlocksStateIdToModelIdMap: config.allBlocksStateIdToModelIdMap + } + } + updatePlayerEntity (e: any) { this.entities.handlePlayerEntity(e) } @@ -228,8 +238,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() + // Update instanced renderer materials when textures change this.instancedRenderer.updateMaterials() if (Object.keys(this.loadedChunks).length > 0) { From 102520233a98f9d327f80060f96b3e6333379664 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 03:14:08 +0300 Subject: [PATCH 06/27] code cleanup towards text --- renderer/viewer/lib/mesher/models.ts | 63 +------- renderer/viewer/lib/mesher/shared.ts | 6 - renderer/viewer/three/instancedRenderer.ts | 169 ++++++++------------ renderer/viewer/three/worldrendererThree.ts | 9 +- 4 files changed, 69 insertions(+), 178 deletions(-) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index cb07d01a..c2b76bce 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -5,7 +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, InstancedBlockEntry, InstancingMode } from './shared' +import { MesherGeometryOutput, InstancingMode } from './shared' +import { isBlockInstanceable } from './instancingUtils' let blockProvider: WorldBlockProvider @@ -550,64 +551,6 @@ const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boo return true } -const getInstancedBlockTextureInfo = (block: Block) => { - // Cache texture info by block state ID - const cacheKey = block.stateId - if (textureInfoCache.has(cacheKey)) { - return textureInfoCache.get(cacheKey) - } - - // Get texture info from the first available face - const model = block.models?.[0]?.[0] - if (!model?.elements?.[0]) return undefined - - const element = model.elements[0] - - // Try to find a face with texture - prefer visible faces - const faceOrder = ['up', 'north', 'east', 'south', 'west', 'down'] - let textureInfo: any - - for (const faceName of faceOrder) { - const face = element.faces[faceName] - if (face?.texture) { - const texture = face.texture as any - textureInfo = { - u: texture.u || 0, - v: texture.v || 0, - su: texture.su || 1, - sv: texture.sv || 1 - } - break - } - } - - // Cache the result - textureInfoCache.set(cacheKey, textureInfo) - return textureInfo -} - -const isBlockInstanceable = (world: World, block: Block): boolean => { - // Use dynamic instanceable blocks data if available - const instancedBlocks = world?.instancedBlocks - if (Array.isArray(instancedBlocks)) { - if (!instancedBlocks.includes(block.name)) return false - } else { - return false - } - - // Check if it's actually a full cube (no rotations, no complex models) - if (!block.models || block.models.length !== 1) return false - - const model = block.models[0][0] // First variant of first model - if (!model || model.x || model.y || model.z) return false // No rotations - - // Check if all elements are full cubes - return (model.elements ?? []).every(element => { - return element.from[0] === 0 && element.from[1] === 0 && element.from[2] === 0 && - element.to[0] === 16 && element.to[1] === 16 && element.to[2] === 16 - }) -} - let unknownBlockModel: BlockModelPartsResolved export function getSectionGeometry (sx: number, sy: number, sz: number, world: World, instancingMode = InstancingMode.None): MesherGeometryOutput { let delayedRender = [] as Array<() => void> @@ -719,7 +662,6 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W const blockKey = block.name if (!attr.instancedBlocks[blockKey]) { - const textureInfo = getInstancedBlockTextureInfo(block) // Get or create block ID const blockId = world.instancedBlockIds[block.stateId] @@ -728,7 +670,6 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W blockId, blockName: block.name, stateId: block.stateId, - textureInfo, positions: [] } } diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 97f9cf1a..aa6fcd84 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -31,12 +31,6 @@ export type InstancedBlockEntry = { blockId: number // Unique ID for this block type blockName: string stateId: number - textureInfo?: { - u: number - v: number - su: number - sv: number - } positions: Array<{ x: number, y: number, z: number }> } diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index f1306156..ee79201f 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -4,6 +4,7 @@ import { versionToNumber } from 'flying-squid/dist/utils' import PrismarineBlock, { Block } from 'prismarine-block' import { IndexedBlock } from 'minecraft-data' import moreBlockData from '../lib/moreBlockDataGenerated.json' +import { MesherGeometryOutput } from '../lib/mesher/shared' import { getPreflatBlock } from './getPreflatBlock' import { WorldRendererThree } from './worldrendererThree' @@ -45,13 +46,14 @@ export interface InstancedBlocksConfig { blocksDataModel: Record allBlocksStateIdToModelIdMap: Record interestedTextureTiles: Set + textureInfoByBlockId: Record } export class InstancedRenderer { private readonly instancedMeshes = new Map() private readonly blockCounts = new Map() private readonly sectionInstances = new Map>() - private readonly maxInstancesPerBlock = 100_000 + private readonly maxInstancesPerBlock = process.env.NODE_ENV === 'development' ? 100_000 : Infinity private readonly cubeGeometry: THREE.BoxGeometry private readonly tempMatrix = new THREE.Matrix4() private readonly blockIdToName = new Map() @@ -96,6 +98,7 @@ export class InstancedRenderer { const blocksProcessed = {} as Record let i = 0 const allBlocksStateIdToModelIdMap = {} as Record + const textureInfoByBlockId: Record = {} const addBlockModel = (state: number, name: string, props: Record, mcBlockData?: IndexedBlock, defaultState = false) => { const possibleIssues = [] as string[] @@ -135,6 +138,7 @@ export class InstancedRenderer { rotation: [0, 0, 0, 0, 0, 0] } + const blockId = i++ for (const [face, { texture, cullface, rotation = 0 }] of Object.entries(elem.faces)) { const faceIndex = facesMapping.findIndex(x => x.includes(face)) if (faceIndex === -1) { @@ -147,11 +151,11 @@ export class InstancedRenderer { throw new Error(`Invalid rotation ${rotation} ${name}`) } interestedTextureTiles.add(texture.debugName) + textureInfoByBlockId[blockId] = { u: texture.u, v: texture.v, su: texture.su, sv: texture.sv } } - const k = i++ - allBlocksStateIdToModelIdMap[state] = k - blocksDataModel[k] = blockData + allBlocksStateIdToModelIdMap[state] = blockId + blocksDataModel[blockId] = blockData instanceableBlocks.add(name) blocksProcessed[name] = true @@ -182,7 +186,7 @@ export class InstancedRenderer { const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined if (textureOverride) { - const k = i++ + const blockId = i++ const { currentResources } = this.worldRenderer.resourcesManager if (!currentResources?.worldBlockProvider) continue const texture = currentResources.worldBlockProvider.getTextureInfo(textureOverride) @@ -191,15 +195,16 @@ export class InstancedRenderer { continue } const texIndex = texture.tileIndex - allBlocksStateIdToModelIdMap[state] = k + allBlocksStateIdToModelIdMap[state] = blockId const blockData: InstancedBlockModelData = { textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], rotation: [0, 0, 0, 0, 0, 0], filterLight: b.filterLight } - blocksDataModel[k] = blockData + blocksDataModel[blockId] = blockData instanceableBlocks.add(block.name) interestedTextureTiles.add(textureOverride) + textureInfoByBlockId[blockId] = { u: texture.u, v: texture.v, su: texture.su, sv: texture.sv } continue } @@ -218,7 +223,20 @@ export class InstancedRenderer { instanceableBlocks, blocksDataModel, allBlocksStateIdToModelIdMap, - interestedTextureTiles + interestedTextureTiles, + textureInfoByBlockId + } + } + + private createBlockMaterial (blockName: string, textureInfo?: { u: number, v: number, su: number, sv: number }): THREE.Material { + const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig + + if (enableSingleColorMode) { + // Ultra-performance mode: solid colors only + const color = this.getBlockColor(blockName) + return new THREE.MeshBasicMaterial({ color }) + } else { + return this.worldRenderer.material } } @@ -230,18 +248,31 @@ export class InstancedRenderer { // Create InstancedMesh for each instanceable block type for (const blockName of this.instancedBlocksConfig.instanceableBlocks) { - const material = this.createBlockMaterial(blockName) + const blockId = this.getBlockId(blockName) + if (this.instancedMeshes.has(blockId)) continue // Skip if already exists + + const textureInfo = this.instancedBlocksConfig.textureInfoByBlockId[blockId] + + const geometry = textureInfo ? this.createCustomGeometry(textureInfo) : this.cubeGeometry + const material = this.createBlockMaterial(blockName, textureInfo) + const mesh = new THREE.InstancedMesh( - this.cubeGeometry, + geometry, material, this.maxInstancesPerBlock ) mesh.name = `instanced_${blockName}` mesh.frustumCulled = false // Important for performance - mesh.count = 0 // Start with 0 instances + mesh.count = 0 - this.instancedMeshes.set(this.getBlockId(blockName), mesh) + this.instancedMeshes.set(blockId, mesh) this.worldRenderer.scene.add(mesh) + + if (textureInfo) { + console.log(`Created instanced mesh for ${blockName} with texture info:`, textureInfo) + } else { + console.warn(`No texture info found for block ${blockName}`) + } } } @@ -255,51 +286,33 @@ export class InstancedRenderer { private createCustomGeometry (textureInfo: { u: number, v: number, su: number, sv: number }): THREE.BufferGeometry { // Create custom geometry with specific UV coordinates for this block type - // This would be more complex but would give perfect texture mapping 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 - // Modify UV coordinates to use the specific texture region - // This is a simplified version - real implementation would need to handle all 6 faces properly - for (let i = 0; i < uvs.length; i += 2) { - const u = uvs[i] - const v = uvs[i + 1] + // BoxGeometry has 6 faces, each with 2 triangles (4 vertices), so 24 UV pairs total + // The order in Three.js BoxGeometry is: +X, -X, +Y, -Y, +Z, -Z + // We need to map the texture coordinates properly for each face - // Map from 0-1 to the specific texture region - uvs[i] = textureInfo.u + u * textureInfo.su - uvs[i + 1] = textureInfo.v + v * textureInfo.sv + if (this.instancedBlocksConfig && textureInfo) { + // For now, apply the same texture to all faces + // In the future, this could be enhanced to use different textures per face + for (let i = 0; i < uvs.length; i += 2) { + const u = uvs[i] + const v = uvs[i + 1] + + // Map from 0-1 to the specific texture region in the atlas + uvs[i] = textureInfo.u + u * textureInfo.su + uvs[i + 1] = textureInfo.v + v * textureInfo.sv + } } uvAttribute.needsUpdate = true return geometry } - private createBlockMaterial (blockName: string, textureInfo?: { u: number, v: number, su: number, sv: number }): THREE.Material { - const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig - - if (enableSingleColorMode) { - // Ultra-performance mode: solid colors only - const color = this.getBlockColor(blockName) - return new THREE.MeshBasicMaterial({ color }) - } else { - // Use texture from the blocks atlas - // eslint-disable-next-line no-lonely-if - if (this.worldRenderer.material.map) { - const material = this.worldRenderer.material.clone() - // The texture is already the correct blocks atlas texture - // Individual block textures are handled by UV coordinates in the geometry - return material - } else { - // Fallback to colored material - const color = this.getBlockColor(blockName) - return new THREE.MeshLambertMaterial({ color }) - } - } - } - private getBlockColor (blockName: string): number { // Get color from moreBlockDataGenerated.json const rgbString = moreBlockData.colors[blockName] @@ -311,43 +324,7 @@ export class InstancedRenderer { return 0x99_99_99 } - handleInstancedGeometry (data: InstancedSectionData) { - const { sectionKey, instancedBlocks } = data - - // Clear previous instances for this section - this.clearSectionInstances(sectionKey) - - // Add new instances - for (const [blockName, blockData] of instancedBlocks) { - if (!this.isBlockInstanceable(blockName)) continue - - const mesh = this.instancedMeshes.get(this.getBlockId(blockName)) - if (!mesh) continue - - const currentCount = this.blockCounts.get(this.getBlockId(blockName)) || 0 - let instanceIndex = currentCount - - for (const pos of blockData.positions) { - if (instanceIndex >= this.maxInstancesPerBlock) { - console.warn(`Exceeded max instances for block ${blockName} (${instanceIndex}/${this.maxInstancesPerBlock})`) - break - } - - this.tempMatrix.setPosition(pos.x, pos.y, pos.z) - mesh.setMatrixAt(instanceIndex, this.tempMatrix) - instanceIndex++ - } - - mesh.count = instanceIndex - mesh.instanceMatrix.needsUpdate = true - this.blockCounts.set(this.getBlockId(blockName), instanceIndex) - } - } - - handleInstancedBlocksFromWorker (instancedBlocks: Record, sectionKey: string) { - // Clear existing instances for this section first - this.removeSectionInstances(sectionKey) - + handleInstancedBlocksFromWorker (instancedBlocks: MesherGeometryOutput['instancedBlocks'], sectionKey: string) { // Initialize section tracking if not exists if (!this.sectionInstances.has(sectionKey)) { this.sectionInstances.set(sectionKey, new Map()) @@ -357,23 +334,13 @@ export class InstancedRenderer { for (const [blockName, blockData] of Object.entries(instancedBlocks)) { if (!this.isBlockInstanceable(blockName)) continue - const { blockId } = blockData + const { blockId, stateId } = blockData this.blockIdToName.set(blockId, blockName) - let mesh = this.instancedMeshes.get(blockId) + const mesh = this.instancedMeshes.get(blockId) if (!mesh) { - // Create new mesh if it doesn't exist - const material = this.createBlockMaterial(blockName, blockData.textureInfo) - mesh = new THREE.InstancedMesh( - this.cubeGeometry, - material, - this.maxInstancesPerBlock - ) - mesh.name = `instanced_${blockName}` - mesh.frustumCulled = false - mesh.count = 0 - this.instancedMeshes.set(blockId, mesh) - this.worldRenderer.scene.add(mesh) + console.warn(`Failed to find mesh for block ${blockName}`) + continue } const instanceIndices: number[] = [] @@ -387,7 +354,7 @@ export class InstancedRenderer { } const instanceIndex = currentCount + instanceIndices.length - this.tempMatrix.setPosition(pos.x, pos.y, pos.z) + this.tempMatrix.setPosition(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5) mesh.setMatrixAt(instanceIndex, this.tempMatrix) instanceIndices.push(instanceIndex) } @@ -402,16 +369,6 @@ export class InstancedRenderer { } } - private clearSectionInstances (sectionKey: string) { - // For now, we'll rebuild all instances each time - // This could be optimized to track instances per section - for (const [blockId, mesh] of this.instancedMeshes) { - mesh.count = 0 - mesh.instanceMatrix.needsUpdate = true - this.blockCounts.set(blockId, 0) - } - } - removeSectionInstances (sectionKey: string) { const sectionMap = this.sectionInstances.get(sectionKey) if (!sectionMap) return // Section not tracked diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 21f6f68f..56eaa3d2 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -366,19 +366,18 @@ export class WorldRendererThree extends WorldRendererCommon { } // debugRecomputedDeletedObjects = 0 - handleInstancedBlocks (instancedBlocks: Record, sectionKey: string): void { - this.instancedRenderer.handleInstancedBlocksFromWorker(instancedBlocks, sectionKey) - } - handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void { if (data.type !== 'geometry') return const chunkCoords = data.key.split(',') const chunkKey = chunkCoords[0] + ',' + chunkCoords[2] + // Always clear old instanced blocks for this section first, then add new ones if any + this.instancedRenderer.removeSectionInstances(data.key) + // Handle instanced blocks data from worker if (data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0) { - this.handleInstancedBlocks(data.geometry.instancedBlocks, data.key) + this.instancedRenderer.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key) } let object: THREE.Object3D = this.sectionObjects[data.key] From 60834169434338f18d5a7d18559a113658f71961 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 03:15:36 +0300 Subject: [PATCH 07/27] utils --- renderer/viewer/lib/mesher/instancingUtils.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 renderer/viewer/lib/mesher/instancingUtils.ts diff --git a/renderer/viewer/lib/mesher/instancingUtils.ts b/renderer/viewer/lib/mesher/instancingUtils.ts new file mode 100644 index 00000000..56947bee --- /dev/null +++ b/renderer/viewer/lib/mesher/instancingUtils.ts @@ -0,0 +1,24 @@ +import { WorldBlock as Block, World } from './world' + +// Returns true if the block is instanceable (full cube, no rotations, etc.) +export const isBlockInstanceable = (world: World, block: Block): boolean => { + // Use dynamic instanceable blocks data if available + const instancedBlocks = world?.instancedBlocks + if (Array.isArray(instancedBlocks)) { + if (!instancedBlocks.includes(block.name)) return false + } else { + return false + } + + // Check if it's actually a full cube (no rotations, no complex models) + if (!block.models || block.models.length !== 1) return false + + const model = block.models[0][0] // First variant of first model + if (!model || model.x || model.y || model.z) return false // No rotations + + // Check if all elements are full cubes + return (model.elements ?? []).every(element => { + return element.from[0] === 0 && element.from[1] === 0 && element.from[2] === 0 && + element.to[0] === 16 && element.to[1] === 16 && element.to[2] === 16 + }) +} From dbce9e7becff0e6af345e3b7916399f4a3432d1d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 04:17:25 +0300 Subject: [PATCH 08/27] working texturing --- renderer/viewer/lib/worldrendererCommon.ts | 5 + renderer/viewer/three/instancedRenderer.ts | 105 ++++++++++++-------- renderer/viewer/three/worldrendererThree.ts | 2 - 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index b176deea..85d677fd 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -684,6 +684,11 @@ export abstract class WorldRendererCommon 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) { diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index ee79201f..4f622114 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -53,7 +53,7 @@ export class InstancedRenderer { private readonly instancedMeshes = new Map() private readonly blockCounts = new Map() private readonly sectionInstances = new Map>() - private readonly maxInstancesPerBlock = process.env.NODE_ENV === 'development' ? 100_000 : Infinity + private readonly maxInstancesPerBlock = process.env.NODE_ENV === 'development' ? 1_000_000 : 10_000_000 private readonly cubeGeometry: THREE.BoxGeometry private readonly tempMatrix = new THREE.Matrix4() private readonly blockIdToName = new Map() @@ -62,12 +62,23 @@ export class InstancedRenderer { // New properties for dynamic block detection private instancedBlocksConfig: InstancedBlocksConfig | null = null + private sharedMaterial: THREE.MeshLambertMaterial | null = null constructor (private readonly worldRenderer: WorldRendererThree) { this.cubeGeometry = this.createCubeGeometry() } prepareInstancedBlocksData (): InstancedBlocksConfig { + if (this.sharedMaterial) { + this.sharedMaterial.dispose() + this.sharedMaterial = null + } + this.sharedMaterial = new THREE.MeshLambertMaterial({ + transparent: true, + alphaTest: 0.1 + }) + this.sharedMaterial.map = this.worldRenderer.material.map + const blocksMap = { 'double_stone_slab': 'stone', 'stone_slab': 'stone', @@ -234,9 +245,11 @@ export class InstancedRenderer { if (enableSingleColorMode) { // Ultra-performance mode: solid colors only const color = this.getBlockColor(blockName) - return new THREE.MeshBasicMaterial({ color }) + const material = new THREE.MeshBasicMaterial({ color }) + material.name = `instanced_color_${blockName}` + return material } else { - return this.worldRenderer.material + return this.sharedMaterial! } } @@ -269,7 +282,7 @@ export class InstancedRenderer { this.worldRenderer.scene.add(mesh) if (textureInfo) { - console.log(`Created instanced mesh for ${blockName} with texture info:`, textureInfo) + // console.log(`Created instanced mesh for ${blockName} with texture info:`, textureInfo) } else { console.warn(`No texture info found for block ${blockName}`) } @@ -331,6 +344,17 @@ export class InstancedRenderer { } const sectionMap = this.sectionInstances.get(sectionKey)! + // Remove old instances for blocks that are being updated + const previousBlockIds = [...sectionMap.keys()] + for (const blockId of previousBlockIds) { + const instanceIndices = sectionMap.get(blockId) + if (instanceIndices) { + this.removeInstancesFromBlock(blockId, instanceIndices) + sectionMap.delete(blockId) + } + } + + // Keep track of blocks that were updated this frame for (const [blockName, blockData] of Object.entries(instancedBlocks)) { if (!this.isBlockInstanceable(blockName)) continue @@ -375,31 +399,29 @@ export class InstancedRenderer { // Remove instances for each block type in this section for (const [blockId, instanceIndices] of sectionMap) { - const mesh = this.instancedMeshes.get(blockId) - if (!mesh) continue - - // For efficiency, we'll rebuild the entire instance array by compacting it - // This removes gaps left by deleted instances - this.compactInstancesForBlock(blockId, instanceIndices) + this.removeInstancesFromBlock(blockId, instanceIndices) } // Remove section from tracking this.sectionInstances.delete(sectionKey) } - private compactInstancesForBlock (blockId: number, indicesToRemove: number[]) { + private removeInstancesFromBlock (blockId: number, indicesToRemove: number[]) { const mesh = this.instancedMeshes.get(blockId) - if (!mesh) return + if (!mesh || indicesToRemove.length === 0) return const currentCount = this.blockCounts.get(blockId) || 0 const removeSet = new Set(indicesToRemove) + // Create mapping from old indices to new indices + const indexMapping = new Map() 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) @@ -408,23 +430,22 @@ export class InstancedRenderer { } } - // Update count and indices in section tracking + // Update count const newCount = writeIndex this.blockCounts.set(blockId, newCount) mesh.count = newCount mesh.instanceMatrix.needsUpdate = true - // Update all section tracking to reflect compacted indices - const offset = 0 + // Update all section tracking to reflect new indices for (const [sectionKey, sectionMap] of this.sectionInstances) { const sectionIndices = sectionMap.get(blockId) if (sectionIndices) { - const compactedIndices = sectionIndices - .filter(index => !removeSet.has(index)) - .map(index => index - removeSet.size + offset) + const updatedIndices = sectionIndices + .map(index => indexMapping.get(index)) + .filter(index => index !== undefined) - if (compactedIndices.length > 0) { - sectionMap.set(blockId, compactedIndices) + if (updatedIndices.length > 0) { + sectionMap.set(blockId, updatedIndices) } else { sectionMap.delete(blockId) } @@ -432,37 +453,38 @@ export class InstancedRenderer { } } - shouldUseInstancedRendering (chunkKey: string): boolean { - const { useInstancedRendering, forceInstancedOnly, instancedOnlyDistance } = this.worldRenderer.worldRendererConfig + // private compactInstancesForBlock (blockId: number, indicesToRemove: number[]) { + // // This method is now replaced by removeInstancesFromBlock + // this.removeInstancesFromBlock(blockId, indicesToRemove) + // } - if (!useInstancedRendering) return false - if (forceInstancedOnly) return true + // shouldUseInstancedRendering (chunkKey: string): boolean { + // const { useInstancedRendering, forceInstancedOnly, instancedOnlyDistance } = this.worldRenderer.worldRendererConfig - // Check distance for automatic switching - const [x, z] = chunkKey.split(',').map(Number) - const chunkPos = new Vec3(x * 16, 0, z * 16) - const [dx, dz] = this.worldRenderer.getDistance(chunkPos) - const maxDistance = Math.max(dx, dz) + // if (!useInstancedRendering) return false + // if (forceInstancedOnly) return true - return maxDistance >= instancedOnlyDistance - } + // // Check distance for automatic switching + // const [x, z] = chunkKey.split(',').map(Number) + // const chunkPos = new Vec3(x * 16, 0, z * 16) + // const [dx, dz] = this.worldRenderer.getDistance(chunkPos) + // const maxDistance = Math.max(dx, dz) + + // return maxDistance >= instancedOnlyDistance + // } isBlockInstanceable (blockName: string): boolean { return this.instancedBlocksConfig?.instanceableBlocks.has(blockName) ?? false } - updateMaterials () { - // Update materials when texture atlas changes + disposeOldMeshes () { for (const [blockId, mesh] of this.instancedMeshes) { - const blockName = this.blockIdToName.get(blockId) - if (blockName) { - const newMaterial = this.createBlockMaterial(blockName) - const oldMaterial = mesh.material - mesh.material = newMaterial - if (oldMaterial instanceof THREE.Material) { - oldMaterial.dispose() - } + if (mesh.material instanceof THREE.Material && mesh.material.name.startsWith('instanced_color_')) { + mesh.material.dispose() } + mesh.geometry.dispose() + this.instancedMeshes.delete(blockId) + this.worldRenderer.scene.remove(mesh) } } @@ -523,6 +545,7 @@ export class InstancedRenderer { [...this.instancedBlocksConfig.instanceableBlocks].slice(0, 10).join(', '), this.instancedBlocksConfig.instanceableBlocks.size > 10 ? '...' : '') + this.disposeOldMeshes() this.initializeInstancedMeshes() } diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 56eaa3d2..b0f880ad 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -244,8 +244,6 @@ export class WorldRendererThree extends WorldRendererCommon { await super.updateAssetsData() this.onAllTexturesLoaded() - // Update instanced renderer materials when textures change - this.instancedRenderer.updateMaterials() if (Object.keys(this.loadedChunks).length > 0) { console.log('rerendering chunks because of texture update') this.rerenderAllChunks() From 9f29491b5dac889d235541eded6a137fa22e4d45 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 04:55:43 +0300 Subject: [PATCH 09/27] maybe support rotation --- renderer/viewer/three/instancedRenderer.ts | 211 +++++++++++++++------ 1 file changed, 155 insertions(+), 56 deletions(-) diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 4f622114..9d45f21f 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -39,6 +39,7 @@ export interface InstancedBlockModelData { transparent?: boolean emitLight?: number filterLight?: number + textureInfos?: Array<{ u: number, v: number, su: number, sv: number }> // Store texture info for each face } export interface InstancedBlocksConfig { @@ -46,20 +47,23 @@ export interface InstancedBlocksConfig { blocksDataModel: Record allBlocksStateIdToModelIdMap: Record interestedTextureTiles: Set - textureInfoByBlockId: Record } export class InstancedRenderer { private readonly instancedMeshes = new Map() private readonly blockCounts = new Map() private readonly sectionInstances = new Map>() - private readonly maxInstancesPerBlock = process.env.NODE_ENV === 'development' ? 1_000_000 : 10_000_000 private readonly cubeGeometry: THREE.BoxGeometry private readonly tempMatrix = new THREE.Matrix4() private readonly blockIdToName = new Map() private readonly blockNameToId = new Map() private nextBlockId = 0 + // Dynamic instance management + private readonly baseInstancesPerBlock = 100_000 // Base instances per block type + private readonly maxTotalInstances = 10_000_000 // Total instance budget across all blocks + private currentTotalInstances = 0 + // New properties for dynamic block detection private instancedBlocksConfig: InstancedBlocksConfig | null = null private sharedMaterial: THREE.MeshLambertMaterial | null = null @@ -68,6 +72,37 @@ export class InstancedRenderer { this.cubeGeometry = this.createCubeGeometry() } + private getMaxInstancesPerBlock (): number { + const renderDistance = this.worldRenderer.viewDistance + if (renderDistance <= 0) return this.baseInstancesPerBlock + + // Calculate dynamic limit based on render distance + // More render distance = more chunks = need more instances + const distanceFactor = Math.max(1, renderDistance / 8) // Normalize around render distance 8 + const dynamicLimit = Math.floor(this.baseInstancesPerBlock * distanceFactor) + + // Cap at reasonable limits to prevent memory issues + return Math.min(dynamicLimit, 500_000) + } + + private canAddMoreInstances (blockId: number, count: number): boolean { + const currentForBlock = this.blockCounts.get(blockId) || 0 + const maxPerBlock = this.getMaxInstancesPerBlock() + + // Check per-block limit + if (currentForBlock + count > maxPerBlock) { + return false + } + + // Check total instance budget + if (this.currentTotalInstances + count > this.maxTotalInstances) { + console.warn(`Total instance limit reached (${this.currentTotalInstances}/${this.maxTotalInstances}). Consider reducing render distance.`) + return false + } + + return true + } + prepareInstancedBlocksData (): InstancedBlocksConfig { if (this.sharedMaterial) { this.sharedMaterial.dispose() @@ -109,7 +144,6 @@ export class InstancedRenderer { const blocksProcessed = {} as Record let i = 0 const allBlocksStateIdToModelIdMap = {} as Record - const textureInfoByBlockId: Record = {} const addBlockModel = (state: number, name: string, props: Record, mcBlockData?: IndexedBlock, defaultState = false) => { const possibleIssues = [] as string[] @@ -146,7 +180,8 @@ export class InstancedRenderer { const blockData: InstancedBlockModelData = { textures: [0, 0, 0, 0, 0, 0], - rotation: [0, 0, 0, 0, 0, 0] + rotation: [0, 0, 0, 0, 0, 0], + textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 })) } const blockId = i++ @@ -162,7 +197,14 @@ export class InstancedRenderer { throw new Error(`Invalid rotation ${rotation} ${name}`) } interestedTextureTiles.add(texture.debugName) - textureInfoByBlockId[blockId] = { u: texture.u, v: texture.v, su: texture.su, sv: texture.sv } + + // Store texture info for this face + blockData.textureInfos![faceIndex] = { + u: texture.u, + v: texture.v, + su: texture.su, + sv: texture.sv + } } allBlocksStateIdToModelIdMap[state] = blockId @@ -210,12 +252,17 @@ export class InstancedRenderer { const blockData: InstancedBlockModelData = { textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], rotation: [0, 0, 0, 0, 0, 0], - filterLight: b.filterLight + filterLight: b.filterLight, + textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ + u: texture.u, + v: texture.v, + su: texture.su, + sv: texture.sv + })) } blocksDataModel[blockId] = blockData instanceableBlocks.add(block.name) interestedTextureTiles.add(textureOverride) - textureInfoByBlockId[blockId] = { u: texture.u, v: texture.v, su: texture.su, sv: texture.sv } continue } @@ -234,12 +281,11 @@ export class InstancedRenderer { instanceableBlocks, blocksDataModel, allBlocksStateIdToModelIdMap, - interestedTextureTiles, - textureInfoByBlockId + interestedTextureTiles } } - private createBlockMaterial (blockName: string, textureInfo?: { u: number, v: number, su: number, sv: number }): THREE.Material { + private createBlockMaterial (blockName: string): THREE.Material { const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig if (enableSingleColorMode) { @@ -264,15 +310,15 @@ export class InstancedRenderer { const blockId = this.getBlockId(blockName) if (this.instancedMeshes.has(blockId)) continue // Skip if already exists - const textureInfo = this.instancedBlocksConfig.textureInfoByBlockId[blockId] + const blockModelData = this.instancedBlocksConfig.blocksDataModel[blockId] - const geometry = textureInfo ? this.createCustomGeometry(textureInfo) : this.cubeGeometry - const material = this.createBlockMaterial(blockName, textureInfo) + const geometry = blockModelData ? this.createCustomGeometry(0, blockModelData) : this.cubeGeometry + const material = this.createBlockMaterial(blockName) const mesh = new THREE.InstancedMesh( geometry, material, - this.maxInstancesPerBlock + this.getMaxInstancesPerBlock() ) mesh.name = `instanced_${blockName}` mesh.frustumCulled = false // Important for performance @@ -281,10 +327,8 @@ export class InstancedRenderer { this.instancedMeshes.set(blockId, mesh) this.worldRenderer.scene.add(mesh) - if (textureInfo) { - // console.log(`Created instanced mesh for ${blockName} with texture info:`, textureInfo) - } else { - console.warn(`No texture info found for block ${blockName}`) + if (!blockModelData) { + console.warn(`No block model data found for block ${blockName}`) } } } @@ -297,28 +341,86 @@ export class InstancedRenderer { return geometry } - private createCustomGeometry (textureInfo: { u: number, v: number, su: number, sv: number }): THREE.BufferGeometry { - // Create custom geometry with specific UV coordinates for this block type + private createCustomGeometry (stateId: number, blockModelData: InstancedBlockModelData): THREE.BufferGeometry { + // 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 - // BoxGeometry has 6 faces, each with 2 triangles (4 vertices), so 24 UV pairs total - // The order in Three.js BoxGeometry is: +X, -X, +Y, -Y, +Z, -Z - // We need to map the texture coordinates properly for each face + if (!blockModelData.textureInfos) { + console.warn('No texture infos available for block model') + return geometry + } - if (this.instancedBlocksConfig && textureInfo) { - // For now, apply the same texture to all faces - // In the future, this could be enhanced to use different textures per face - for (let i = 0; i < uvs.length; i += 2) { - const u = uvs[i] - const v = uvs[i + 1] + // 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] - // Map from 0-1 to the specific texture region in the atlas - uvs[i] = textureInfo.u + u * textureInfo.su - uvs[i + 1] = textureInfo.v + v * textureInfo.sv + 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°) + if (rotation > 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 + rotation) % 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 } } @@ -370,10 +472,10 @@ export class InstancedRenderer { const instanceIndices: number[] = [] const currentCount = this.blockCounts.get(blockId) || 0 - // Add new instances for this section + // Add new instances for this section (with limit checking) for (const pos of blockData.positions) { - if (currentCount + instanceIndices.length >= this.maxInstancesPerBlock) { - console.warn(`Exceeded max instances for block ${blockName} (${currentCount + instanceIndices.length}/${this.maxInstancesPerBlock})`) + if (!this.canAddMoreInstances(blockId, 1)) { + console.warn(`Exceeded max instances for block ${blockName} (${currentCount + instanceIndices.length}/${this.getMaxInstancesPerBlock()})`) break } @@ -387,6 +489,7 @@ export class InstancedRenderer { if (instanceIndices.length > 0) { sectionMap.set(blockId, instanceIndices) this.blockCounts.set(blockId, currentCount + instanceIndices.length) + this.currentTotalInstances += instanceIndices.length mesh.count = this.blockCounts.get(blockId) || 0 mesh.instanceMatrix.needsUpdate = true } @@ -413,6 +516,9 @@ export class InstancedRenderer { const currentCount = this.blockCounts.get(blockId) || 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() let writeIndex = 0 @@ -453,31 +559,14 @@ export class InstancedRenderer { } } - // private compactInstancesForBlock (blockId: number, indicesToRemove: number[]) { - // // This method is now replaced by removeInstancesFromBlock - // this.removeInstancesFromBlock(blockId, indicesToRemove) - // } - - // shouldUseInstancedRendering (chunkKey: string): boolean { - // const { useInstancedRendering, forceInstancedOnly, instancedOnlyDistance } = this.worldRenderer.worldRendererConfig - - // if (!useInstancedRendering) return false - // if (forceInstancedOnly) return true - - // // Check distance for automatic switching - // const [x, z] = chunkKey.split(',').map(Number) - // const chunkPos = new Vec3(x * 16, 0, z * 16) - // const [dx, dz] = this.worldRenderer.getDistance(chunkPos) - // const maxDistance = Math.max(dx, dz) - - // return maxDistance >= instancedOnlyDistance - // } - isBlockInstanceable (blockName: string): boolean { return this.instancedBlocksConfig?.instanceableBlocks.has(blockName) ?? false } disposeOldMeshes () { + // Reset total instance count since we're clearing everything + this.currentTotalInstances = 0 + for (const [blockId, mesh] of this.instancedMeshes) { if (mesh.material instanceof THREE.Material && mesh.material.name.startsWith('instanced_color_')) { mesh.material.dispose() @@ -486,6 +575,9 @@ export class InstancedRenderer { this.instancedMeshes.delete(blockId) this.worldRenderer.scene.remove(mesh) } + + // Clear counts + this.blockCounts.clear() } destroy () { @@ -517,11 +609,18 @@ export class InstancedRenderer { } } + const maxPerBlock = this.getMaxInstancesPerBlock() + const renderDistance = this.worldRenderer.viewDistance + return { totalInstances, activeBlockTypes, drawCalls: activeBlockTypes, // One draw call per active block type - memoryEstimate: totalInstances * 64 // Rough estimate in bytes + memoryEstimate: totalInstances * 64, // Rough estimate in bytes + maxInstancesPerBlock: maxPerBlock, + totalInstanceBudget: this.maxTotalInstances, + renderDistance, + instanceUtilization: totalInstances / this.maxTotalInstances } } From 7b06561fc7359af28d9a1409225fed07dee10034 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 06:21:13 +0300 Subject: [PATCH 10/27] debug, try to fix growth, fix perf --- renderer/viewer/lib/mesher/instancingUtils.ts | 18 +- renderer/viewer/lib/mesher/models.ts | 46 +-- renderer/viewer/lib/mesher/shared.ts | 3 +- renderer/viewer/three/entities.ts | 6 +- renderer/viewer/three/holdingBlock.ts | 8 +- renderer/viewer/three/instancedRenderer.ts | 308 ++++++++++++++---- renderer/viewer/three/threeJsMedia.ts | 2 +- renderer/viewer/three/worldrendererThree.ts | 27 +- 8 files changed, 308 insertions(+), 110 deletions(-) diff --git a/renderer/viewer/lib/mesher/instancingUtils.ts b/renderer/viewer/lib/mesher/instancingUtils.ts index 56947bee..4fa211d7 100644 --- a/renderer/viewer/lib/mesher/instancingUtils.ts +++ b/renderer/viewer/lib/mesher/instancingUtils.ts @@ -4,21 +4,5 @@ import { WorldBlock as Block, World } from './world' export const isBlockInstanceable = (world: World, block: Block): boolean => { // Use dynamic instanceable blocks data if available const instancedBlocks = world?.instancedBlocks - if (Array.isArray(instancedBlocks)) { - if (!instancedBlocks.includes(block.name)) return false - } else { - return false - } - - // Check if it's actually a full cube (no rotations, no complex models) - if (!block.models || block.models.length !== 1) return false - - const model = block.models[0][0] // First variant of first model - if (!model || model.x || model.y || model.z) return false // No rotations - - // Check if all elements are full cubes - return (model.elements ?? []).every(element => { - return element.from[0] === 0 && element.from[1] === 0 && element.from[2] === 0 && - element.to[0] === 16 && element.to[1] === 16 && element.to[2] === 16 - }) + return instancedBlocks?.includes(block.name) ?? false } diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index c2b76bce..b4aa927b 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -135,7 +135,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++) { @@ -557,7 +561,7 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W // Check if instanced rendering is enabled for this section const enableInstancedRendering = instancingMode !== InstancingMode.None - const forceInstancedOnly = instancingMode === InstancingMode.TexturedInstancing // Only force when using full instancing + const forceInstancedOnly = instancingMode === InstancingMode.BlockInstancingOnly || instancingMode === InstancingMode.ColorOnly const attr: MesherGeometryOutput = { sx: sx + 8, @@ -644,11 +648,11 @@ 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)) { @@ -661,26 +665,26 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W } const blockKey = block.name - if (!attr.instancedBlocks[blockKey]) { - // Get or create block ID - const blockId = world.instancedBlockIds[block.stateId] - - if (blockId !== undefined) { - attr.instancedBlocks[blockKey] = { - blockId, - blockName: block.name, - stateId: block.stateId, - positions: [] + const blockId = world.instancedBlockIds[block.stateId] + if (blockId !== undefined) { + if (!attr.instancedBlocks[blockKey]) { + if (blockId !== undefined) { + attr.instancedBlocks[blockKey] = { + blockId, + blockName: block.name, + stateId: block.stateId, + positions: [] + } } } + attr.instancedBlocks[blockKey].positions.push({ + x: cursor.x, + y: cursor.y, + z: cursor.z + }) + attr.blocksCount++ + continue // Skip regular geometry generation for instanceable blocks } - attr.instancedBlocks[blockKey].positions.push({ - x: cursor.x, - y: cursor.y, - z: cursor.z - }) - attr.blocksCount++ - continue // Skip regular geometry generation for instanceable blocks } // Skip buffer geometry generation if force instanced only mode is enabled diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index aa6fcd84..32b2e703 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -3,7 +3,8 @@ import { BlockType } from '../../../playground/shared' export enum InstancingMode { None = 'none', ColorOnly = 'color_only', - TexturedInstancing = 'textured_instancing' + BlockInstancing = 'block_instancing', + BlockInstancingOnly = 'block_instancing_only' } // only here for easier testing diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index c05aac99..4f2e4eb3 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -1327,12 +1327,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 { diff --git a/renderer/viewer/three/holdingBlock.ts b/renderer/viewer/three/holdingBlock.ts index af355f2e..5d22c2bb 100644 --- a/renderer/viewer/three/holdingBlock.ts +++ b/renderer/viewer/three/holdingBlock.ts @@ -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 () { diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 9d45f21f..bd9ad580 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -1,7 +1,7 @@ import * as THREE from 'three' import { Vec3 } from 'vec3' import { versionToNumber } from 'flying-squid/dist/utils' -import PrismarineBlock, { Block } from 'prismarine-block' +import PrismarineBlock from 'prismarine-block' import { IndexedBlock } from 'minecraft-data' import moreBlockData from '../lib/moreBlockDataGenerated.json' import { MesherGeometryOutput } from '../lib/mesher/shared' @@ -45,24 +45,32 @@ export interface InstancedBlockModelData { export interface InstancedBlocksConfig { instanceableBlocks: Set blocksDataModel: Record - allBlocksStateIdToModelIdMap: Record + stateIdToModelIdMap: Record + blockNameToIdMap: Record interestedTextureTiles: Set } export class InstancedRenderer { private readonly instancedMeshes = new Map() + private readonly sceneUsedMeshes = new Map() private readonly blockCounts = new Map() private readonly sectionInstances = new Map>() private readonly cubeGeometry: THREE.BoxGeometry private readonly tempMatrix = new THREE.Matrix4() private readonly blockIdToName = new Map() - private readonly blockNameToId = new Map() - private nextBlockId = 0 // Dynamic instance management - private readonly baseInstancesPerBlock = 100_000 // Base instances per block type - private readonly maxTotalInstances = 10_000_000 // Total instance budget across all blocks + private readonly initialInstancesPerBlock = 15_000 // Increased initial size to reduce early resizing + private readonly maxInstancesPerBlock = 100_000 // Cap per block type + private readonly maxTotalInstances = 10_000_000 // Total instance budget private currentTotalInstances = 0 + private readonly growthFactor = 1.5 // How much to grow when needed + + // Visibility control + private _instancedMeshesVisible = true + + // Memory tracking + private totalAllocatedInstances = 0 // New properties for dynamic block detection private instancedBlocksConfig: InstancedBlocksConfig | null = null @@ -72,31 +80,176 @@ export class InstancedRenderer { this.cubeGeometry = this.createCubeGeometry() } - private getMaxInstancesPerBlock (): number { - const renderDistance = this.worldRenderer.viewDistance - if (renderDistance <= 0) return this.baseInstancesPerBlock + private getBlockId (blockName: string): number { + if (!this.instancedBlocksConfig) { + throw new Error('Instanced blocks config not prepared') + } - // Calculate dynamic limit based on render distance - // More render distance = more chunks = need more instances - const distanceFactor = Math.max(1, renderDistance / 8) // Normalize around render distance 8 - const dynamicLimit = Math.floor(this.baseInstancesPerBlock * distanceFactor) + const blockId = this.instancedBlocksConfig.blockNameToIdMap[blockName] + if (blockId === undefined) { + throw new Error(`Block ${blockName} not found in blockNameToIdMap`) + } - // Cap at reasonable limits to prevent memory issues - return Math.min(dynamicLimit, 500_000) + return blockId + } + + // 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 blockId = this.getBlockId(blockName) + const mesh = this.instancedMeshes.get(blockId) + if (!mesh) { + console.warn('Debug: Mesh not found for', blockName) + return + } + console.log('Debug: Before resize -', + 'Mesh in scene:', + this.worldRenderer.scene.children.includes(mesh), + 'Instance count:', + mesh.count, + 'Matrix count:', + mesh.instanceMatrix.count) + + const result = this.resizeInstancedMesh(blockId, mesh.instanceMatrix.count * 2) + + const newMesh = this.instancedMeshes.get(blockId) + console.log('Debug: After resize -', + 'Success:', + result, + 'New mesh in scene:', + this.worldRenderer.scene.children.includes(newMesh!), + 'Old mesh in scene:', + this.worldRenderer.scene.children.includes(mesh), + 'New count:', + newMesh?.count, + 'New matrix count:', + newMesh?.instanceMatrix.count) + } + + private resizeInstancedMesh (blockId: number, newSize: number): boolean { + const mesh = this.instancedMeshes.get(blockId) + if (!mesh) return false + + const blockName = this.blockIdToName.get(blockId) || 'unknown' + const oldSize = mesh.instanceMatrix.count + const actualInstanceCount = this.blockCounts.get(blockId) || 0 + + console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`) + console.log(`Current actual instances: ${actualInstanceCount}, Is old mesh in scene:`, this.worldRenderer.scene.children.includes(mesh)) + + 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) + } + + // Set the count to match our tracked instances + newMesh.count = actualInstanceCount + newMesh.instanceMatrix.needsUpdate = true + + // Update tracking + this.totalAllocatedInstances += (newSize - oldSize) + + // Important: Add new mesh before removing old one + this.worldRenderer.scene.add(newMesh) + console.log(`Added new mesh to scene, is present:`, this.worldRenderer.scene.children.includes(newMesh)) + + // Update our tracking to point to new mesh + this.instancedMeshes.set(blockId, newMesh) + + // Now safe to remove old mesh + this.worldRenderer.scene.remove(mesh) + console.log(`Removed old mesh from scene, still present:`, this.worldRenderer.scene.children.includes(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}`) + console.log(`Scene check - New mesh in scene: ${this.worldRenderer.scene.children.includes(newMesh)}, Old mesh in scene: ${this.worldRenderer.scene.children.includes(mesh)}`) + + if (newMesh.count !== actualInstanceCount) { + console.warn(`Instance count mismatch for ${blockName}! Mesh: ${newMesh.count}, Tracked: ${actualInstanceCount}`) + } + + return true } private canAddMoreInstances (blockId: number, count: number): boolean { const currentForBlock = this.blockCounts.get(blockId) || 0 - const maxPerBlock = this.getMaxInstancesPerBlock() + const mesh = this.instancedMeshes.get(blockId) + if (!mesh) return false - // Check per-block limit - if (currentForBlock + count > maxPerBlock) { - return false + const blockName = this.blockIdToName.get(blockId) || '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(blockId, 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}). Consider reducing render distance.`) + console.warn(`Total instance limit reached (${this.currentTotalInstances}/${this.maxTotalInstances})`) return false } @@ -114,7 +267,8 @@ export class InstancedRenderer { }) this.sharedMaterial.map = this.worldRenderer.material.map - const blocksMap = { + const { forceInstancedOnly } = this.worldRenderer.worldRendererConfig + const debugBlocksMap = forceInstancedOnly ? { 'double_stone_slab': 'stone', 'stone_slab': 'stone', 'oak_stairs': 'planks', @@ -133,7 +287,7 @@ export class InstancedRenderer { 'stone_slab2': 'stone_slab', 'purpur_stairs': 'purpur_block', 'purpur_slab': 'purpur_block', - } + } : {} const isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13') const PBlockOriginal = PrismarineBlock(this.worldRenderer.version) @@ -141,9 +295,9 @@ export class InstancedRenderer { const instanceableBlocks = new Set() const blocksDataModel = {} as Record const interestedTextureTiles = new Set() - const blocksProcessed = {} as Record - let i = 0 - const allBlocksStateIdToModelIdMap = {} as Record + const stateIdToModelIdMap = {} as Record + const blockNameToIdMap = {} as Record + let nextModelId = 0 const addBlockModel = (state: number, name: string, props: Record, mcBlockData?: IndexedBlock, defaultState = false) => { const possibleIssues = [] as string[] @@ -184,7 +338,7 @@ export class InstancedRenderer { textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 })) } - const blockId = i++ + const blockId = nextModelId++ for (const [face, { texture, cullface, rotation = 0 }] of Object.entries(elem.faces)) { const faceIndex = facesMapping.findIndex(x => x.includes(face)) if (faceIndex === -1) { @@ -207,10 +361,10 @@ export class InstancedRenderer { } } - allBlocksStateIdToModelIdMap[state] = blockId + stateIdToModelIdMap[state] = blockId blocksDataModel[blockId] = blockData instanceableBlocks.add(name) - blocksProcessed[name] = true + blockNameToIdMap[name] = blockId if (mcBlockData) { blockData.transparent = mcBlockData.transparent @@ -231,7 +385,7 @@ export class InstancedRenderer { // Process all blocks to find instanceable ones for (const b of (globalThis as any).loadedData.blocksArray) { for (let state = b.minStateId; state <= b.maxStateId; state++) { - const mapping = blocksMap[b.name] + const mapping = debugBlocksMap[b.name] const block = PBlockOriginal.fromStateId(mapping && (globalThis as any).loadedData.blocksByName[mapping] ? (globalThis as any).loadedData.blocksByName[mapping].defaultState : state, 0) if (isPreflat) { getPreflatBlock(block) @@ -239,7 +393,7 @@ export class InstancedRenderer { const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined if (textureOverride) { - const blockId = i++ + const blockId = nextModelId++ const { currentResources } = this.worldRenderer.resourcesManager if (!currentResources?.worldBlockProvider) continue const texture = currentResources.worldBlockProvider.getTextureInfo(textureOverride) @@ -248,7 +402,7 @@ export class InstancedRenderer { continue } const texIndex = texture.tileIndex - allBlocksStateIdToModelIdMap[state] = blockId + stateIdToModelIdMap[state] = blockId const blockData: InstancedBlockModelData = { textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], rotation: [0, 0, 0, 0, 0, 0], @@ -263,6 +417,7 @@ export class InstancedRenderer { blocksDataModel[blockId] = blockData instanceableBlocks.add(block.name) interestedTextureTiles.add(textureOverride) + blockNameToIdMap[block.name] = blockId continue } @@ -280,7 +435,8 @@ export class InstancedRenderer { return { instanceableBlocks, blocksDataModel, - allBlocksStateIdToModelIdMap, + stateIdToModelIdMap, + blockNameToIdMap, interestedTextureTiles } } @@ -299,6 +455,7 @@ export class InstancedRenderer { } } + // Update initializeInstancedMeshes to respect visibility setting initializeInstancedMeshes () { if (!this.instancedBlocksConfig) { console.warn('Instanced blocks config not prepared') @@ -311,6 +468,7 @@ export class InstancedRenderer { if (this.instancedMeshes.has(blockId)) continue // Skip if already exists const blockModelData = this.instancedBlocksConfig.blocksDataModel[blockId] + const initialCount = this.getInitialInstanceCount(blockName) const geometry = blockModelData ? this.createCustomGeometry(0, blockModelData) : this.cubeGeometry const material = this.createBlockMaterial(blockName) @@ -318,14 +476,16 @@ export class InstancedRenderer { const mesh = new THREE.InstancedMesh( geometry, material, - this.getMaxInstancesPerBlock() + initialCount ) mesh.name = `instanced_${blockName}` - mesh.frustumCulled = false // Important for performance + mesh.frustumCulled = false mesh.count = 0 + mesh.visible = this._instancedMeshesVisible // Set initial visibility this.instancedMeshes.set(blockId, mesh) this.worldRenderer.scene.add(mesh) + this.totalAllocatedInstances += initialCount if (!blockModelData) { console.warn(`No block model data found for block ${blockName}`) @@ -333,6 +493,19 @@ export class InstancedRenderer { } } + 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 @@ -475,7 +648,7 @@ export class InstancedRenderer { // Add new instances for this section (with limit checking) for (const pos of blockData.positions) { if (!this.canAddMoreInstances(blockId, 1)) { - console.warn(`Exceeded max instances for block ${blockName} (${currentCount + instanceIndices.length}/${this.getMaxInstancesPerBlock()})`) + console.warn(`Exceeded max instances for block ${blockName} (${currentCount + instanceIndices.length}/${this.maxInstancesPerBlock})`) break } @@ -488,10 +661,16 @@ export class InstancedRenderer { // Update tracking if (instanceIndices.length > 0) { sectionMap.set(blockId, instanceIndices) - this.blockCounts.set(blockId, currentCount + instanceIndices.length) + const newCount = currentCount + instanceIndices.length + this.blockCounts.set(blockId, newCount) this.currentTotalInstances += instanceIndices.length - mesh.count = this.blockCounts.get(blockId) || 0 + mesh.count = newCount // Ensure mesh.count matches our tracking mesh.instanceMatrix.needsUpdate = true + + // Only track mesh in sceneUsedMeshes if it's actually being used + if (newCount > 0) { + this.sceneUsedMeshes.set(blockName, mesh) + } } } } @@ -503,6 +682,12 @@ export class InstancedRenderer { // Remove instances for each block type in this section for (const [blockId, instanceIndices] of sectionMap) { this.removeInstancesFromBlock(blockId, instanceIndices) + + // Remove from sceneUsedMeshes if no instances left + const blockName = this.blockIdToName.get(blockId) + if (blockName && (this.blockCounts.get(blockId) || 0) === 0) { + this.sceneUsedMeshes.delete(blockName) + } } // Remove section from tracking @@ -557,6 +742,14 @@ export class InstancedRenderer { } } } + + // Update sceneUsedMeshes if no instances left + if (newCount === 0) { + const blockName = this.blockIdToName.get(blockId) + if (blockName) { + this.sceneUsedMeshes.delete(blockName) + } + } } isBlockInstanceable (blockName: string): boolean { @@ -593,49 +786,50 @@ export class InstancedRenderer { this.blockCounts.clear() this.sectionInstances.clear() this.blockIdToName.clear() - this.blockNameToId.clear() - this.nextBlockId = 0 + this.sceneUsedMeshes.clear() this.cubeGeometry.dispose() } + // Add visibility info to stats getStats () { let totalInstances = 0 let activeBlockTypes = 0 + let totalWastedMemory = 0 for (const [blockId, mesh] of this.instancedMeshes) { - if (mesh.count > 0) { - totalInstances += mesh.count + 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.getMaxInstancesPerBlock() + const maxPerBlock = this.maxInstancesPerBlock const renderDistance = this.worldRenderer.viewDistance return { totalInstances, activeBlockTypes, - drawCalls: activeBlockTypes, // One draw call per active block type - memoryEstimate: totalInstances * 64, // Rough estimate in bytes + 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 + instanceUtilization: totalInstances / this.maxTotalInstances, + instancedMeshesVisible: this._instancedMeshesVisible } } - private getBlockId (blockName: string): number { - // Get the block ID from our local map - let blockId = this.blockNameToId.get(blockName) - if (blockId === undefined) { - // If the block ID doesn't exist, create a new one - blockId = this.nextBlockId++ - this.blockNameToId.set(blockName, blockId) - this.blockIdToName.set(blockId, blockName) - } - return blockId - } - // New method to prepare and initialize everything prepareAndInitialize () { console.log('Preparing instanced blocks data...') diff --git a/renderer/viewer/three/threeJsMedia.ts b/renderer/viewer/three/threeJsMedia.ts index 582273d1..92599c8d 100644 --- a/renderer/viewer/three/threeJsMedia.ts +++ b/renderer/viewer/three/threeJsMedia.ts @@ -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 diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index b0f880ad..9116e289 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -38,6 +38,7 @@ export class WorldRendererThree extends WorldRendererCommon { holdingBlock: HoldingBlock holdingBlockLeft: HoldingBlock scene = new THREE.Scene() + templateScene = new THREE.Scene() ambientLight = new THREE.AmbientLight(0xcc_cc_cc) directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5) entities = new Entities(this) @@ -149,7 +150,7 @@ export class WorldRendererThree extends WorldRendererCommon { return { instanceableBlocks: config.instanceableBlocks, - allBlocksStateIdToModelIdMap: config.allBlocksStateIdToModelIdMap + allBlocksStateIdToModelIdMap: config.stateIdToModelIdMap } } @@ -157,6 +158,12 @@ 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) @@ -170,6 +177,8 @@ export class WorldRendererThree extends WorldRendererCommon { this.cameraContainer = new THREE.Object3D() this.cameraContainer.add(this.camera) this.scene.add(this.cameraContainer) + + this.resetTemplateScene() } override watchReactivePlayerState () { @@ -180,10 +189,12 @@ export class WorldRendererThree extends WorldRendererCommon { 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) @@ -757,8 +768,8 @@ 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) { @@ -955,11 +966,15 @@ export class WorldRendererThree extends WorldRendererCommon { } setSectionDirty (pos: Vec3, value = true) { - const { useInstancedRendering, enableSingleColorMode } = this.worldRendererConfig + const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly } = this.worldRendererConfig let instancingMode = InstancingMode.None - if (useInstancedRendering) { - instancingMode = enableSingleColorMode ? InstancingMode.ColorOnly : InstancingMode.TexturedInstancing + if (useInstancedRendering || enableSingleColorMode) { + instancingMode = enableSingleColorMode + ? InstancingMode.ColorOnly + : forceInstancedOnly + ? InstancingMode.BlockInstancingOnly + : InstancingMode.BlockInstancing } this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! From 3c358c9d2245d68b4f0f52069c0aadbe63977fa7 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 06:46:50 +0300 Subject: [PATCH 11/27] all done! --- renderer/viewer/three/instancedRenderer.ts | 66 ++++------------------ src/optionsGuiScheme.tsx | 44 +++++++++++++++ 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index bd9ad580..3c0efd64 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -60,7 +60,7 @@ export class InstancedRenderer { private readonly blockIdToName = new Map() // Dynamic instance management - private readonly initialInstancesPerBlock = 15_000 // Increased initial size to reduce early resizing + private readonly initialInstancesPerBlock = 2000 // Increased initial size to reduce early resizing private readonly maxInstancesPerBlock = 100_000 // Cap per block type private readonly maxTotalInstances = 10_000_000 // Total instance budget private currentTotalInstances = 0 @@ -116,32 +116,7 @@ export class InstancedRenderer { const blockName = 'grass_block' const blockId = this.getBlockId(blockName) const mesh = this.instancedMeshes.get(blockId) - if (!mesh) { - console.warn('Debug: Mesh not found for', blockName) - return - } - console.log('Debug: Before resize -', - 'Mesh in scene:', - this.worldRenderer.scene.children.includes(mesh), - 'Instance count:', - mesh.count, - 'Matrix count:', - mesh.instanceMatrix.count) - - const result = this.resizeInstancedMesh(blockId, mesh.instanceMatrix.count * 2) - - const newMesh = this.instancedMeshes.get(blockId) - console.log('Debug: After resize -', - 'Success:', - result, - 'New mesh in scene:', - this.worldRenderer.scene.children.includes(newMesh!), - 'Old mesh in scene:', - this.worldRenderer.scene.children.includes(mesh), - 'New count:', - newMesh?.count, - 'New matrix count:', - newMesh?.instanceMatrix.count) + this.resizeInstancedMesh(blockId, mesh!.instanceMatrix.count * this.growthFactor) } private resizeInstancedMesh (blockId: number, newSize: number): boolean { @@ -153,7 +128,6 @@ export class InstancedRenderer { const actualInstanceCount = this.blockCounts.get(blockId) || 0 console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`) - console.log(`Current actual instances: ${actualInstanceCount}, Is old mesh in scene:`, this.worldRenderer.scene.children.includes(mesh)) const { geometry } = mesh const { material } = mesh @@ -175,23 +149,14 @@ export class InstancedRenderer { newMesh.setMatrixAt(i, this.tempMatrix) } - // Set the count to match our tracked instances newMesh.count = actualInstanceCount newMesh.instanceMatrix.needsUpdate = true - // Update tracking this.totalAllocatedInstances += (newSize - oldSize) - // Important: Add new mesh before removing old one this.worldRenderer.scene.add(newMesh) - console.log(`Added new mesh to scene, is present:`, this.worldRenderer.scene.children.includes(newMesh)) - - // Update our tracking to point to new mesh this.instancedMeshes.set(blockId, newMesh) - - // Now safe to remove old mesh this.worldRenderer.scene.remove(mesh) - console.log(`Removed old mesh from scene, still present:`, this.worldRenderer.scene.children.includes(mesh)) // Clean up old mesh mesh.geometry.dispose() @@ -203,11 +168,6 @@ export class InstancedRenderer { // Verify instance count matches console.log(`Finished growing ${blockName}. Actual instances: ${actualInstanceCount}, New capacity: ${newSize}, Mesh count: ${newMesh.count}`) - console.log(`Scene check - New mesh in scene: ${this.worldRenderer.scene.children.includes(newMesh)}, Old mesh in scene: ${this.worldRenderer.scene.children.includes(mesh)}`) - - if (newMesh.count !== actualInstanceCount) { - console.warn(`Instance count mismatch for ${blockName}! Mesh: ${newMesh.count}, Tracked: ${actualInstanceCount}`) - } return true } @@ -636,22 +596,20 @@ export class InstancedRenderer { const { blockId, stateId } = blockData this.blockIdToName.set(blockId, blockName) - const mesh = this.instancedMeshes.get(blockId) - if (!mesh) { - console.warn(`Failed to find mesh for block ${blockName}`) - continue - } - const instanceIndices: number[] = [] const currentCount = this.blockCounts.get(blockId) || 0 - // Add new instances for this section (with limit checking) - for (const pos of blockData.positions) { - if (!this.canAddMoreInstances(blockId, 1)) { - console.warn(`Exceeded max instances for block ${blockName} (${currentCount + instanceIndices.length}/${this.maxInstancesPerBlock})`) - break - } + // Check if we can add all positions at once + const neededInstances = blockData.positions.length + if (!this.canAddMoreInstances(blockId, neededInstances)) { + console.warn(`Cannot add ${neededInstances} instances for block ${blockName} (current: ${currentCount}, max: ${this.maxInstancesPerBlock})`) + continue + } + const mesh = this.instancedMeshes.get(blockId)! + + // Add new instances for this section + for (const pos of blockData.positions) { const instanceIndex = currentCount + instanceIndices.length this.tempMatrix.setPosition(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5) mesh.setMatrixAt(instanceIndex, this.tempMatrix) diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index b03db37d..1beadb07 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -114,6 +114,50 @@ 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 + }, + }, { custom () { const { _renderByChunks } = useSnapshot(options).rendererSharedOptions From 336aad678bddac9bea83b2087b59e190f74151e4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 07:52:17 +0300 Subject: [PATCH 12/27] real --- README.MD | 1 - renderer/viewer/lib/mesher/mesher.ts | 3 +-- renderer/viewer/three/entities.ts | 1 + renderer/viewer/three/instancedRenderer.ts | 27 ++++++++++++++++++--- renderer/viewer/three/worldrendererThree.ts | 6 ++--- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/README.MD b/README.MD index 61a5b733..9bae3e56 100644 --- a/README.MD +++ b/README.MD @@ -39,7 +39,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) diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index f781275c..daf0b8ab 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -68,8 +68,7 @@ function setSectionDirty (pos, value = true, instancingMode = InstancingMode.Non const softCleanup = () => { // clean block cache and loaded chunks - world = new World(world.config.version) - globalThis.world = world + world.blockCache = {} } const handleMessage = data => { diff --git a/renderer/viewer/three/entities.ts b/renderer/viewer/three/entities.ts index 4f2e4eb3..7e04370f 100644 --- a/renderer/viewer/three/entities.ts +++ b/renderer/viewer/three/entities.ts @@ -730,6 +730,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, diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 3c0efd64..4c149147 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -34,6 +34,7 @@ export interface InstancedSectionData { } export interface InstancedBlockModelData { + stateId: number textures: number[] rotation: number[] transparent?: boolean @@ -51,6 +52,7 @@ export interface InstancedBlocksConfig { } export class InstancedRenderer { + USE_APP_GEOMETRY = true private readonly instancedMeshes = new Map() private readonly sceneUsedMeshes = new Map() private readonly blockCounts = new Map() @@ -78,6 +80,7 @@ export class InstancedRenderer { constructor (private readonly worldRenderer: WorldRendererThree) { this.cubeGeometry = this.createCubeGeometry() + // Initialize world origin at camera position } private getBlockId (blockName: string): number { @@ -293,6 +296,7 @@ export class InstancedRenderer { ] const blockData: InstancedBlockModelData = { + stateId: state, textures: [0, 0, 0, 0, 0, 0], rotation: [0, 0, 0, 0, 0, 0], textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 })) @@ -364,6 +368,7 @@ export class InstancedRenderer { const texIndex = texture.tileIndex stateIdToModelIdMap[state] = blockId const blockData: InstancedBlockModelData = { + stateId: state, textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], rotation: [0, 0, 0, 0, 0, 0], filterLight: b.filterLight, @@ -425,12 +430,13 @@ export class InstancedRenderer { // Create InstancedMesh for each instanceable block type for (const blockName of this.instancedBlocksConfig.instanceableBlocks) { const blockId = this.getBlockId(blockName) + const { stateId } = this.instancedBlocksConfig.blocksDataModel[blockId] if (this.instancedMeshes.has(blockId)) continue // Skip if already exists const blockModelData = this.instancedBlocksConfig.blocksDataModel[blockId] const initialCount = this.getInitialInstanceCount(blockName) - const geometry = blockModelData ? this.createCustomGeometry(0, blockModelData) : this.cubeGeometry + const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry const material = this.createBlockMaterial(blockName) const mesh = new THREE.InstancedMesh( @@ -475,6 +481,14 @@ export class InstancedRenderer { } private createCustomGeometry (stateId: number, blockModelData: InstancedBlockModelData): THREE.BufferGeometry { + if (this.USE_APP_GEOMETRY) { + const itemMesh = this.worldRenderer.entities.getItemMesh({ + blockState: stateId + }, {}) + + return itemMesh?.meshGeometry + } + // Create custom geometry with specific UV coordinates per face const geometry = new THREE.BoxGeometry(1, 1, 1) @@ -525,7 +539,9 @@ export class InstancedRenderer { const faceUVs = uvs.slice(faceUvStart, faceUvStart + 8) // Apply rotation if needed (0=0°, 1=90°, 2=180°, 3=270°) - if (rotation > 0) { + // 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[] = [] @@ -539,7 +555,7 @@ export class InstancedRenderer { // Rotate vertices const rotatedVertices: UVVertex[] = [] for (let i = 0; i < 4; i++) { - const srcIndex = (i + rotation) % 4 + const srcIndex = (i + totalRotation) % 4 rotatedVertices.push(vertices[srcIndex]) } @@ -611,7 +627,10 @@ export class InstancedRenderer { // Add new instances for this section for (const pos of blockData.positions) { const instanceIndex = currentCount + instanceIndices.length - this.tempMatrix.setPosition(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5) + const offset = this.USE_APP_GEOMETRY ? 0 : 0.5 + + // Calculate position relative to world origin + this.tempMatrix.makeTranslation(pos.x + offset, pos.y + offset, pos.z + offset) mesh.setMatrixAt(instanceIndex, this.tempMatrix) instanceIndices.push(instanceIndex) } diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 9116e289..849c432d 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -50,7 +50,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraShake: CameraShake cameraContainer: THREE.Object3D media: ThreeJsMedia - instancedRenderer: InstancedRenderer + instancedRenderer: InstancedRenderer | undefined waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } camera: THREE.PerspectiveCamera renderTimeAvg = 0 @@ -496,7 +496,7 @@ export class WorldRendererThree extends WorldRendererCommon { return worldPos } - getWorldCameraPosition () { + getSectionCameraPosition () { const pos = this.getCameraPosition() return new Vec3( Math.floor(pos.x / 16), @@ -506,7 +506,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() From dbbe5445d8aebbdbd7d5c26ff2411ce6cbe18f46 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 07:55:29 +0300 Subject: [PATCH 13/27] realScene test --- renderer/viewer/three/worldrendererThree.ts | 83 ++++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 849c432d..13eba9b1 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -35,9 +35,10 @@ export class WorldRendererThree extends WorldRendererCommon { signsCache = new Map() 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) @@ -76,6 +77,7 @@ export class WorldRendererThree extends WorldRendererCommon { private currentPosTween?: tweenJs.Tween 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) @@ -114,6 +116,12 @@ export class WorldRendererThree extends WorldRendererCommon { 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 () { return this.cameraGroupVr ?? this.cameraContainer } @@ -145,7 +153,7 @@ export class WorldRendererThree extends WorldRendererCommon { } getInstancedBlocksData () { - const config = this.instancedRenderer.getInstancedBlocksConfig() + const config = this.instancedRenderer?.getInstancedBlocksConfig() if (!config) return undefined return { @@ -166,17 +174,18 @@ export class WorldRendererThree extends WorldRendererCommon { 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() } @@ -184,7 +193,7 @@ export class WorldRendererThree extends WorldRendererCommon { 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 @@ -220,9 +229,9 @@ export class WorldRendererThree extends WorldRendererCommon { changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) { const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock if (isAnimationPlaying) { - holdingBlock.startSwing() + holdingBlock?.startSwing() } else { - holdingBlock.stopSwing() + holdingBlock?.stopSwing() } } @@ -250,7 +259,7 @@ export class WorldRendererThree extends WorldRendererCommon { } // Prepare and initialize instanced renderer with dynamic block detection - this.instancedRenderer.prepareAndInitialize() + this.instancedRenderer?.prepareAndInitialize() await super.updateAssetsData() this.onAllTexturesLoaded() @@ -262,14 +271,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 { @@ -323,15 +336,17 @@ export class WorldRendererThree extends WorldRendererCommon { const formatBigNumber = (num: number) => { return new Intl.NumberFormat('en-US', {}).format(num) } - const instancedStats = this.instancedRenderer.getStats() + 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 += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t ` - text += `DC: ${formatBigNumber(instancedStats.drawCalls)}` + if (instancedStats) { + text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t ` + text += `DC: ${formatBigNumber(instancedStats.drawCalls)}` + } pane.updateText(text) this.backendInfoReport = text } @@ -382,11 +397,11 @@ export class WorldRendererThree extends WorldRendererCommon { const chunkKey = chunkCoords[0] + ',' + chunkCoords[2] // Always clear old instanced blocks for this section first, then add new ones if any - this.instancedRenderer.removeSectionInstances(data.key) + this.instancedRenderer?.removeSectionInstances(data.key) // Handle instanced blocks data from worker if (data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0) { - this.instancedRenderer.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key) + this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key) } let object: THREE.Object3D = this.sectionObjects[data.key] @@ -493,7 +508,8 @@ export class WorldRendererThree extends WorldRendererCommon { 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) } getSectionCameraPosition () { @@ -516,6 +532,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() @@ -759,7 +786,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 && @@ -768,8 +795,8 @@ export class WorldRendererThree extends WorldRendererCommon { // !this.freeFlyMode && !this.renderer.xr.isPresenting ) { - this.holdingBlock.render(this.renderer) - this.holdingBlockLeft.render(this.renderer) + this.holdingBlock?.render(this.renderer) + this.holdingBlockLeft?.render(this.renderer) } for (const fountain of this.fountains) { @@ -954,7 +981,7 @@ export class WorldRendererThree extends WorldRendererCommon { const key = `${x},${y},${z}` // Remove instanced blocks for this section - this.instancedRenderer.removeSectionInstances(key) + this.instancedRenderer?.removeSectionInstances(key) const mesh = this.sectionObjects[key] if (mesh) { @@ -995,7 +1022,7 @@ export class WorldRendererThree extends WorldRendererCommon { } destroy (): void { - this.instancedRenderer.destroy() + this.instancedRenderer?.destroy() super.destroy() } From 6868068705026c5f6c0099b92aeb162e31b17bcf Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 08:41:05 +0300 Subject: [PATCH 14/27] fixes --- renderer/viewer/three/instancedRenderer.ts | 3 +-- renderer/viewer/three/panorama.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 4c149147..7c5ae7d8 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -629,8 +629,7 @@ export class InstancedRenderer { const instanceIndex = currentCount + instanceIndices.length const offset = this.USE_APP_GEOMETRY ? 0 : 0.5 - // Calculate position relative to world origin - this.tempMatrix.makeTranslation(pos.x + offset, pos.y + offset, pos.z + offset) + this.tempMatrix.setPosition(pos.x + offset, pos.y + offset, pos.z + offset) mesh.setMatrixAt(instanceIndex, this.tempMatrix) instanceIndices.push(instanceIndex) } diff --git a/renderer/viewer/three/panorama.ts b/renderer/viewer/three/panorama.ts index 7e975d4b..0037ff1d 100644 --- a/renderer/viewer/three/panorama.ts +++ b/renderer/viewer/three/panorama.ts @@ -203,7 +203,7 @@ export class PanoramaRenderer { } ) if (this.worldRenderer instanceof WorldRendererThree) { - this.scene = this.worldRenderer.scene + this.scene = this.worldRenderer.realScene } void worldView.init(initPos) From 83018cd828e11bd616a8adfc62445fb19b9e0e46 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 09:12:56 +0300 Subject: [PATCH 15/27] [before test] refactor to use state id, force! --- renderer/viewer/lib/mesher/instancingUtils.ts | 6 +- renderer/viewer/lib/mesher/models.ts | 33 +- renderer/viewer/lib/mesher/shared.ts | 3 +- renderer/viewer/lib/mesher/world.ts | 4 +- renderer/viewer/three/instancedRenderer.ts | 387 +++++++++--------- 5 files changed, 212 insertions(+), 221 deletions(-) diff --git a/renderer/viewer/lib/mesher/instancingUtils.ts b/renderer/viewer/lib/mesher/instancingUtils.ts index 4fa211d7..ff4bdd10 100644 --- a/renderer/viewer/lib/mesher/instancingUtils.ts +++ b/renderer/viewer/lib/mesher/instancingUtils.ts @@ -1,8 +1,8 @@ import { WorldBlock as Block, World } from './world' -// Returns true if the block is instanceable (full cube, no rotations, etc.) export const isBlockInstanceable = (world: World, block: Block): boolean => { - // Use dynamic instanceable blocks data if available const instancedBlocks = world?.instancedBlocks - return instancedBlocks?.includes(block.name) ?? false + if (!instancedBlocks) return false + + return instancedBlocks[block.stateId] !== undefined } diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index b4aa927b..e918d853 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -657,34 +657,29 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W } 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)) { + 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 - const blockId = world.instancedBlockIds[block.stateId] - if (blockId !== undefined) { - if (!attr.instancedBlocks[blockKey]) { - if (blockId !== undefined) { - attr.instancedBlocks[blockKey] = { - blockId, - blockName: block.name, - stateId: block.stateId, - positions: [] - } - } + if (!attr.instancedBlocks[blockKey]) { + attr.instancedBlocks[blockKey] = { + stateId: block.stateId, + blockName: block.name, + positions: [] } - attr.instancedBlocks[blockKey].positions.push({ - x: cursor.x, - y: cursor.y, - z: cursor.z - }) - attr.blocksCount++ - continue // Skip regular geometry generation for instanceable blocks } + attr.instancedBlocks[blockKey].positions.push({ + x: cursor.x, + y: cursor.y, + z: cursor.z + }) + attr.blocksCount++ + continue // Skip regular geometry generation for instanceable blocks } // Skip buffer geometry generation if force instanced only mode is enabled diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 32b2e703..11ca4ec2 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -29,9 +29,8 @@ export type CustomBlockModels = { export type MesherConfig = typeof defaultMesherConfig export type InstancedBlockEntry = { - blockId: number // Unique ID for this block type - blockName: string stateId: number + blockName: string positions: Array<{ x: number, y: number, z: number }> } diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts index bcff11b7..e02a502a 100644 --- a/renderer/viewer/lib/mesher/world.ts +++ b/renderer/viewer/lib/mesher/world.ts @@ -42,8 +42,8 @@ export class World { customBlockModels = new Map() // chunkKey -> blockModels sentBlockStateModels = new Set() blockStateModelInfo = new Map() - instancedBlocks: Record = {} - instancedBlockIds = {} as Record + instancedBlocks: Record = {} + instancedBlockIds = {} as Record constructor (version: string) { this.Chunk = Chunks(version) as any diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 7c5ae7d8..29a7ff2b 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -21,37 +21,37 @@ function parseRgbColor (rgbString: string): number { } export interface InstancedBlockData { - blockId: number + stateId: number positions: Vec3[] blockName: string - stateId: number } export interface InstancedSectionData { sectionKey: string - instancedBlocks: Map + instancedBlocks: Map shouldUseInstancedOnly: boolean } export interface InstancedBlockModelData { stateId: number - textures: number[] + // textures: number[] rotation: number[] transparent?: boolean emitLight?: number filterLight?: number - textureInfos?: Array<{ u: number, v: number, su: number, sv: number }> // Store texture info for each face + textureInfos?: Array<{ u: number, v: number, su: number, sv: number }> } export interface InstancedBlocksConfig { instanceableBlocks: Set - blocksDataModel: Record - stateIdToModelIdMap: Record - blockNameToIdMap: Record + blocksDataModel: Record + blockNameToStateIdMap: Record interestedTextureTiles: Set } export class InstancedRenderer { + isPreflat: boolean + USE_APP_GEOMETRY = true private readonly instancedMeshes = new Map() private readonly sceneUsedMeshes = new Map() @@ -59,12 +59,12 @@ export class InstancedRenderer { private readonly sectionInstances = new Map>() private readonly cubeGeometry: THREE.BoxGeometry private readonly tempMatrix = new THREE.Matrix4() - private readonly blockIdToName = new Map() + private readonly stateIdToName = new Map() // Dynamic instance management - private readonly initialInstancesPerBlock = 2000 // Increased initial size to reduce early resizing - private readonly maxInstancesPerBlock = 100_000 // Cap per block type - private readonly maxTotalInstances = 10_000_000 // Total instance budget + 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 @@ -74,26 +74,25 @@ export class InstancedRenderer { // Memory tracking private totalAllocatedInstances = 0 - // New properties for dynamic block detection private instancedBlocksConfig: InstancedBlocksConfig | null = null private sharedMaterial: THREE.MeshLambertMaterial | null = null constructor (private readonly worldRenderer: WorldRendererThree) { this.cubeGeometry = this.createCubeGeometry() - // Initialize world origin at camera position + this.isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13') } - private getBlockId (blockName: string): number { + private getStateId (blockName: string): number { if (!this.instancedBlocksConfig) { throw new Error('Instanced blocks config not prepared') } - const blockId = this.instancedBlocksConfig.blockNameToIdMap[blockName] - if (blockId === undefined) { - throw new Error(`Block ${blockName} not found in blockNameToIdMap`) + const stateId = this.instancedBlocksConfig.blockNameToStateIdMap[blockName] + if (stateId === undefined) { + throw new Error(`Block ${blockName} not found in blockNameToStateIdMap`) } - return blockId + return stateId } // Add getter/setter for visibility @@ -117,20 +116,20 @@ export class InstancedRenderer { debugResizeMesh () { // Debug helper to test resize operation const blockName = 'grass_block' - const blockId = this.getBlockId(blockName) - const mesh = this.instancedMeshes.get(blockId) - this.resizeInstancedMesh(blockId, mesh!.instanceMatrix.count * this.growthFactor) + const stateId = this.getStateId(blockName) + const mesh = this.instancedMeshes.get(stateId) + this.resizeInstancedMesh(stateId, mesh!.instanceMatrix.count * this.growthFactor) } - private resizeInstancedMesh (blockId: number, newSize: number): boolean { - const mesh = this.instancedMeshes.get(blockId) + private resizeInstancedMesh (stateId: number, newSize: number): boolean { + const mesh = this.instancedMeshes.get(stateId) if (!mesh) return false - const blockName = this.blockIdToName.get(blockId) || 'unknown' + const blockName = this.stateIdToName.get(stateId) || 'unknown' const oldSize = mesh.instanceMatrix.count - const actualInstanceCount = this.blockCounts.get(blockId) || 0 + const actualInstanceCount = this.blockCounts.get(stateId) || 0 - console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`) + // console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`) const { geometry } = mesh const { material } = mesh @@ -158,7 +157,7 @@ export class InstancedRenderer { this.totalAllocatedInstances += (newSize - oldSize) this.worldRenderer.scene.add(newMesh) - this.instancedMeshes.set(blockId, newMesh) + this.instancedMeshes.set(stateId, newMesh) this.worldRenderer.scene.remove(mesh) // Clean up old mesh @@ -175,12 +174,12 @@ export class InstancedRenderer { return true } - private canAddMoreInstances (blockId: number, count: number): boolean { - const currentForBlock = this.blockCounts.get(blockId) || 0 - const mesh = this.instancedMeshes.get(blockId) + 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.blockIdToName.get(blockId) || 'unknown' + const blockName = this.stateIdToName.get(stateId) || 'unknown' // If we would exceed current capacity, try to grow if (currentForBlock + count > mesh.instanceMatrix.count) { @@ -204,7 +203,7 @@ export class InstancedRenderer { } // Try to grow - if (!this.resizeInstancedMesh(blockId, newSize)) { + if (!this.resizeInstancedMesh(stateId, newSize)) { console.warn(`Failed to grow instances for ${blockName}`) return false } @@ -219,7 +218,80 @@ export class InstancedRenderer { return true } - prepareInstancedBlocksData (): InstancedBlocksConfig { + prepareInstancedBlock (stateId: number, name: string, props: Record, 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(name) + config.blockNameToStateIdMap[name] = stateId + + if (mcBlockData) { + blockData.transparent = mcBlockData.transparent + blockData.emitLight = mcBlockData.emitLight + blockData.filterLight = mcBlockData.filterLight + } + } + + prepareInstancedBlocksData () { if (this.sharedMaterial) { this.sharedMaterial.dispose() this.sharedMaterial = null @@ -252,93 +324,17 @@ export class InstancedRenderer { 'purpur_slab': 'purpur_block', } : {} - const isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13') const PBlockOriginal = PrismarineBlock(this.worldRenderer.version) - const instanceableBlocks = new Set() - const blocksDataModel = {} as Record - const interestedTextureTiles = new Set() - const stateIdToModelIdMap = {} as Record - const blockNameToIdMap = {} as Record - let nextModelId = 0 - - const addBlockModel = (state: number, name: string, props: Record, mcBlockData?: IndexedBlock, defaultState = false) => { - const possibleIssues = [] as string[] - const { currentResources } = this.worldRenderer.resourcesManager - if (!currentResources?.worldBlockProvider) return - - const models = currentResources.worldBlockProvider.getAllResolvedModels0_1({ - name, - properties: props - }, 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: state, - textures: [0, 0, 0, 0, 0, 0], - rotation: [0, 0, 0, 0, 0, 0], - textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 })) - } - - const blockId = nextModelId++ - 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.textures[faceIndex] = texture.tileIndex - blockData.rotation[faceIndex] = rotation / 90 - if (Math.floor(blockData.rotation[faceIndex]) !== blockData.rotation[faceIndex]) { - throw new Error(`Invalid rotation ${rotation} ${name}`) - } - 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 - } - } - - stateIdToModelIdMap[state] = blockId - blocksDataModel[blockId] = blockData - instanceableBlocks.add(name) - blockNameToIdMap[name] = blockId - - if (mcBlockData) { - blockData.transparent = mcBlockData.transparent - blockData.emitLight = mcBlockData.emitLight - blockData.filterLight = mcBlockData.filterLight - } + this.instancedBlocksConfig = { + instanceableBlocks: new Set(), + blocksDataModel: {} as Record, + blockNameToStateIdMap: {} as Record, + interestedTextureTiles: new Set(), } // Add unknown block model - addBlockModel(-1, 'unknown', {}) + this.prepareInstancedBlock(-1, 'unknown', {}) // Handle texture overrides for special blocks const textureOverrideFullBlocks = { @@ -347,17 +343,18 @@ export class InstancedRenderer { } // Process all blocks to find instanceable ones - for (const b of (globalThis as any).loadedData.blocksArray) { - for (let state = b.minStateId; state <= b.maxStateId; state++) { + 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 && (globalThis as any).loadedData.blocksByName[mapping] ? (globalThis as any).loadedData.blocksByName[mapping].defaultState : state, 0) - if (isPreflat) { + 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 blockId = nextModelId++ const { currentResources } = this.worldRenderer.resourcesManager if (!currentResources?.worldBlockProvider) continue const texture = currentResources.worldBlockProvider.getTextureInfo(textureOverride) @@ -366,10 +363,8 @@ export class InstancedRenderer { continue } const texIndex = texture.tileIndex - stateIdToModelIdMap[state] = blockId - const blockData: InstancedBlockModelData = { - stateId: state, - textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], + config.blocksDataModel[stateId] = { + stateId, rotation: [0, 0, 0, 0, 0, 0], filterLight: b.filterLight, textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ @@ -379,10 +374,9 @@ export class InstancedRenderer { sv: texture.sv })) } - blocksDataModel[blockId] = blockData - instanceableBlocks.add(block.name) - interestedTextureTiles.add(textureOverride) - blockNameToIdMap[block.name] = blockId + config.instanceableBlocks.add(block.name) + config.interestedTextureTiles.add(textureOverride) + config.blockNameToStateIdMap[block.name] = stateId continue } @@ -393,17 +387,9 @@ export class InstancedRenderer { continue } - addBlockModel(state, block.name, block.getProperties(), b, state === b.defaultState) + this.prepareInstancedBlock(stateId, block.name, block.getProperties(), b, stateId === b.defaultState) } } - - return { - instanceableBlocks, - blocksDataModel, - stateIdToModelIdMap, - blockNameToIdMap, - interestedTextureTiles - } } private createBlockMaterial (blockName: string): THREE.Material { @@ -429,33 +415,40 @@ export class InstancedRenderer { // Create InstancedMesh for each instanceable block type for (const blockName of this.instancedBlocksConfig.instanceableBlocks) { - const blockId = this.getBlockId(blockName) - const { stateId } = this.instancedBlocksConfig.blocksDataModel[blockId] - if (this.instancedMeshes.has(blockId)) continue // Skip if already exists + const stateId = this.getStateId(blockName) + this.initializeInstancedMesh(stateId, blockName) + } + } - const blockModelData = this.instancedBlocksConfig.blocksDataModel[blockId] - const initialCount = this.getInitialInstanceCount(blockName) + initializeInstancedMesh (stateId: number, blockName: string) { + if (this.instancedMeshes.has(stateId)) return // Skip if already exists - const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry - const material = this.createBlockMaterial(blockName) + if (!this.instancedBlocksConfig!.blocksDataModel) { + this.prepareInstancedBlock(stateId, blockName, {}) + } - 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 + const blockModelData = this.instancedBlocksConfig!.blocksDataModel[stateId] + const initialCount = this.getInitialInstanceCount(blockName) - this.instancedMeshes.set(blockId, mesh) - this.worldRenderer.scene.add(mesh) - this.totalAllocatedInstances += initialCount + const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry + const material = this.createBlockMaterial(blockName) - if (!blockModelData) { - console.warn(`No block model data found for block ${blockName}`) - } + 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 + + this.instancedMeshes.set(stateId, mesh) + this.worldRenderer.scene.add(mesh) + this.totalAllocatedInstances += initialCount + + if (!blockModelData) { + console.warn(`No block model data found for block ${blockName}`) } } @@ -482,7 +475,9 @@ export class InstancedRenderer { private createCustomGeometry (stateId: number, blockModelData: InstancedBlockModelData): THREE.BufferGeometry { if (this.USE_APP_GEOMETRY) { - const itemMesh = this.worldRenderer.entities.getItemMesh({ + const itemMesh = this.worldRenderer.entities.getItemMesh(stateId === -1 ? { + name: 'unknown' + } : { blockState: stateId }, {}) @@ -596,33 +591,36 @@ export class InstancedRenderer { const sectionMap = this.sectionInstances.get(sectionKey)! // Remove old instances for blocks that are being updated - const previousBlockIds = [...sectionMap.keys()] - for (const blockId of previousBlockIds) { - const instanceIndices = sectionMap.get(blockId) + const previousStateIds = [...sectionMap.keys()] + // TODO stress test updates, maybe need more effective way to remove old instances + for (const stateId of previousStateIds) { + const instanceIndices = sectionMap.get(stateId) if (instanceIndices) { - this.removeInstancesFromBlock(blockId, instanceIndices) - sectionMap.delete(blockId) + this.removeInstancesFromBlock(stateId, instanceIndices) + sectionMap.delete(stateId) } } // Keep track of blocks that were updated this frame for (const [blockName, blockData] of Object.entries(instancedBlocks)) { - if (!this.isBlockInstanceable(blockName)) continue + const { stateId } = blockData + this.stateIdToName.set(stateId, blockName) - const { blockId, stateId } = blockData - this.blockIdToName.set(blockId, blockName) + if (this.USE_APP_GEOMETRY) { // only dynamically initialize meshes for app geometry + this.initializeInstancedMesh(stateId, blockName) + } const instanceIndices: number[] = [] - const currentCount = this.blockCounts.get(blockId) || 0 + const currentCount = this.blockCounts.get(stateId) || 0 // Check if we can add all positions at once const neededInstances = blockData.positions.length - if (!this.canAddMoreInstances(blockId, neededInstances)) { + 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(blockId)! + const mesh = this.instancedMeshes.get(stateId)! // Add new instances for this section for (const pos of blockData.positions) { @@ -636,9 +634,9 @@ export class InstancedRenderer { // Update tracking if (instanceIndices.length > 0) { - sectionMap.set(blockId, instanceIndices) + sectionMap.set(stateId, instanceIndices) const newCount = currentCount + instanceIndices.length - this.blockCounts.set(blockId, newCount) + this.blockCounts.set(stateId, newCount) this.currentTotalInstances += instanceIndices.length mesh.count = newCount // Ensure mesh.count matches our tracking mesh.instanceMatrix.needsUpdate = true @@ -656,12 +654,12 @@ export class InstancedRenderer { if (!sectionMap) return // Section not tracked // Remove instances for each block type in this section - for (const [blockId, instanceIndices] of sectionMap) { - this.removeInstancesFromBlock(blockId, instanceIndices) + for (const [stateId, instanceIndices] of sectionMap) { + this.removeInstancesFromBlock(stateId, instanceIndices) // Remove from sceneUsedMeshes if no instances left - const blockName = this.blockIdToName.get(blockId) - if (blockName && (this.blockCounts.get(blockId) || 0) === 0) { + const blockName = this.stateIdToName.get(stateId) + if (blockName && (this.blockCounts.get(stateId) || 0) === 0) { this.sceneUsedMeshes.delete(blockName) } } @@ -670,11 +668,11 @@ export class InstancedRenderer { this.sectionInstances.delete(sectionKey) } - private removeInstancesFromBlock (blockId: number, indicesToRemove: number[]) { - const mesh = this.instancedMeshes.get(blockId) + private removeInstancesFromBlock (stateId: number, indicesToRemove: number[]) { + const mesh = this.instancedMeshes.get(stateId) if (!mesh || indicesToRemove.length === 0) return - const currentCount = this.blockCounts.get(blockId) || 0 + const currentCount = this.blockCounts.get(stateId) || 0 const removeSet = new Set(indicesToRemove) // Update total instance count @@ -699,29 +697,29 @@ export class InstancedRenderer { // Update count const newCount = writeIndex - this.blockCounts.set(blockId, newCount) + 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(blockId) + 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(blockId, updatedIndices) + sectionMap.set(stateId, updatedIndices) } else { - sectionMap.delete(blockId) + sectionMap.delete(stateId) } } } // Update sceneUsedMeshes if no instances left if (newCount === 0) { - const blockName = this.blockIdToName.get(blockId) + const blockName = this.stateIdToName.get(stateId) if (blockName) { this.sceneUsedMeshes.delete(blockName) } @@ -736,12 +734,12 @@ export class InstancedRenderer { // Reset total instance count since we're clearing everything this.currentTotalInstances = 0 - for (const [blockId, mesh] of this.instancedMeshes) { + 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(blockId) + this.instancedMeshes.delete(stateId) this.worldRenderer.scene.remove(mesh) } @@ -751,7 +749,7 @@ export class InstancedRenderer { destroy () { // Clean up resources - for (const [blockId, mesh] of this.instancedMeshes) { + for (const [stateId, mesh] of this.instancedMeshes) { this.worldRenderer.scene.remove(mesh) mesh.geometry.dispose() if (mesh.material instanceof THREE.Material) { @@ -761,7 +759,7 @@ export class InstancedRenderer { this.instancedMeshes.clear() this.blockCounts.clear() this.sectionInstances.clear() - this.blockIdToName.clear() + this.stateIdToName.clear() this.sceneUsedMeshes.clear() this.cubeGeometry.dispose() } @@ -772,7 +770,7 @@ export class InstancedRenderer { let activeBlockTypes = 0 let totalWastedMemory = 0 - for (const [blockId, mesh] of this.instancedMeshes) { + 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) @@ -809,10 +807,9 @@ export class InstancedRenderer { // New method to prepare and initialize everything prepareAndInitialize () { console.log('Preparing instanced blocks data...') - this.instancedBlocksConfig = this.prepareInstancedBlocksData() - console.log(`Found ${this.instancedBlocksConfig.instanceableBlocks.size} instanceable blocks:`, - [...this.instancedBlocksConfig.instanceableBlocks].slice(0, 10).join(', '), - this.instancedBlocksConfig.instanceableBlocks.size > 10 ? '...' : '') + this.prepareInstancedBlocksData() + const config = this.instancedBlocksConfig! + console.log(`Found ${config.instanceableBlocks.size} instanceable blocks`) this.disposeOldMeshes() this.initializeInstancedMeshes() From 5e30a4736edc379be7785e7d6cdb7dc27e3d27d4 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 09:14:04 +0300 Subject: [PATCH 16/27] rm cache --- renderer/viewer/lib/mesher/models.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index e918d853..d902e776 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -13,9 +13,6 @@ let blockProvider: WorldBlockProvider const tints: any = {} let needTiles = false -// Cache for texture info to avoid repeated calculations -const textureInfoCache = new Map() - let tintsData try { tintsData = require('esbuild-data').tints From b2257d8ae48628f4159f92be6b14e8ef51902f17 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 16 Jul 2025 09:16:06 +0300 Subject: [PATCH 17/27] rm unused code --- renderer/viewer/three/worldrendererThree.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 13eba9b1..245b03e5 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -152,16 +152,6 @@ export class WorldRendererThree extends WorldRendererCommon { } } - getInstancedBlocksData () { - const config = this.instancedRenderer?.getInstancedBlocksConfig() - if (!config) return undefined - - return { - instanceableBlocks: config.instanceableBlocks, - allBlocksStateIdToModelIdMap: config.stateIdToModelIdMap - } - } - updatePlayerEntity (e: any) { this.entities.handlePlayerEntity(e) } From 1c8799242a11d9449b219299718cd7b522860a21 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 19 Jul 2025 18:10:09 +0300 Subject: [PATCH 18/27] some important fixes --- renderer/viewer/lib/mesher/instancingUtils.ts | 3 +- renderer/viewer/lib/mesher/mesher.ts | 53 ++++++++++++++++- renderer/viewer/lib/mesher/world.ts | 2 +- renderer/viewer/lib/worldrendererCommon.ts | 5 +- renderer/viewer/three/instancedRenderer.ts | 58 ++++++++++--------- renderer/viewer/three/worldrendererThree.ts | 53 ++++++++++++++--- src/index.ts | 4 ++ 7 files changed, 138 insertions(+), 40 deletions(-) diff --git a/renderer/viewer/lib/mesher/instancingUtils.ts b/renderer/viewer/lib/mesher/instancingUtils.ts index ff4bdd10..7e5cc2a1 100644 --- a/renderer/viewer/lib/mesher/instancingUtils.ts +++ b/renderer/viewer/lib/mesher/instancingUtils.ts @@ -3,6 +3,5 @@ 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[block.stateId] !== undefined + return instancedBlocks.includes(block.stateId) } diff --git a/renderer/viewer/lib/mesher/mesher.ts b/renderer/viewer/lib/mesher/mesher.ts index daf0b8ab..abc4d44d 100644 --- a/renderer/viewer/lib/mesher/mesher.ts +++ b/renderer/viewer/lib/mesher/mesher.ts @@ -191,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 @@ -205,10 +217,24 @@ setInterval(() => { if (chunk?.getSection(new Vec3(x, y, z))) { const start = performance.now() const geometry = getSectionGeometry(x, y, z, world, instancingMode) - 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 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) } @@ -238,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) diff --git a/renderer/viewer/lib/mesher/world.ts b/renderer/viewer/lib/mesher/world.ts index e02a502a..59bc5a34 100644 --- a/renderer/viewer/lib/mesher/world.ts +++ b/renderer/viewer/lib/mesher/world.ts @@ -42,7 +42,7 @@ export class World { customBlockModels = new Map() // chunkKey -> blockModels sentBlockStateModels = new Set() blockStateModelInfo = new Map() - instancedBlocks: Record = {} + instancedBlocks: number[] = [] instancedBlockIds = {} as Record constructor (version: string) { diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 85d677fd..a8153ddd 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -60,6 +60,9 @@ export const defaultWorldRendererConfig = { // 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 } @@ -161,7 +164,7 @@ export abstract class WorldRendererCommon abstract changeBackgroundColor (color: [number, number, number]): void // Optional method for getting instanced blocks data (implemented by Three.js renderer) - getInstancedBlocksData? (): { instanceableBlocks: Set, allBlocksStateIdToModelIdMap: Record } | undefined + getInstancedBlocksData? (): { instanceableBlocks?: Set, allBlocksStateIdToModelIdMap?: Record } | undefined worldRendererConfig: WorldRendererConfig playerStateReactive: PlayerStateReactive diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 29a7ff2b..00359fe4 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -4,7 +4,7 @@ import { versionToNumber } from 'flying-squid/dist/utils' import PrismarineBlock from 'prismarine-block' import { IndexedBlock } from 'minecraft-data' import moreBlockData from '../lib/moreBlockDataGenerated.json' -import { MesherGeometryOutput } from '../lib/mesher/shared' +import { InstancingMode, MesherGeometryOutput } from '../lib/mesher/shared' import { getPreflatBlock } from './getPreflatBlock' import { WorldRendererThree } from './worldrendererThree' @@ -43,7 +43,7 @@ export interface InstancedBlockModelData { } export interface InstancedBlocksConfig { - instanceableBlocks: Set + instanceableBlocks: Set blocksDataModel: Record blockNameToStateIdMap: Record interestedTextureTiles: Set @@ -75,7 +75,8 @@ export class InstancedRenderer { private totalAllocatedInstances = 0 private instancedBlocksConfig: InstancedBlocksConfig | null = null - private sharedMaterial: THREE.MeshLambertMaterial | null = null + private sharedSolidMaterial: THREE.MeshLambertMaterial | null = null + private sharedTransparentMaterial: THREE.MeshLambertMaterial | null = null constructor (private readonly worldRenderer: WorldRendererThree) { this.cubeGeometry = this.createCubeGeometry() @@ -281,7 +282,7 @@ export class InstancedRenderer { } config.blocksDataModel[stateId] = blockData - config.instanceableBlocks.add(name) + config.instanceableBlocks.add(stateId) config.blockNameToStateIdMap[name] = stateId if (mcBlockData) { @@ -292,15 +293,22 @@ export class InstancedRenderer { } prepareInstancedBlocksData () { - if (this.sharedMaterial) { - this.sharedMaterial.dispose() - this.sharedMaterial = null + if (this.sharedSolidMaterial) { + this.sharedSolidMaterial.dispose() + this.sharedSolidMaterial = null } - this.sharedMaterial = new THREE.MeshLambertMaterial({ + this.sharedSolidMaterial = new THREE.MeshLambertMaterial({ transparent: true, + // depthWrite: true, alphaTest: 0.1 }) - this.sharedMaterial.map = this.worldRenderer.material.map + 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 ? { @@ -327,11 +335,11 @@ export class InstancedRenderer { const PBlockOriginal = PrismarineBlock(this.worldRenderer.version) this.instancedBlocksConfig = { - instanceableBlocks: new Set(), - blocksDataModel: {} as Record, - blockNameToStateIdMap: {} as Record, - interestedTextureTiles: new Set(), - } + instanceableBlocks: new Set(), + blocksDataModel: {}, + blockNameToStateIdMap: {}, + interestedTextureTiles: new Set(), + } satisfies InstancedBlocksConfig // Add unknown block model this.prepareInstancedBlock(-1, 'unknown', {}) @@ -374,7 +382,7 @@ export class InstancedRenderer { sv: texture.sv })) } - config.instanceableBlocks.add(block.name) + config.instanceableBlocks.add(block.stateId) config.interestedTextureTiles.add(textureOverride) config.blockNameToStateIdMap[block.name] = stateId continue @@ -392,7 +400,7 @@ export class InstancedRenderer { } } - private createBlockMaterial (blockName: string): THREE.Material { + private createBlockMaterial (blockName: string, isTransparent: boolean): THREE.Material { const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig if (enableSingleColorMode) { @@ -402,7 +410,7 @@ export class InstancedRenderer { material.name = `instanced_color_${blockName}` return material } else { - return this.sharedMaterial! + return isTransparent ? this.sharedTransparentMaterial! : this.sharedSolidMaterial! } } @@ -414,9 +422,8 @@ export class InstancedRenderer { } // Create InstancedMesh for each instanceable block type - for (const blockName of this.instancedBlocksConfig.instanceableBlocks) { - const stateId = this.getStateId(blockName) - this.initializeInstancedMesh(stateId, blockName) + for (const stateId of this.instancedBlocksConfig.instanceableBlocks) { + this.initializeInstancedMesh(stateId, this.stateIdToName.get(stateId)!) } } @@ -428,10 +435,11 @@ export class InstancedRenderer { } 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) + const material = this.createBlockMaterial(blockName, isTransparent) const mesh = new THREE.InstancedMesh( geometry, @@ -443,6 +451,8 @@ export class InstancedRenderer { mesh.count = 0 mesh.visible = this._instancedMeshesVisible // Set initial visibility + // mesh.renderOrder = isTransparent ? 1 : 0 + this.instancedMeshes.set(stateId, mesh) this.worldRenderer.scene.add(mesh) this.totalAllocatedInstances += initialCount @@ -583,7 +593,7 @@ export class InstancedRenderer { return 0x99_99_99 } - handleInstancedBlocksFromWorker (instancedBlocks: MesherGeometryOutput['instancedBlocks'], sectionKey: string) { + 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()) @@ -726,10 +736,6 @@ export class InstancedRenderer { } } - isBlockInstanceable (blockName: string): boolean { - return this.instancedBlocksConfig?.instanceableBlocks.has(blockName) ?? false - } - disposeOldMeshes () { // Reset total instance count since we're clearing everything this.currentTotalInstances = 0 diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 245b03e5..884cc840 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -31,6 +31,7 @@ type SectionKey = string export class WorldRendererThree extends WorldRendererCommon { outputFormat = 'threeJs' as const sectionObjects: Record = {} + sectionInstancingMode: Record = {} chunkTextures = new Map() signsCache = new Map() starField: StarField @@ -209,6 +210,15 @@ 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) => { @@ -364,6 +374,16 @@ export class WorldRendererThree extends WorldRendererCommon { const value = this.sectionObjects[key] if (!value) continue this.updatePosDataChunk(key) + + if (this.worldRendererConfig.dynamicInstancing) { + const [x, y, z] = key.split(',').map(x => +x / 16) + const pos = new Vec3(x, y, z) + const instancingMode = this.getInstancingMode(pos) + if (instancingMode !== this.sectionInstancingMode[key]) { + // update section + this.setSectionDirty(pos) + } + } } } @@ -386,12 +406,17 @@ export class WorldRendererThree extends WorldRendererCommon { const chunkCoords = data.key.split(',') const chunkKey = chunkCoords[0] + ',' + chunkCoords[2] - // Always clear old instanced blocks for this section first, then add new ones if any - this.instancedRenderer?.removeSectionInstances(data.key) + const hasInstancedBlocks = data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0 + const shouldUpdateInstanced = hasInstancedBlocks || this.sectionObjects[data.key] + + if (shouldUpdateInstanced) { + // Only remove if we have something to replace it with or need to clear existing + this.instancedRenderer?.removeSectionInstances(data.key) + } // Handle instanced blocks data from worker - if (data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0) { - this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key) + if (hasInstancedBlocks) { + this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key, this.getInstancingMode(new Vec3(chunkCoords[0], chunkCoords[1], chunkCoords[2]))) } let object: THREE.Object3D = this.sectionObjects[data.key] @@ -982,8 +1007,8 @@ export class WorldRendererThree extends WorldRendererCommon { } } - setSectionDirty (pos: Vec3, value = true) { - const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly } = this.worldRendererConfig + getInstancingMode (pos: Vec3) { + const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly, dynamicInstancing, dynamicInstancingModeDistance, dynamicColorModeDistance } = this.worldRendererConfig let instancingMode = InstancingMode.None if (useInstancedRendering || enableSingleColorMode) { @@ -992,10 +1017,24 @@ export class WorldRendererThree extends WorldRendererCommon { : forceInstancedOnly ? InstancingMode.BlockInstancingOnly : InstancingMode.BlockInstancing + } else if (dynamicInstancing) { + const distance = Math.abs(pos.x / 16 - this.cameraSectionPos.x) + Math.abs(pos.z / 16 - this.cameraSectionPos.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(pos, value, undefined, instancingMode) + super.setSectionDirty(pos, value, undefined, this.getInstancingMode(pos)) + if (value) { + this.sectionInstancingMode[pos.toString()] = this.getInstancingMode(pos) + } } static getRendererInfo (renderer: THREE.WebGLRenderer) { diff --git a/src/index.ts b/src/index.ts index 185caab6..639a8dfe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -276,6 +276,10 @@ export async function connect (connectOptions: ConnectOptions) { return } } + if (e.reason?.stack?.includes('chrome-extension://')) { + // ignore issues caused by chrome extension + return + } handleError(e.reason) }, { signal: errorAbortController.signal From 0240a752ada055d7f27732a900b153f7479c5a83 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 19 Jul 2025 18:11:01 +0300 Subject: [PATCH 19/27] box helper optim --- renderer/viewer/lib/mesher/models.ts | 36 +++++- renderer/viewer/lib/mesher/shared.ts | 3 +- renderer/viewer/three/instancedRenderer.ts | 104 ++++++++++++------ renderer/viewer/three/worldrendererThree.ts | 115 +++++++++++++------- 4 files changed, 179 insertions(+), 79 deletions(-) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index d902e776..6a01bf2e 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -552,7 +552,27 @@ const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boo 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, instancingMode = InstancingMode.None): MesherGeometryOutput { let delayedRender = [] as Array<() => void> @@ -667,14 +687,24 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W attr.instancedBlocks[blockKey] = { stateId: block.stateId, blockName: block.name, - positions: [] + positions: [], + matrices: [] // Add matrices array } } - attr.instancedBlocks[blockKey].positions.push({ + + 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 } diff --git a/renderer/viewer/lib/mesher/shared.ts b/renderer/viewer/lib/mesher/shared.ts index 11ca4ec2..53fc78ff 100644 --- a/renderer/viewer/lib/mesher/shared.ts +++ b/renderer/viewer/lib/mesher/shared.ts @@ -28,10 +28,11 @@ export type CustomBlockModels = { export type MesherConfig = typeof defaultMesherConfig -export type InstancedBlockEntry = { +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 = { diff --git a/renderer/viewer/three/instancedRenderer.ts b/renderer/viewer/three/instancedRenderer.ts index 00359fe4..0b461c51 100644 --- a/renderer/viewer/three/instancedRenderer.ts +++ b/renderer/viewer/three/instancedRenderer.ts @@ -61,6 +61,9 @@ export class InstancedRenderer { private readonly tempMatrix = new THREE.Matrix4() private readonly stateIdToName = new Map() + // Cache for single color materials + private readonly colorMaterials = new Map() + // Dynamic instance management private readonly initialInstancesPerBlock = 2000 private readonly maxInstancesPerBlock = 100_000 @@ -76,11 +79,16 @@ export class InstancedRenderer { private instancedBlocksConfig: InstancedBlocksConfig | null = null private sharedSolidMaterial: THREE.MeshLambertMaterial | null = null - private sharedTransparentMaterial: 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 { @@ -170,7 +178,7 @@ export class InstancedRenderer { } // Verify instance count matches - console.log(`Finished growing ${blockName}. Actual instances: ${actualInstanceCount}, New capacity: ${newSize}, Mesh count: ${newMesh.count}`) + // console.log(`Finished growing ${blockName}. Actual instances: ${actualInstanceCount}, New capacity: ${newSize}, Mesh count: ${newMesh.count}`) return true } @@ -194,7 +202,7 @@ export class InstancedRenderer { )) ) - console.log(`Need to grow ${blockName}: current ${currentForBlock}/${currentCapacity}, need ${neededCapacity}, growing to ${newSize}`) + // 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 @@ -303,12 +311,12 @@ export class InstancedRenderer { 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 + // 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 ? { @@ -400,17 +408,27 @@ export class InstancedRenderer { } } - private createBlockMaterial (blockName: string, isTransparent: boolean): THREE.Material { - const { enableSingleColorMode } = this.worldRenderer.worldRendererConfig + private getOrCreateColorMaterial (blockName: string): THREE.Material { + const color = this.getBlockColor(blockName) + const materialKey = color - if (enableSingleColorMode) { - // Ultra-performance mode: solid colors only - const color = this.getBlockColor(blockName) - const material = new THREE.MeshBasicMaterial({ color }) + let material = this.colorMaterials.get(materialKey) + if (!material) { + material = new THREE.MeshBasicMaterial({ + color, + transparent: false + }) material.name = `instanced_color_${blockName}` - return material + 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 isTransparent ? this.sharedTransparentMaterial! : this.sharedSolidMaterial! + return this.sharedSolidMaterial! } } @@ -423,11 +441,14 @@ export class InstancedRenderer { // Create InstancedMesh for each instanceable block type for (const stateId of this.instancedBlocksConfig.instanceableBlocks) { - this.initializeInstancedMesh(stateId, this.stateIdToName.get(stateId)!) + const blockName = this.stateIdToName.get(stateId) + if (blockName) { + this.initializeInstancedMesh(stateId, blockName, InstancingMode.ColorOnly) + } } } - initializeInstancedMesh (stateId: number, blockName: string) { + initializeInstancedMesh (stateId: number, blockName: string, instancingMode: InstancingMode) { if (this.instancedMeshes.has(stateId)) return // Skip if already exists if (!this.instancedBlocksConfig!.blocksDataModel) { @@ -439,7 +460,7 @@ export class InstancedRenderer { const initialCount = this.getInitialInstanceCount(blockName) const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry - const material = this.createBlockMaterial(blockName, isTransparent) + const material = this.createBlockMaterial(blockName, instancingMode) const mesh = new THREE.InstancedMesh( geometry, @@ -454,7 +475,7 @@ export class InstancedRenderer { // mesh.renderOrder = isTransparent ? 1 : 0 this.instancedMeshes.set(stateId, mesh) - this.worldRenderer.scene.add(mesh) + // Don't add to scene until actually used this.totalAllocatedInstances += initialCount if (!blockModelData) { @@ -589,6 +610,9 @@ export class InstancedRenderer { 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 } @@ -602,7 +626,6 @@ export class InstancedRenderer { // Remove old instances for blocks that are being updated const previousStateIds = [...sectionMap.keys()] - // TODO stress test updates, maybe need more effective way to remove old instances for (const stateId of previousStateIds) { const instanceIndices = sectionMap.get(stateId) if (instanceIndices) { @@ -613,18 +636,18 @@ export class InstancedRenderer { // Keep track of blocks that were updated this frame for (const [blockName, blockData] of Object.entries(instancedBlocks)) { - const { stateId } = blockData + const { stateId, positions, matrices } = blockData this.stateIdToName.set(stateId, blockName) - if (this.USE_APP_GEOMETRY) { // only dynamically initialize meshes for app geometry - this.initializeInstancedMesh(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 = blockData.positions.length + const neededInstances = positions.length if (!this.canAddMoreInstances(stateId, neededInstances)) { console.warn(`Cannot add ${neededInstances} instances for block ${blockName} (current: ${currentCount}, max: ${this.maxInstancesPerBlock})`) continue @@ -632,13 +655,10 @@ export class InstancedRenderer { const mesh = this.instancedMeshes.get(stateId)! - // Add new instances for this section - for (const pos of blockData.positions) { + // 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 - const offset = this.USE_APP_GEOMETRY ? 0 : 0.5 - - this.tempMatrix.setPosition(pos.x + offset, pos.y + offset, pos.z + offset) - mesh.setMatrixAt(instanceIndex, this.tempMatrix) + mesh.setMatrixAt(instanceIndex, new THREE.Matrix4().fromArray(matrices[i])) instanceIndices.push(instanceIndex) } @@ -648,13 +668,14 @@ export class InstancedRenderer { const newCount = currentCount + instanceIndices.length this.blockCounts.set(stateId, newCount) this.currentTotalInstances += instanceIndices.length - mesh.count = newCount // Ensure mesh.count matches our tracking + mesh.count = newCount mesh.instanceMatrix.needsUpdate = true - // Only track mesh in sceneUsedMeshes if it's actually being used - if (newCount > 0) { - this.sceneUsedMeshes.set(blockName, mesh) + // Only add mesh to scene when it's first used + if (newCount === instanceIndices.length) { + this.worldRenderer.scene.add(mesh) } + this.sceneUsedMeshes.set(blockName, mesh) } } } @@ -762,6 +783,17 @@ export class InstancedRenderer { 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() diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 884cc840..ce21b1de 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -30,7 +30,7 @@ type SectionKey = string export class WorldRendererThree extends WorldRendererCommon { outputFormat = 'threeJs' as const - sectionObjects: Record = {} + sectionObjects: Record }> = {} sectionInstancingMode: Record = {} chunkTextures = new Map() signsCache = new Map() @@ -56,6 +56,7 @@ export class WorldRendererThree extends WorldRendererCommon { 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, @@ -222,7 +223,7 @@ export class WorldRendererThree extends WorldRendererCommon { override watchReactiveConfig () { super.watchReactiveConfig() this.onReactiveConfigUpdated('showChunkBorders', (value) => { - this.updateShowChunksBorder(value) + this.updateShowChunksBorder() }) } @@ -370,16 +371,18 @@ 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 object = this.sectionObjects[key] + if (object) { + this.updatePosDataChunk(key) + } if (this.worldRendererConfig.dynamicInstancing) { - const [x, y, z] = key.split(',').map(x => +x / 16) + 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) } @@ -407,55 +410,55 @@ export class WorldRendererThree extends WorldRendererCommon { const chunkKey = chunkCoords[0] + ',' + chunkCoords[2] const hasInstancedBlocks = data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0 - const shouldUpdateInstanced = hasInstancedBlocks || this.sectionObjects[data.key] - if (shouldUpdateInstanced) { - // Only remove if we have something to replace it with or need to clear existing - this.instancedRenderer?.removeSectionInstances(data.key) - } + this.instancedRenderer?.removeSectionInstances(data.key) + + // if (data.key === '48,64,32') { + // console.log('handleWorkerMessage', data.key, this.sectionObjects[data.key], this.sectionInstancingMode[data.key], Object.keys(data.geometry.instancedBlocks).length, data.geometry.positions.length) + // } // 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]))) } - let object: THREE.Object3D = this.sectionObjects[data.key] + let object = this.sectionObjects[data.key] if (object) { this.scene.remove(object) disposeObject(object) delete this.sectionObjects[data.key] } + object = this.sectionObjects[data.key] + if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) return // if (object) { // this.debugRecomputedDeletedObjects++ // } - const geometry = new THREE.BufferGeometry() + const geometry = object?.mesh?.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) + const { sx, sy, sz } = data.geometry - const mesh = new THREE.Mesh(geometry, this.material) + // Set bounding box for the 16x16x16 section + geometry.boundingBox = new THREE.Box3(new THREE.Vector3(-8, -8, -8), new THREE.Vector3(8, 8, 8)) + geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.sqrt(3 * 8 ** 2)) + + const mesh = object?.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'; + if (!object) { + object = new THREE.Group() + object.add(mesh) + } (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 - } + // should not compute it once if (Object.keys(data.geometry.signs).length) { for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) { @@ -477,7 +480,15 @@ export class WorldRendererThree extends WorldRendererCommon { object.add(head) } } - this.sectionObjects[data.key] = object + + if (!this.sectionObjects[data.key]) { + this.sectionObjects[data.key] = object + this.sectionObjects[data.key].mesh = mesh + this.scene.add(object) + } + + this.updateBoxHelper(data.key) + if (this.displayOptions.inWorldRenderingConfig._renderByChunks) { object.visible = false const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}` @@ -494,8 +505,6 @@ export class WorldRendererThree extends WorldRendererCommon { mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => { // mesh.matrixAutoUpdate = false } - - this.scene.add(object) } getSignTexture (position: Vec3, blockEntity, backSide = false) { @@ -923,13 +932,32 @@ export class WorldRendererThree extends WorldRendererCommon { } } - 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.sectionObjects)) { + this.updateBoxHelper(key) + } + } + + updateBoxHelper (key: string) { + const { showChunkBorders } = this.worldRendererConfig + const section = this.sectionObjects[key] + if (!section) return + if (showChunkBorders) { + if (!section.boxHelper) { + // mesh with static dimensions: 16x16x16 + const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), this.chunkBoxMaterial) + staticChunkMesh.position.set(section.mesh!.position.x, section.mesh!.position.y, section.mesh!.position.z) + const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00) + boxHelper.name = 'helper' + // boxHelper.geometry.boundingSphere = section.mesh!.geometry.boundingSphere + section.add(boxHelper) + section.name = 'chunk' + section.boxHelper = boxHelper } + + section.boxHelper.visible = true + } else if (section.boxHelper) { + section.boxHelper.visible = false } } @@ -987,6 +1015,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) @@ -1018,7 +1051,10 @@ export class WorldRendererThree extends WorldRendererCommon { ? InstancingMode.BlockInstancingOnly : InstancingMode.BlockInstancing } else if (dynamicInstancing) { - const distance = Math.abs(pos.x / 16 - this.cameraSectionPos.x) + Math.abs(pos.z / 16 - this.cameraSectionPos.z) + 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) { @@ -1031,9 +1067,10 @@ export class WorldRendererThree extends WorldRendererCommon { setSectionDirty (pos: Vec3, value = true) { this.cleanChunkTextures(pos.x, pos.z) // todo don't do this! - super.setSectionDirty(pos, value, undefined, this.getInstancingMode(pos)) + const instancingMode = this.getInstancingMode(pos) + super.setSectionDirty(pos, value, undefined, instancingMode) if (value) { - this.sectionInstancingMode[pos.toString()] = this.getInstancingMode(pos) + this.sectionInstancingMode[`${pos.x},${pos.y},${pos.z}`] = instancingMode } } From 9ee28ef62f8dfb5b1d806d249cb32c9b5142dbe3 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 08:21:15 +0300 Subject: [PATCH 20/27] working geometry pool manager! --- renderer/viewer/lib/worldDataEmitter.ts | 4 +- renderer/viewer/lib/worldrendererCommon.ts | 1 - renderer/viewer/three/chunkMeshManager.ts | 298 ++++++++++++++++++++ renderer/viewer/three/worldrendererThree.ts | 158 ++++++----- 4 files changed, 390 insertions(+), 71 deletions(-) create mode 100644 renderer/viewer/three/chunkMeshManager.ts diff --git a/renderer/viewer/lib/worldDataEmitter.ts b/renderer/viewer/lib/worldDataEmitter.ts index e1ac2f24..c43c9353 100644 --- a/renderer/viewer/lib/worldDataEmitter.ts +++ b/renderer/viewer/lib/worldDataEmitter.ts @@ -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 | { blockEntities: Record }) => 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 this.allChunksFinished = true this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn! } - this.updateChunksStats() } changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { } diff --git a/renderer/viewer/three/chunkMeshManager.ts b/renderer/viewer/three/chunkMeshManager.ts new file mode 100644 index 00000000..3038eb9b --- /dev/null +++ b/renderer/viewer/three/chunkMeshManager.ts @@ -0,0 +1,298 @@ +import * as THREE from 'three' +import { MesherGeometryOutput } from '../lib/mesher/shared' + +export interface ChunkMeshPool { + mesh: THREE.Mesh + inUse: boolean + lastUsedTime: number + sectionKey?: string +} + +export class ChunkMeshManager { + private readonly meshPool: ChunkMeshPool[] = [] + private readonly activeSections = new Map() + private poolSize: number + private maxPoolSize: number + private minPoolSize: number + + // Performance tracking + private hits = 0 + private misses = 0 + + // Debug flag to bypass pooling + public bypassPooling = false + + constructor ( + public material: THREE.Material, + public worldHeight: number, + viewDistance = 3, + ) { + this.updateViewDistance(viewDistance) + + console.log(`ChunkMeshManager: Initializing with pool size ${this.poolSize} (min: ${this.minPoolSize}, max: ${this.maxPoolSize})`) + + 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): THREE.Mesh | null { + // Normal pooling mode + // Check if section already exists + let poolEntry = this.activeSections.get(sectionKey) + + if (!poolEntry) { + // Get mesh from pool + 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 + const { geometry } = mesh + + // Update geometry attributes efficiently + this.updateGeometryAttribute(geometry, 'position', geometryData.positions, 3) + this.updateGeometryAttribute(geometry, 'normal', geometryData.normals, 3) + this.updateGeometryAttribute(geometry, 'color', geometryData.colors, 3) + this.updateGeometryAttribute(geometry, 'uv', geometryData.uvs, 2) + + // Use direct index assignment for better performance (like before) + geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1) + + // Set bounding box and sphere for the 16x16x16 section + geometry.boundingBox = new THREE.Box3( + new THREE.Vector3(-8, -8, -8), + new THREE.Vector3(8, 8, 8) + ) + 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 + + // Store metadata + ;(mesh as any).tilesCount = geometryData.positions.length / 3 / 4 + ;(mesh as any).blocksCount = geometryData.blocksCount + + poolEntry.lastUsedTime = performance.now() + + return mesh + } + + /** + * Release a section and return its mesh to the pool + */ + releaseSection (sectionKey: string): boolean { + 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) + + return true + } + + /** + * 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' + + return { + poolSize: this.poolSize, + activeCount: this.activeSections.size, + freeCount, + hitRate: `${hitRate}%`, + hits: this.hits, + misses: this.misses + } + } + + /** + * 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 + } + } +} diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index ce21b1de..f3921f45 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -7,6 +7,7 @@ import { renderSign } from '../sign-renderer' import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer' import { chunkPos, sectionPos } from '../lib/simpleUtils' import { WorldRendererCommon } from '../lib/worldrendererCommon' +import { WorldDataEmitterWorker } from '../lib/worldDataEmitter' import { addNewStat } from '../lib/ui/newStats' import { MesherGeometryOutput, InstancingMode } from '../lib/mesher/shared' import { ItemSpecificContextProperties } from '../lib/basePlayerState' @@ -25,6 +26,7 @@ import { CameraShake } from './cameraShake' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' import { InstancedRenderer } from './instancedRenderer' +import { ChunkMeshManager } from './chunkMeshManager' type SectionKey = string @@ -53,6 +55,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraContainer: THREE.Object3D media: ThreeJsMedia instancedRenderer: InstancedRenderer | undefined + chunkMeshManager: ChunkMeshManager waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } camera: THREE.PerspectiveCamera renderTimeAvg = 0 @@ -106,6 +109,14 @@ export class WorldRendererThree extends WorldRendererCommon { this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) this.instancedRenderer = new InstancedRenderer(this) + this.chunkMeshManager = new ChunkMeshManager(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), // }) @@ -137,6 +148,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: { @@ -346,8 +366,11 @@ export class WorldRendererThree extends WorldRendererCommon { text += `B: ${formatBigNumber(this.blocksRendered)} ` if (instancedStats) { text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t ` - text += `DC: ${formatBigNumber(instancedStats.drawCalls)}` + 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 } @@ -361,7 +384,7 @@ 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')! + const section = this.sectionObjects[key].mesh! section.renderOrder = 500 - chunkDistance } @@ -402,7 +425,6 @@ export class WorldRendererThree extends WorldRendererCommon { delete this.waitingChunksToDisplay[chunkKey] } - // debugRecomputedDeletedObjects = 0 handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void { if (data.type !== 'geometry') return @@ -413,55 +435,74 @@ export class WorldRendererThree extends WorldRendererCommon { this.instancedRenderer?.removeSectionInstances(data.key) - // if (data.key === '48,64,32') { - // console.log('handleWorkerMessage', data.key, this.sectionObjects[data.key], this.sectionInstancingMode[data.key], Object.keys(data.geometry.instancedBlocks).length, data.geometry.positions.length) - // } - // 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]))) } + // 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 + } + + // remvoe object from scene let object = this.sectionObjects[data.key] if (object) { this.scene.remove(object) - disposeObject(object) + // disposeObject(object) delete this.sectionObjects[data.key] } - object = this.sectionObjects[data.key] + // Use ChunkMeshManager for optimized mesh handling + const mesh = this.chunkMeshManager.updateSection(data.key, data.geometry) - if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) return + if (!mesh) { + console.warn(`Failed to get mesh for section ${data.key}`) + return + } - // if (object) { - // this.debugRecomputedDeletedObjects++ + // Create or update the section object container + object = new THREE.Group() + this.sectionObjects[data.key] = object + this.scene.add(object) + + // Add the pooled mesh to the container + object.add(mesh) + this.sectionObjects[data.key].mesh = mesh as THREE.Mesh + + // Handle signs and heads (these are added to the container, not the pooled mesh) + this.addSignsAndHeads(object as THREE.Group, data.geometry) + + this.updateBoxHelper(data.key) + + // Handle chunk-based rendering + if (this.displayOptions.inWorldRenderingConfig._renderByChunks) { + object.visible = false + const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}` + this.waitingChunksToDisplay[chunkKey] ??= [] + this.waitingChunksToDisplay[chunkKey].push(data.key) + if (this.finishedChunks[chunkKey]) { + this.finishChunk(chunkKey) + } + } + + this.updatePosDataChunk(data.key) + object.matrixAutoUpdate = false + } + + private addSignsAndHeads (object: THREE.Group, geometry: MesherGeometryOutput) { + // Clear existing signs and heads + // const childrenToRemove = object.children.filter(child => child.name !== 'mesh' && child.name !== 'helper') + // for (const child of childrenToRemove) { + // object.remove(child) + // disposeObject(child) // } - const geometry = object?.mesh?.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) - const { sx, sy, sz } = data.geometry - - // Set bounding box for the 16x16x16 section - geometry.boundingBox = new THREE.Box3(new THREE.Vector3(-8, -8, -8), new THREE.Vector3(8, 8, 8)) - geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.sqrt(3 * 8 ** 2)) - - const mesh = object?.mesh ?? new THREE.Mesh(geometry, this.material) - mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz) - mesh.name = 'mesh' - if (!object) { - object = new THREE.Group() - object.add(mesh) - } - (object as any).tilesCount = data.geometry.positions.length / 3 / 4; - (object as any).blocksCount = data.geometry.blocksCount - - // should not compute it once - if (Object.keys(data.geometry.signs).length) { - for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) { + // Add signs + if (Object.keys(geometry.signs).length) { + for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometry.signs)) { const signBlockEntity = this.blockEntities[posKey] if (!signBlockEntity) continue const [x, y, z] = posKey.split(',') @@ -470,8 +511,10 @@ export class WorldRendererThree extends WorldRendererCommon { object.add(sign) } } - if (Object.keys(data.geometry.heads).length) { - for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) { + + // Add heads + if (Object.keys(geometry.heads).length) { + for (const [posKey, { isWall, rotation }] of Object.entries(geometry.heads)) { const headBlockEntity = this.blockEntities[posKey] if (!headBlockEntity) continue const [x, y, z] = posKey.split(',') @@ -480,31 +523,6 @@ export class WorldRendererThree extends WorldRendererCommon { object.add(head) } } - - if (!this.sectionObjects[data.key]) { - this.sectionObjects[data.key] = object - this.sectionObjects[data.key].mesh = mesh - this.scene.add(object) - } - - this.updateBoxHelper(data.key) - - if (this.displayOptions.inWorldRenderingConfig._renderByChunks) { - object.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 - } } getSignTexture (position: Vec3, blockEntity, backSide = false) { @@ -1031,10 +1049,13 @@ export class WorldRendererThree extends WorldRendererCommon { // Remove instanced blocks for this section this.instancedRenderer?.removeSectionInstances(key) - const mesh = this.sectionObjects[key] - if (mesh) { - this.scene.remove(mesh) - disposeObject(mesh) + // Release section from mesh pool + this.chunkMeshManager.releaseSection(key) + + const object = this.sectionObjects[key] + if (object) { + this.scene.remove(object) + disposeObject(object) } delete this.sectionObjects[key] } @@ -1089,6 +1110,7 @@ export class WorldRendererThree extends WorldRendererCommon { destroy (): void { this.instancedRenderer?.destroy() + this.chunkMeshManager.dispose() super.destroy() } From 2f49cbb35be5c3d16947b1f8f697cbba71027c99 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 09:24:57 +0300 Subject: [PATCH 21/27] finish manager! --- experiments/three.ts | 218 +++++++-- renderer/viewer/lib/worldrendererCommon.ts | 1 + renderer/viewer/three/chunkMeshManager.ts | 483 +++++++++++++++++++- renderer/viewer/three/worldrendererThree.ts | 261 ++--------- src/controls.ts | 4 +- src/defaultOptions.ts | 1 + src/watchOptions.ts | 4 + 7 files changed, 696 insertions(+), 276 deletions(-) diff --git a/experiments/three.ts b/experiments/three.ts index 9b158dec..63e08d9e 100644 --- a/experiments/three.ts +++ b/experiments/three.ts @@ -1,5 +1,5 @@ import * as THREE from 'three' -import { loadThreeJsTextureFromBitmap } from '../renderer/viewer/lib/utils/skins' +import globalTexture from 'mc-assets/dist/blocksAtlasLegacy.png' // Create scene, camera and renderer const scene = new THREE.Scene() @@ -9,53 +9,193 @@ 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) +// Create shared material that will be used by all blocks +const sharedMaterial = new THREE.MeshLambertMaterial({ + vertexColors: true, + transparent: true, + alphaTest: 0.1 +}) -// 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) +function createCustomGeometry(textureInfo: { u: number, v: number, su: number, sv: number }): THREE.BufferGeometry { + // Create custom geometry with specific UV coordinates for this block type + 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 + + console.log('Original UVs:', Array.from(uvs)) + console.log('Texture info:', textureInfo) + + // BoxGeometry has 6 faces, each with 2 triangles (4 vertices), so 24 UV pairs total + // Apply the same texture to all faces for simplicity + for (let i = 0; i < uvs.length; i += 2) { + const u = uvs[i] + const v = uvs[i + 1] + + // Map from 0-1 to the specific texture region in the atlas + uvs[i] = textureInfo.u + u * textureInfo.su + uvs[i + 1] = textureInfo.v + v * textureInfo.sv + } + + console.log('Modified UVs:', Array.from(uvs)) + uvAttribute.needsUpdate = true + return geometry +} + +let currentInstancedMesh: THREE.InstancedMesh | null = null +let currentRefCube: THREE.Mesh | null = null + +async function createInstancedBlock() { + try { + // Clean up previous meshes if they exist + if (currentInstancedMesh) { + scene.remove(currentInstancedMesh) + currentInstancedMesh.geometry.dispose() + } + if (currentRefCube) { + scene.remove(currentRefCube) + currentRefCube.geometry.dispose() + } + + // Load the blocks atlas texture + const textureLoader = new THREE.TextureLoader() + const texture = await new Promise((resolve, reject) => { + textureLoader.load( + globalTexture, + resolve, + undefined, + reject + ) + }) + + // 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 (first 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 custom geometry with proper UV mapping + const geometry = createCustomGeometry(textureInfo) + + // Create instanced mesh using shared material + currentInstancedMesh = new THREE.InstancedMesh(geometry, sharedMaterial, 1) + const matrix = new THREE.Matrix4() + matrix.setPosition(0.5, 0.5, 0.5) // Offset by +0.5 on each axis + currentInstancedMesh.setMatrixAt(0, matrix) + currentInstancedMesh.count = 1 + currentInstancedMesh.instanceMatrix.needsUpdate = true + scene.add(currentInstancedMesh) + + // Reference non-instanced cube using same material + currentRefCube = new THREE.Mesh(geometry, sharedMaterial) + currentRefCube.position.set(2.5, 0.5, 0.5) // Offset by +0.5 on each axis + scene.add(currentRefCube) + + console.log('Instanced block created successfully') + + } catch (error) { + console.error('Error creating instanced block:', error) + + // Fallback: create a colored cube + const geometry = new THREE.BoxGeometry(1, 1, 1) + const material = new THREE.MeshLambertMaterial({ color: 0xff0000 }) + currentRefCube = new THREE.Mesh(geometry, material) + scene.add(currentRefCube) + console.log('Created fallback colored cube') + } } -// Create the textured box -createTexturedBox() +// Create the instanced block +createInstancedBlock() -// 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 cube + 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() diff --git a/renderer/viewer/lib/worldrendererCommon.ts b/renderer/viewer/lib/worldrendererCommon.ts index 2ac7af86..7db0acc3 100644 --- a/renderer/viewer/lib/worldrendererCommon.ts +++ b/renderer/viewer/lib/worldrendererCommon.ts @@ -65,6 +65,7 @@ export const defaultWorldRendererConfig = { 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 diff --git a/renderer/viewer/three/chunkMeshManager.ts b/renderer/viewer/three/chunkMeshManager.ts index 3038eb9b..a5ac58a6 100644 --- a/renderer/viewer/three/chunkMeshManager.ts +++ b/renderer/viewer/three/chunkMeshManager.ts @@ -1,5 +1,14 @@ +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 @@ -8,12 +17,25 @@ export interface ChunkMeshPool { sectionKey?: string } +export interface SectionObject extends THREE.Group { + mesh?: THREE.Mesh + 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() + readonly sectionObjects: Record = {} private poolSize: number private maxPoolSize: number private minPoolSize: number + private readonly signHeadsRenderer: SignHeadsRenderer // Performance tracking private hits = 0 @@ -22,14 +44,30 @@ export class ChunkMeshManager { // 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.Scene, public material: THREE.Material, public worldHeight: number, viewDistance = 3, ) { this.updateViewDistance(viewDistance) - - console.log(`ChunkMeshManager: Initializing with pool size ${this.poolSize} (min: ${this.minPoolSize}, max: ${this.maxPoolSize})`) + this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer) this.initializePool() } @@ -57,13 +95,16 @@ export class ChunkMeshManager { /** * Update or create a section with new geometry data */ - updateSection (sectionKey: string, geometryData: MesherGeometryOutput): THREE.Mesh | null { - // Normal pooling mode - // Check if section already exists - let poolEntry = this.activeSections.get(sectionKey) + 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) { - // Get mesh from pool poolEntry = this.acquireMesh() if (!poolEntry) { console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`) @@ -75,23 +116,22 @@ export class ChunkMeshManager { } const { mesh } = poolEntry - const { geometry } = mesh // Update geometry attributes efficiently - this.updateGeometryAttribute(geometry, 'position', geometryData.positions, 3) - this.updateGeometryAttribute(geometry, 'normal', geometryData.normals, 3) - this.updateGeometryAttribute(geometry, 'color', geometryData.colors, 3) - this.updateGeometryAttribute(geometry, 'uv', geometryData.uvs, 2) + 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) - geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1) + mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1) // Set bounding box and sphere for the 16x16x16 section - geometry.boundingBox = new THREE.Box3( + mesh.geometry.boundingBox = new THREE.Box3( new THREE.Vector3(-8, -8, -8), new THREE.Vector3(8, 8, 8) ) - geometry.boundingSphere = new THREE.Sphere( + mesh.geometry.boundingSphere = new THREE.Sphere( new THREE.Vector3(0, 0, 0), Math.sqrt(3 * 8 ** 2) ) @@ -100,20 +140,81 @@ export class ChunkMeshManager { mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz) mesh.updateMatrix() mesh.visible = true - - // Store metadata - ;(mesh as any).tilesCount = geometryData.positions.length / 3 / 4 - ;(mesh as any).blocksCount = geometryData.blocksCount + mesh.name = 'mesh' poolEntry.lastUsedTime = performance.now() - return mesh + // Create or update the section object container + sectionObject = new THREE.Group() as SectionObject + sectionObject.add(mesh) + sectionObject.mesh = mesh as THREE.Mesh + + // Store metadata + sectionObject.tilesCount = geometryData.positions.length / 3 / 4 + sectionObject.blocksCount = geometryData.blocksCount + + // 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 + } + + // 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 @@ -130,9 +231,43 @@ export class ChunkMeshManager { 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 */ @@ -174,6 +309,7 @@ export class ChunkMeshManager { 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, @@ -181,7 +317,87 @@ export class ChunkMeshManager { freeCount, hitRate: `${hitRate}%`, hits: this.hits, - misses: this.misses + 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`, + } } } @@ -295,4 +511,229 @@ export class ChunkMeshManager { 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() + + 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) + + 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, 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, 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 + } } diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index f3921f45..24d09f6f 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -1,11 +1,8 @@ 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' @@ -15,8 +12,6 @@ import { getMyHand } from '../lib/hand' import { setBlockPosition } from '../lib/mesher/standaloneRenderer' import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins' import HoldingBlock from './holdingBlock' -import { getMesh } from './entity/EntityMesh' -import { armorModel } from './entity/armorModels' import { disposeObject } from './threeJsUtils' import { CursorBlock } from './world/cursorBlock' import { getItemUv } from './appShared' @@ -32,7 +27,6 @@ type SectionKey = string export class WorldRendererThree extends WorldRendererCommon { outputFormat = 'threeJs' as const - sectionObjects: Record }> = {} sectionInstancingMode: Record = {} chunkTextures = new Map() signsCache = new Map() @@ -85,11 +79,11 @@ export class WorldRendererThree extends WorldRendererCommon { 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) { @@ -109,7 +103,7 @@ export class WorldRendererThree extends WorldRendererCommon { this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) this.instancedRenderer = new InstancedRenderer(this) - this.chunkMeshManager = new ChunkMeshManager(this.material, this.worldSizeParams.worldHeight, this.viewDistance) + this.chunkMeshManager = new ChunkMeshManager(this, this.realScene, 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') { @@ -245,6 +239,12 @@ export class WorldRendererThree extends WorldRendererCommon { this.onReactiveConfigUpdated('showChunkBorders', (value) => { this.updateShowChunksBorder() }) + this.onReactiveConfigUpdated('enableDebugOverlay', (value) => { + if (!value) { + // restore visibility + this.chunkMeshManager.updateSectionsVisibility() + } + }) } changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) { @@ -384,8 +384,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].mesh! - section.renderOrder = 500 - chunkDistance + const sectionObject = this.chunkMeshManager.getSectionObject(key)! + sectionObject.renderOrder = 500 - chunkDistance } override updateViewerPosition (pos: Vec3): void { @@ -395,8 +395,8 @@ export class WorldRendererThree extends WorldRendererCommon { cameraSectionPositionUpdate () { // eslint-disable-next-line guard-for-in for (const key in this.sectionInstancingMode) { - const object = this.sectionObjects[key] - if (object) { + const sectionObject = this.chunkMeshManager.getSectionObject(key)! + if (sectionObject) { this.updatePosDataChunk(key) } @@ -420,7 +420,10 @@ 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] } @@ -447,39 +450,19 @@ export class WorldRendererThree extends WorldRendererCommon { return } - // remvoe object from scene - let object = this.sectionObjects[data.key] - if (object) { - this.scene.remove(object) - // disposeObject(object) - delete this.sectionObjects[data.key] - } - // Use ChunkMeshManager for optimized mesh handling - const mesh = this.chunkMeshManager.updateSection(data.key, data.geometry) + const sectionObject = this.chunkMeshManager.updateSection(data.key, data.geometry) - if (!mesh) { - console.warn(`Failed to get mesh for section ${data.key}`) + if (!sectionObject) { return } - // Create or update the section object container - object = new THREE.Group() - this.sectionObjects[data.key] = object - this.scene.add(object) - - // Add the pooled mesh to the container - object.add(mesh) - this.sectionObjects[data.key].mesh = mesh as THREE.Mesh - - // Handle signs and heads (these are added to the container, not the pooled mesh) - this.addSignsAndHeads(object as THREE.Group, data.geometry) 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) @@ -489,62 +472,6 @@ export class WorldRendererThree extends WorldRendererCommon { } this.updatePosDataChunk(data.key) - object.matrixAutoUpdate = false - } - - private addSignsAndHeads (object: THREE.Group, geometry: MesherGeometryOutput) { - // Clear existing signs and heads - // const childrenToRemove = object.children.filter(child => child.name !== 'mesh' && child.name !== 'helper') - // for (const child of childrenToRemove) { - // object.remove(child) - // disposeObject(child) - // } - - // Add signs - if (Object.keys(geometry.signs).length) { - for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(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) - } - } - - // Add heads - if (Object.keys(geometry.heads).length) { - for (const [posKey, { isWall, rotation }] of Object.entries(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) - } - } - } - - getSignTexture (position: Vec3, blockEntity, 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, 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 () { @@ -629,7 +556,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 @@ -787,7 +714,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 @@ -799,10 +726,6 @@ export class WorldRendererThree extends WorldRendererCommon { object.visible = isVisible } - } else { - for (const object of Object.values(this.sectionObjects)) { - object.visible = true - } } } @@ -842,9 +765,10 @@ export class WorldRendererThree extends WorldRendererCommon { } 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() } @@ -854,87 +778,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) - - 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 @@ -944,45 +799,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 () { - for (const key of Object.keys(this.sectionObjects)) { + for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) { this.updateBoxHelper(key) } } updateBoxHelper (key: string) { const { showChunkBorders } = this.worldRendererConfig - const section = this.sectionObjects[key] - if (!section) return - if (showChunkBorders) { - if (!section.boxHelper) { - // mesh with static dimensions: 16x16x16 - const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), this.chunkBoxMaterial) - staticChunkMesh.position.set(section.mesh!.position.x, section.mesh!.position.y, section.mesh!.position.z) - const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00) - boxHelper.name = 'helper' - // boxHelper.geometry.boundingSphere = section.mesh!.geometry.boundingSphere - section.add(boxHelper) - section.name = 'chunk' - section.boxHelper = boxHelper - } - - section.boxHelper.visible = true - } else if (section.boxHelper) { - section.boxHelper.visible = false - } + 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) } @@ -999,7 +836,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}` @@ -1016,12 +853,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) } @@ -1049,15 +887,8 @@ export class WorldRendererThree extends WorldRendererCommon { // Remove instanced blocks for this section this.instancedRenderer?.removeSectionInstances(key) - // Release section from mesh pool + // Release section from mesh pool (this will also remove from scene) this.chunkMeshManager.releaseSection(key) - - const object = this.sectionObjects[key] - if (object) { - this.scene.remove(object) - disposeObject(object) - } - delete this.sectionObjects[key] } } @@ -1123,7 +954,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 () { @@ -1161,7 +992,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, diff --git a/src/controls.ts b/src/controls.ts index 9430f9c1..34876664 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => { lockUrl() } if (command === 'communication.toggleMicrophone') { - toggleMicrophoneMuted?.() + if (typeof toggleMicrophoneMuted === 'function') { + toggleMicrophoneMuted() + } } } diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index be5851b2..df2ff4fd 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -44,6 +44,7 @@ export const defaultOptions = { useVersionsTextures: 'latest', // Instanced rendering options useInstancedRendering: false, + autoLowerRenderDistance: false, forceInstancedOnly: false, instancedOnlyDistance: 6, enableSingleColorMode: false, diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 8d1e46b9..c530f831 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -116,6 +116,10 @@ export const watchOptionsAfterViewerInit = () => { // appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates }) + watchValue(options, o => { + appViewer.inWorldRenderingConfig.autoLowerRenderDistance = o.autoLowerRenderDistance + }) + // Instanced rendering options watchValue(options, o => { appViewer.inWorldRenderingConfig.useInstancedRendering = o.useInstancedRendering From 96c5ebb379038bb75ce30179a6ab08e7349c13d3 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 09:38:22 +0300 Subject: [PATCH 22/27] [to pick] fix chat crash --- src/react/Chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index a45d7a69..20d77a10 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -45,7 +45,7 @@ const MessageLine = ({ message, currentPlayerName, chatOpened }: { message: Mess return
  • val).map(([name]) => name).join(' ')} data-time={message.timestamp ? new Date(message.timestamp).toLocaleString('en-US', { hour12: false }) : undefined}> {message.parts.map((msg, i) => { // Check if this is a text part that might contain a mention - if (msg.text && currentPlayerName) { + if (typeof msg.text === 'string' && currentPlayerName) { const parts = msg.text.split(new RegExp(`(@${currentPlayerName})`, 'i')) if (parts.length > 1) { return parts.map((txtPart, j) => { From 0921b40f882d15c7f7ef25edecee0def71a63c45 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 09:38:49 +0300 Subject: [PATCH 23/27] fix signs --- renderer/viewer/three/worldrendererThree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index 24d09f6f..31340304 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -385,7 +385,7 @@ export class WorldRendererThree extends WorldRendererCommon { // 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 sectionObject = this.chunkMeshManager.getSectionObject(key)! - sectionObject.renderOrder = 500 - chunkDistance + sectionObject.mesh!.renderOrder = 500 - chunkDistance } override updateViewerPosition (pos: Vec3): void { From be0993a00bc02ea7e6833538147cc74e73df876f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 10:02:21 +0300 Subject: [PATCH 24/27] up all --- renderer/viewer/lib/mesher/models.ts | 2 +- renderer/viewer/three/chunkMeshManager.ts | 8 ++++---- renderer/viewer/three/worldrendererThree.ts | 2 +- src/optionsGuiScheme.tsx | 18 ++++++++++-------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/renderer/viewer/lib/mesher/models.ts b/renderer/viewer/lib/mesher/models.ts index 6a01bf2e..d38bcbee 100644 --- a/renderer/viewer/lib/mesher/models.ts +++ b/renderer/viewer/lib/mesher/models.ts @@ -674,7 +674,7 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W } 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) { + 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)) { diff --git a/renderer/viewer/three/chunkMeshManager.ts b/renderer/viewer/three/chunkMeshManager.ts index a5ac58a6..ef2738dd 100644 --- a/renderer/viewer/three/chunkMeshManager.ts +++ b/renderer/viewer/three/chunkMeshManager.ts @@ -61,7 +61,7 @@ export class ChunkMeshManager { constructor ( public worldRenderer: WorldRendererThree, - public scene: THREE.Scene, + public scene: THREE.Group, public material: THREE.Material, public worldHeight: number, viewDistance = 3, @@ -674,7 +674,7 @@ class SignHeadsRenderer { } renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) { - const tex = this.getSignTexture(position, blockEntity) + const tex = this.getSignTexture(position, blockEntity, isHanging) if (!tex) return @@ -715,7 +715,7 @@ class SignHeadsRenderer { return group } - getSignTexture (position: Vec3, blockEntity, backSide = false) { + getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) { const chunk = chunkPos(position) let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`) if (!textures) { @@ -727,7 +727,7 @@ class SignHeadsRenderer { if (textures[texturekey]) return textures[texturekey] const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version) - const canvas = renderSign(blockEntity, PrismarineChat) + const canvas = renderSign(blockEntity, isHanging, PrismarineChat) if (!canvas) return const tex = new THREE.Texture(canvas) tex.magFilter = THREE.NearestFilter diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index ceb48735..c066a1e8 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -102,7 +102,7 @@ export class WorldRendererThree extends WorldRendererCommon { this.cameraShake = new CameraShake(this, this.onRender) this.media = new ThreeJsMedia(this) this.instancedRenderer = new InstancedRenderer(this) - this.chunkMeshManager = new ChunkMeshManager(this, this.realScene, this.material, this.worldSizeParams.worldHeight, this.viewDistance) + 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') { diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 1beadb07..84cebb49 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -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' @@ -127,7 +127,14 @@ export const guiOptionsScheme: { } } - return + /> }, }, { From c93b7a6f8b71e6f45a7b46690181140970406225 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 20 Jul 2025 23:02:29 +0300 Subject: [PATCH 25/27] Refactor sign and head container addition in ChunkMeshManager with error handling --- renderer/viewer/three/chunkMeshManager.ts | 58 ++++++++++++----------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/renderer/viewer/three/chunkMeshManager.ts b/renderer/viewer/three/chunkMeshManager.ts index ef2738dd..0e32ed49 100644 --- a/renderer/viewer/three/chunkMeshManager.ts +++ b/renderer/viewer/three/chunkMeshManager.ts @@ -153,36 +153,40 @@ export class ChunkMeshManager { sectionObject.tilesCount = geometryData.positions.length / 3 / 4 sectionObject.blocksCount = geometryData.blocksCount - // 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) + 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 } - 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) + // 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 } - sectionObject.add(headsContainer) - sectionObject.headsContainer = headsContainer + } catch (err) { + console.error('ChunkMeshManager: Error adding signs or heads to section', err) } // Store and add to scene From c63b6bb5367abfe56f43db423e0d51ec2b9ac6fd Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 1 Aug 2025 03:35:45 +0300 Subject: [PATCH 26/27] [skip ci] test grid --- experiments/three.ts | 230 +++++++++++++++++++++++++++++++------------ 1 file changed, 166 insertions(+), 64 deletions(-) diff --git a/experiments/three.ts b/experiments/three.ts index 63e08d9e..cd82f5ec 100644 --- a/experiments/three.ts +++ b/experiments/three.ts @@ -1,6 +1,9 @@ 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() const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) @@ -22,65 +25,107 @@ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4) directionalLight.position.set(1, 1, 1) scene.add(directionalLight) +// Add grid helper for orientation +const gridHelper = new THREE.GridHelper(10, 10) +scene.add(gridHelper) + // Create shared material that will be used by all blocks const sharedMaterial = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, - alphaTest: 0.1 + alphaTest: 0.1, + // wireframe: true // Add wireframe for debugging }) -function createCustomGeometry(textureInfo: { u: number, v: number, su: number, sv: number }): THREE.BufferGeometry { - // Create custom geometry with specific UV coordinates for this block type - 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 - - console.log('Original UVs:', Array.from(uvs)) - console.log('Texture info:', textureInfo) - - // BoxGeometry has 6 faces, each with 2 triangles (4 vertices), so 24 UV pairs total - // Apply the same texture to all faces for simplicity - for (let i = 0; i < uvs.length; i += 2) { - const u = uvs[i] - const v = uvs[i + 1] - - // Map from 0-1 to the specific texture region in the atlas - uvs[i] = textureInfo.u + u * textureInfo.su - uvs[i + 1] = textureInfo.v + v * textureInfo.sv - } - - console.log('Modified UVs:', Array.from(uvs)) - uvAttribute.needsUpdate = true - return geometry +// 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] + } + } + }] + }]] } -let currentInstancedMesh: THREE.InstancedMesh | null = null -let currentRefCube: THREE.Mesh | null = null +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 (currentInstancedMesh) { - scene.remove(currentInstancedMesh) - currentInstancedMesh.geometry.dispose() + if (currentFullBlockInstancedMesh) { + scene.remove(currentFullBlockInstancedMesh) + currentFullBlockInstancedMesh.geometry.dispose() } - if (currentRefCube) { - scene.remove(currentRefCube) - currentRefCube.geometry.dispose() + if (currentHalfBlockInstancedMesh) { + scene.remove(currentHalfBlockInstancedMesh) + currentHalfBlockInstancedMesh.geometry.dispose() } // Load the blocks atlas texture const textureLoader = new THREE.TextureLoader() - const texture = await new Promise((resolve, reject) => { - textureLoader.load( - globalTexture, - resolve, - undefined, - reject - ) - }) + const texture = await textureLoader.loadAsync(globalTexture) // Configure texture for pixel art texture.magFilter = THREE.NearestFilter @@ -101,7 +146,7 @@ async function createInstancedBlock() { const textureInfo = { u: 0 / atlasWidth, // Left edge (first column) - v: 2 * tileSize / atlasHeight, // Top edge (first row) + v: 2 * tileSize / atlasHeight, // Top edge (third row) su: tileSize / atlasWidth, // Width of one tile sv: tileSize / atlasHeight // Height of one tile } @@ -109,39 +154,96 @@ async function createInstancedBlock() { console.log('Atlas size:', atlasWidth, 'x', atlasHeight) console.log('Calculated texture info:', textureInfo) - // Create custom geometry with proper UV mapping - const geometry = createCustomGeometry(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 instanced mesh using shared material - currentInstancedMesh = new THREE.InstancedMesh(geometry, sharedMaterial, 1) + // 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() - matrix.setPosition(0.5, 0.5, 0.5) // Offset by +0.5 on each axis - currentInstancedMesh.setMatrixAt(0, matrix) - currentInstancedMesh.count = 1 - currentInstancedMesh.instanceMatrix.needsUpdate = true - scene.add(currentInstancedMesh) - // Reference non-instanced cube using same material - currentRefCube = new THREE.Mesh(geometry, sharedMaterial) - currentRefCube.position.set(2.5, 0.5, 0.5) // Offset by +0.5 on each axis - scene.add(currentRefCube) + // First instance (full block) + matrix.setPosition(-1.5, 0.5, 0.5) + currentFullBlockInstancedMesh.setMatrixAt(0, matrix) - console.log('Instanced block created successfully') + // 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 block:', error) + console.error('Error creating instanced blocks:', error) - // Fallback: create a colored cube + // Fallback: create colored cubes const geometry = new THREE.BoxGeometry(1, 1, 1) - const material = new THREE.MeshLambertMaterial({ color: 0xff0000 }) - currentRefCube = new THREE.Mesh(geometry, material) - scene.add(currentRefCube) + 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 instanced block -createInstancedBlock() +createInstancedBlock().then(() => { + render() +}) // Simple render loop (no animation) function render() { @@ -165,7 +267,7 @@ renderer.domElement.addEventListener('mousemove', (event) => { const deltaX = event.clientX - mouseX const deltaY = event.clientY - mouseY - // Rotate camera around the cube + // Rotate camera around the center const spherical = new THREE.Spherical() spherical.setFromVector3(camera.position) spherical.theta -= deltaX * 0.01 From c3aad71024a7d8bd724bf25f0ad2b780432505cc Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 6 Aug 2025 18:02:24 +0200 Subject: [PATCH 27/27] [deploy] send token channel --- src/customChannels.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/customChannels.ts b/src/customChannels.ts index 57c057d5..882c46d0 100644 --- a/src/customChannels.ts +++ b/src/customChannels.ts @@ -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') ?? '' }) }