rm blocks hardcode

This commit is contained in:
Vitaly Turovsky 2025-07-16 02:16:17 +03:00
commit a19d459e8a
7 changed files with 298 additions and 311 deletions

View file

@ -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
}

View file

@ -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({

View file

@ -43,7 +43,7 @@ export class World {
sentBlockStateModels = new Set<string>()
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
instancedBlocks: Record<string, any> = {}
instancedBlockIds = new Map<string, number>()
instancedBlockIds = {} as Record<string, number>
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]) {

View file

@ -160,6 +160,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
abstract changeBackgroundColor (color: [number, number, number]): void
// Optional method for getting instanced blocks data (implemented by Three.js renderer)
getInstancedBlocksData? (): { instanceableBlocks: Set<string>, allBlocksStateIdToModelIdMap: Record<number, number> } | undefined
worldRendererConfig: WorldRendererConfig
playerStateReactive: PlayerStateReactive
playerStateUtils: PlayerStateUtils
@ -596,6 +599,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
const resources = this.resourcesManager.currentResources
if (this.workers.length === 0) throw new Error('workers not initialized yet')
// Get instanceable blocks data if available (Three.js specific)
const instancedBlocksData = this.getInstancedBlocksData?.()
for (const [i, worker] of this.workers.entries()) {
const { blockstatesModels } = resources
@ -607,6 +614,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
},
blockstatesModels,
config: this.getMesherConfig(),
instancedBlocks: instancedBlocksData?.instanceableBlocks ? [...instancedBlocksData.instanceableBlocks] : [],
instancedBlockIds: instancedBlocksData?.allBlocksStateIdToModelIdMap || {}
})
}

View file

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

View file

@ -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<string>
blocksDataModel: Record<string, InstancedBlockModelData>
allBlocksStateIdToModelIdMap: Record<number, number>
interestedTextureTiles: Set<string>
}
export class InstancedRenderer {
private readonly instancedMeshes = new Map<number, THREE.InstancedMesh>()
private readonly blockCounts = new Map<number, number>()
@ -173,9 +58,191 @@ export class InstancedRenderer {
private readonly blockNameToId = new Map<string, number>()
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<string>()
const blocksDataModel = {} as Record<string, InstancedBlockModelData>
const interestedTextureTiles = new Set<string>()
const blocksProcessed = {} as Record<string, boolean>
let i = 0
const allBlocksStateIdToModelIdMap = {} as Record<number, number>
const addBlockModel = (state: number, name: string, props: Record<string, any>, 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
}
}

View file

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