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) {