Compare commits
32 commits
next
...
instancing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3aad71024 |
||
|
|
6324fe50b7 |
||
|
|
c63b6bb536 | ||
|
|
c93b7a6f8b | ||
|
|
be0993a00b | ||
|
|
3e5c036512 | ||
|
|
370f07712b | ||
|
|
0921b40f88 | ||
|
|
96c5ebb379 | ||
|
|
2f49cbb35b | ||
|
|
9ee28ef62f | ||
|
|
0240a752ad | ||
|
|
1c8799242a | ||
|
|
b2257d8ae4 | ||
|
|
5e30a4736e | ||
|
|
83018cd828 | ||
|
|
6868068705 | ||
|
|
dbbe5445d8 | ||
|
|
336aad678b | ||
|
|
3c358c9d22 | ||
|
|
7b06561fc7 | ||
|
|
9f29491b5d | ||
|
|
dbce9e7bec | ||
|
|
6083416943 | ||
|
|
102520233a | ||
|
|
a19d459e8a | ||
|
|
561a18527f | ||
|
|
7ec9d10787 | ||
|
|
7a692ac210 | ||
|
|
52a90ce8ff | ||
|
|
4aebfecf69 | ||
|
|
97f8061b06 |
23 changed files with 2524 additions and 323 deletions
|
|
@ -38,7 +38,6 @@ All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) ar
|
||||||
|
|
||||||
- Controls -> **Touch Controls Type** -> **Joystick**
|
- Controls -> **Touch Controls Type** -> **Joystick**
|
||||||
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
- 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)
|
- 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)
|
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import * as THREE from 'three'
|
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
|
// Create scene, camera and renderer
|
||||||
const scene = new THREE.Scene()
|
const scene = new THREE.Scene()
|
||||||
|
|
@ -8,53 +12,292 @@ renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
document.body.appendChild(renderer.domElement)
|
document.body.appendChild(renderer.domElement)
|
||||||
|
|
||||||
// Position camera
|
// Position camera
|
||||||
camera.position.z = 5
|
camera.position.set(3, 3, 3)
|
||||||
|
camera.lookAt(0, 0, 0)
|
||||||
|
|
||||||
// Create a canvas with some content
|
// Dark background
|
||||||
const canvas = document.createElement('canvas')
|
scene.background = new THREE.Color(0x333333)
|
||||||
canvas.width = 256
|
|
||||||
canvas.height = 256
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
|
|
||||||
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
|
// Add grid helper for orientation
|
||||||
ctx.fillStyle = '#444444'
|
const gridHelper = new THREE.GridHelper(10, 10)
|
||||||
// ctx.fillRect(0, 0, 256, 256)
|
scene.add(gridHelper)
|
||||||
ctx.fillStyle = 'red'
|
|
||||||
ctx.font = '48px Arial'
|
|
||||||
ctx.textAlign = 'center'
|
|
||||||
ctx.textBaseline = 'middle'
|
|
||||||
ctx.fillText('Hello!', 128, 128)
|
|
||||||
|
|
||||||
// Create bitmap and texture
|
// Create shared material that will be used by all blocks
|
||||||
async function createTexturedBox() {
|
const sharedMaterial = new THREE.MeshLambertMaterial({
|
||||||
const canvas2 = new OffscreenCanvas(256, 256)
|
vertexColors: true,
|
||||||
const ctx2 = canvas2.getContext('2d')!
|
transparent: true,
|
||||||
ctx2.drawImage(canvas, 0, 0)
|
alphaTest: 0.1,
|
||||||
const texture = new THREE.Texture(canvas2)
|
// wireframe: true // Add wireframe for debugging
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create simple block models for testing
|
||||||
|
function createFullBlockModel(textureObj: any): any {
|
||||||
|
return [[{
|
||||||
|
elements: [{
|
||||||
|
from: [0, 0, 0],
|
||||||
|
to: [16, 16, 16],
|
||||||
|
faces: {
|
||||||
|
up: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
north: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
south: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
east: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
west: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHalfBlockModel(textureObj: any): any {
|
||||||
|
return [[{
|
||||||
|
elements: [{
|
||||||
|
from: [0, 0, 0],
|
||||||
|
to: [16, 8, 16], // Half height (8 instead of 16)
|
||||||
|
faces: {
|
||||||
|
up: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 16]
|
||||||
|
},
|
||||||
|
north: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 8] // Half height UV
|
||||||
|
},
|
||||||
|
south: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 8] // Half height UV
|
||||||
|
},
|
||||||
|
east: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 8] // Half height UV
|
||||||
|
},
|
||||||
|
west: {
|
||||||
|
texture: textureObj,
|
||||||
|
uv: [0, 0, 16, 8] // Half height UV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentFullBlockInstancedMesh: THREE.InstancedMesh | null = null
|
||||||
|
let currentHalfBlockInstancedMesh: THREE.InstancedMesh | null = null
|
||||||
|
|
||||||
|
async function createInstancedBlock() {
|
||||||
|
try {
|
||||||
|
// Clean up previous meshes if they exist
|
||||||
|
if (currentFullBlockInstancedMesh) {
|
||||||
|
scene.remove(currentFullBlockInstancedMesh)
|
||||||
|
currentFullBlockInstancedMesh.geometry.dispose()
|
||||||
|
}
|
||||||
|
if (currentHalfBlockInstancedMesh) {
|
||||||
|
scene.remove(currentHalfBlockInstancedMesh)
|
||||||
|
currentHalfBlockInstancedMesh.geometry.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the blocks atlas texture
|
||||||
|
const textureLoader = new THREE.TextureLoader()
|
||||||
|
const texture = await textureLoader.loadAsync(globalTexture)
|
||||||
|
|
||||||
|
// Configure texture for pixel art
|
||||||
texture.magFilter = THREE.NearestFilter
|
texture.magFilter = THREE.NearestFilter
|
||||||
texture.minFilter = THREE.NearestFilter
|
texture.minFilter = THREE.NearestFilter
|
||||||
texture.needsUpdate = true
|
texture.generateMipmaps = false
|
||||||
texture.flipY = false
|
texture.flipY = false
|
||||||
|
|
||||||
// Create box with texture
|
// Set the texture on our shared material
|
||||||
const geometry = new THREE.BoxGeometry(2, 2, 2)
|
sharedMaterial.map = texture
|
||||||
const material = new THREE.MeshBasicMaterial({
|
sharedMaterial.needsUpdate = true
|
||||||
map: texture,
|
|
||||||
side: THREE.DoubleSide,
|
console.log('Texture loaded:', texture.image.width, 'x', texture.image.height)
|
||||||
premultipliedAlpha: false,
|
|
||||||
})
|
// Calculate UV coordinates for the first tile (top-left, 16x16)
|
||||||
const cube = new THREE.Mesh(geometry, material)
|
const atlasWidth = texture.image.width
|
||||||
scene.add(cube)
|
const atlasHeight = texture.image.height
|
||||||
|
const tileSize = 16
|
||||||
|
|
||||||
|
const textureInfo = {
|
||||||
|
u: 0 / atlasWidth, // Left edge (first column)
|
||||||
|
v: 2 * tileSize / atlasHeight, // Top edge (third row)
|
||||||
|
su: tileSize / atlasWidth, // Width of one tile
|
||||||
|
sv: tileSize / atlasHeight // Height of one tile
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Atlas size:', atlasWidth, 'x', atlasHeight)
|
||||||
|
console.log('Calculated texture info:', textureInfo)
|
||||||
|
|
||||||
|
// Create mock texture object that matches what the renderer expects
|
||||||
|
const mockTexture = {
|
||||||
|
u: textureInfo.u,
|
||||||
|
v: textureInfo.v,
|
||||||
|
su: textureInfo.su,
|
||||||
|
sv: textureInfo.sv,
|
||||||
|
debugName: 'test_texture'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create block models with the mock texture
|
||||||
|
const fullBlockModel = createFullBlockModel(mockTexture)
|
||||||
|
const halfBlockModel = createHalfBlockModel(mockTexture)
|
||||||
|
|
||||||
|
// Mock data for the renderBlockThree function
|
||||||
|
const mockBlock = undefined // No specific block data needed for this test
|
||||||
|
const mockBiome = 'plains'
|
||||||
|
const mockMcData = {} as any
|
||||||
|
const mockVariants = []
|
||||||
|
const mockNeighbors = {}
|
||||||
|
|
||||||
|
// Render the full block
|
||||||
|
const fullBlockGeometry = renderBlockThree(
|
||||||
|
fullBlockModel,
|
||||||
|
mockBlock,
|
||||||
|
mockBiome,
|
||||||
|
mockMcData,
|
||||||
|
mockVariants,
|
||||||
|
mockNeighbors
|
||||||
|
)
|
||||||
|
|
||||||
|
// Render the half block
|
||||||
|
const halfBlockGeometry = renderBlockThree(
|
||||||
|
halfBlockModel,
|
||||||
|
mockBlock,
|
||||||
|
mockBiome,
|
||||||
|
mockMcData,
|
||||||
|
mockVariants,
|
||||||
|
mockNeighbors
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create instanced mesh for full blocks
|
||||||
|
currentFullBlockInstancedMesh = new THREE.InstancedMesh(fullBlockGeometry, sharedMaterial, 2) // Support 2 instances
|
||||||
|
const matrix = new THREE.Matrix4()
|
||||||
|
|
||||||
|
// First instance (full block)
|
||||||
|
matrix.setPosition(-1.5, 0.5, 0.5)
|
||||||
|
currentFullBlockInstancedMesh.setMatrixAt(0, matrix)
|
||||||
|
|
||||||
|
// Second instance (full block)
|
||||||
|
matrix.setPosition(1.5, 0.5, 0.5)
|
||||||
|
currentFullBlockInstancedMesh.setMatrixAt(1, matrix)
|
||||||
|
|
||||||
|
currentFullBlockInstancedMesh.count = 2
|
||||||
|
currentFullBlockInstancedMesh.instanceMatrix.needsUpdate = true
|
||||||
|
scene.add(currentFullBlockInstancedMesh)
|
||||||
|
|
||||||
|
// Create instanced mesh for half blocks
|
||||||
|
currentHalfBlockInstancedMesh = new THREE.InstancedMesh(halfBlockGeometry, sharedMaterial, 1) // Support 1 instance
|
||||||
|
const halfMatrix = new THREE.Matrix4()
|
||||||
|
|
||||||
|
// Half block instance
|
||||||
|
halfMatrix.setPosition(0, 0.75, 0.5) // Positioned higher so top aligns with full blocks
|
||||||
|
currentHalfBlockInstancedMesh.setMatrixAt(0, halfMatrix)
|
||||||
|
|
||||||
|
currentHalfBlockInstancedMesh.count = 1
|
||||||
|
currentHalfBlockInstancedMesh.instanceMatrix.needsUpdate = true
|
||||||
|
scene.add(currentHalfBlockInstancedMesh)
|
||||||
|
|
||||||
|
console.log('Instanced blocks created successfully')
|
||||||
|
console.log('Full block geometry:', fullBlockGeometry)
|
||||||
|
console.log('Half block geometry:', halfBlockGeometry)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating instanced blocks:', error)
|
||||||
|
|
||||||
|
// Fallback: create colored cubes
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||||
|
const material = new THREE.MeshLambertMaterial({ color: 0xff0000, wireframe: true })
|
||||||
|
const fallbackMesh = new THREE.Mesh(geometry, material)
|
||||||
|
fallbackMesh.position.set(0, 0.5, 0.5)
|
||||||
|
scene.add(fallbackMesh)
|
||||||
|
|
||||||
|
console.log('Created fallback colored cube')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the textured box
|
// Create the instanced block
|
||||||
createTexturedBox()
|
createInstancedBlock().then(() => {
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
|
||||||
// Animation loop
|
// Simple render loop (no animation)
|
||||||
function animate() {
|
function render() {
|
||||||
requestAnimationFrame(animate)
|
renderer.render(scene, camera)
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
}
|
||||||
animate()
|
|
||||||
|
// Add mouse controls for better viewing
|
||||||
|
let mouseDown = false
|
||||||
|
let mouseX = 0
|
||||||
|
let mouseY = 0
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('mousedown', (event) => {
|
||||||
|
mouseDown = true
|
||||||
|
mouseX = event.clientX
|
||||||
|
mouseY = event.clientY
|
||||||
|
})
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('mousemove', (event) => {
|
||||||
|
if (!mouseDown) return
|
||||||
|
|
||||||
|
const deltaX = event.clientX - mouseX
|
||||||
|
const deltaY = event.clientY - mouseY
|
||||||
|
|
||||||
|
// Rotate camera around the center
|
||||||
|
const spherical = new THREE.Spherical()
|
||||||
|
spherical.setFromVector3(camera.position)
|
||||||
|
spherical.theta -= deltaX * 0.01
|
||||||
|
spherical.phi += deltaY * 0.01
|
||||||
|
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
|
||||||
|
|
||||||
|
camera.position.setFromSpherical(spherical)
|
||||||
|
camera.lookAt(0, 0, 0)
|
||||||
|
|
||||||
|
mouseX = event.clientX
|
||||||
|
mouseY = event.clientY
|
||||||
|
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('mouseup', () => {
|
||||||
|
mouseDown = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add button to recreate blocks (for testing)
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.textContent = 'Recreate Blocks'
|
||||||
|
button.style.position = 'fixed'
|
||||||
|
button.style.top = '10px'
|
||||||
|
button.style.left = '10px'
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
createInstancedBlock()
|
||||||
|
render()
|
||||||
|
})
|
||||||
|
document.body.appendChild(button)
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
render()
|
||||||
|
|
|
||||||
7
renderer/viewer/lib/mesher/instancingUtils.ts
Normal file
7
renderer/viewer/lib/mesher/instancingUtils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { WorldBlock as Block, World } from './world'
|
||||||
|
|
||||||
|
export const isBlockInstanceable = (world: World, block: Block): boolean => {
|
||||||
|
const instancedBlocks = world?.instancedBlocks
|
||||||
|
if (!instancedBlocks) return false
|
||||||
|
return instancedBlocks.includes(block.stateId)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import { World } from './world'
|
import { World } from './world'
|
||||||
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
||||||
import { BlockStateModelInfo } from './shared'
|
import { BlockStateModelInfo, InstancingMode } from './shared'
|
||||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||||
|
|
||||||
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
||||||
|
|
@ -17,7 +17,7 @@ if (module.require) {
|
||||||
|
|
||||||
let workerIndex = 0
|
let workerIndex = 0
|
||||||
let world: World
|
let world: World
|
||||||
let dirtySections = new Map<string, number>()
|
let dirtySections = new Map<string, { key: string, instancingMode: InstancingMode, times: number }>()
|
||||||
let allDataReady = false
|
let allDataReady = false
|
||||||
|
|
||||||
function sectionKey (x, y, z) {
|
function sectionKey (x, y, z) {
|
||||||
|
|
@ -47,7 +47,7 @@ function drainQueue (from, to) {
|
||||||
queuedMessages = queuedMessages.slice(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 x = Math.floor(pos.x / 16) * 16
|
||||||
const y = Math.floor(pos.y / 16) * 16
|
const y = Math.floor(pos.y / 16) * 16
|
||||||
const z = Math.floor(pos.z / 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)
|
const chunk = world.getColumn(x, z)
|
||||||
if (chunk?.getSection(pos)) {
|
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 {
|
} else {
|
||||||
postMessage({ type: 'sectionFinished', key, workerIndex })
|
postMessage({ type: 'sectionFinished', key, workerIndex })
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +68,7 @@ function setSectionDirty (pos, value = true) {
|
||||||
|
|
||||||
const softCleanup = () => {
|
const softCleanup = () => {
|
||||||
// clean block cache and loaded chunks
|
// clean block cache and loaded chunks
|
||||||
world = new World(world.config.version)
|
world.blockCache = {}
|
||||||
globalThis.world = world
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMessage = data => {
|
const handleMessage = data => {
|
||||||
|
|
@ -98,12 +97,14 @@ const handleMessage = data => {
|
||||||
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
||||||
allDataReady = true
|
allDataReady = true
|
||||||
workerIndex = data.workerIndex
|
workerIndex = data.workerIndex
|
||||||
|
world.instancedBlocks = data.instancedBlocks
|
||||||
|
world.instancedBlockIds = data.instancedBlockIds || {}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'dirty': {
|
case 'dirty': {
|
||||||
const loc = new Vec3(data.x, data.y, data.z)
|
const loc = new Vec3(data.x, data.y, data.z)
|
||||||
setSectionDirty(loc, data.value)
|
setSectionDirty(loc, data.value, data.instancingMode || InstancingMode.None)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -190,6 +191,18 @@ self.onmessage = ({ data }) => {
|
||||||
handleMessage(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(() => {
|
setInterval(() => {
|
||||||
if (world === null || !allDataReady) return
|
if (world === null || !allDataReady) return
|
||||||
|
|
||||||
|
|
@ -197,23 +210,37 @@ setInterval(() => {
|
||||||
// console.log(sections.length + ' dirty sections')
|
// console.log(sections.length + ' dirty sections')
|
||||||
|
|
||||||
// const start = performance.now()
|
// 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 [x, y, z] = key.split(',').map(v => parseInt(v, 10))
|
||||||
const chunk = world.getColumn(x, z)
|
const chunk = world.getColumn(x, z)
|
||||||
let processTime = 0
|
let processTime = 0
|
||||||
if (chunk?.getSection(new Vec3(x, y, z))) {
|
if (chunk?.getSection(new Vec3(x, y, z))) {
|
||||||
const start = performance.now()
|
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)
|
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) as any
|
||||||
//@ts-expect-error
|
postMessage({ type: 'geometry', key, geometry, workerIndex }/* , transferable */)
|
||||||
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
|
|
||||||
processTime = performance.now() - start
|
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 {
|
} else {
|
||||||
// console.info('[mesher] Missing section', x, y, z)
|
// console.info('[mesher] Missing section', x, y, z)
|
||||||
}
|
}
|
||||||
const dirtyTimes = dirtySections.get(key)
|
const dirtyTimes = dirtySections.get(key)
|
||||||
if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy')
|
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 })
|
postMessage({ type: 'sectionFinished', key, workerIndex, processTime })
|
||||||
processTime = 0
|
processTime = 0
|
||||||
}
|
}
|
||||||
|
|
@ -237,3 +264,24 @@ setInterval(() => {
|
||||||
// const time = performance.now() - start
|
// const time = performance.now() - start
|
||||||
// console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`)
|
// console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`)
|
||||||
}, 50)
|
}, 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)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { BlockType } from '../../../playground/shared'
|
||||||
import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock } from './world'
|
import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock } from './world'
|
||||||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||||
import { MesherGeometryOutput, HighestBlockInfo } from './shared'
|
import { MesherGeometryOutput, InstancingMode } from './shared'
|
||||||
|
import { isBlockInstanceable } from './instancingUtils'
|
||||||
|
|
||||||
let blockProvider: WorldBlockProvider
|
let blockProvider: WorldBlockProvider
|
||||||
|
|
||||||
|
|
@ -132,7 +132,11 @@ const getVec = (v: Vec3, dir: Vec3) => {
|
||||||
return v.plus(dir)
|
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[] = []
|
const heights: number[] = []
|
||||||
for (let z = -1; z <= 1; z++) {
|
for (let z = -1; z <= 1; z++) {
|
||||||
for (let x = -1; x <= 1; x++) {
|
for (let x = -1; x <= 1; x++) {
|
||||||
|
|
@ -518,10 +522,64 @@ const isBlockWaterlogged = (block: Block) => {
|
||||||
return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name)
|
return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boolean => {
|
||||||
|
// Early return for blocks that should never be culled
|
||||||
|
if (block.transparent) return false
|
||||||
|
|
||||||
|
// Check if all 6 faces would be culled (hidden by neighbors)
|
||||||
|
const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice')
|
||||||
|
|
||||||
|
// Cache cursor offset to avoid creating new Vec3 instances
|
||||||
|
const offsetCursor = new Vec3(0, 0, 0)
|
||||||
|
|
||||||
|
// eslint-disable-next-line guard-for-in
|
||||||
|
for (const face in elemFaces) {
|
||||||
|
const { dir } = elemFaces[face]
|
||||||
|
offsetCursor.set(cursor.x + dir[0], cursor.y + dir[1], cursor.z + dir[2])
|
||||||
|
const neighbor = world.getBlock(offsetCursor, blockProvider, {})
|
||||||
|
|
||||||
|
// Face is exposed to air/void - block must be rendered
|
||||||
|
if (!neighbor) return false
|
||||||
|
|
||||||
|
// Handle special case for identical blocks (glass/ice)
|
||||||
|
if (cullIfIdentical && neighbor.stateId === block.stateId) continue
|
||||||
|
|
||||||
|
// If neighbor is not a full opaque cube, face is visible
|
||||||
|
if (neighbor.transparent || !isCube(neighbor)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// All faces are culled, block should not be rendered
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add matrix calculation helper
|
||||||
|
function calculateInstanceMatrix (pos: { x: number, y: number, z: number }, offset = 0.5): number[] {
|
||||||
|
// Create a 4x4 matrix array (16 elements)
|
||||||
|
const matrix = Array.from({ length: 16 }).fill(0) as number[]
|
||||||
|
|
||||||
|
// Set identity matrix
|
||||||
|
matrix[0] = 1 // m11
|
||||||
|
matrix[5] = 1 // m22
|
||||||
|
matrix[10] = 1 // m33
|
||||||
|
matrix[15] = 1 // m44
|
||||||
|
|
||||||
|
// Set translation (position)
|
||||||
|
matrix[12] = pos.x + offset // tx
|
||||||
|
matrix[13] = pos.y + offset // ty
|
||||||
|
matrix[14] = pos.z + offset // tz
|
||||||
|
|
||||||
|
return matrix
|
||||||
|
}
|
||||||
|
|
||||||
let unknownBlockModel: BlockModelPartsResolved
|
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>
|
let delayedRender = [] as Array<() => void>
|
||||||
|
|
||||||
|
// Check if instanced rendering is enabled for this section
|
||||||
|
const enableInstancedRendering = instancingMode !== InstancingMode.None
|
||||||
|
const forceInstancedOnly = instancingMode === InstancingMode.BlockInstancingOnly || instancingMode === InstancingMode.ColorOnly
|
||||||
|
|
||||||
const attr: MesherGeometryOutput = {
|
const attr: MesherGeometryOutput = {
|
||||||
sx: sx + 8,
|
sx: sx + 8,
|
||||||
sy: sy + 8,
|
sy: sy + 8,
|
||||||
|
|
@ -543,7 +601,8 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
||||||
signs: {},
|
signs: {},
|
||||||
// isFull: true,
|
// isFull: true,
|
||||||
hadErrors: false,
|
hadErrors: false,
|
||||||
blocksCount: 0
|
blocksCount: 0,
|
||||||
|
instancedBlocks: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursor = new Vec3(0, 0, 0)
|
const cursor = new Vec3(0, 0, 0)
|
||||||
|
|
@ -606,14 +665,56 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
||||||
const pos = cursor.clone()
|
const pos = cursor.clone()
|
||||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||||
delayedRender.push(() => {
|
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++
|
attr.blocksCount++
|
||||||
} else if (block.name === 'lava') {
|
} 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++
|
attr.blocksCount++
|
||||||
}
|
}
|
||||||
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
|
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
|
||||||
|
// Check if this block can use instanced rendering
|
||||||
|
if ((enableInstancedRendering && isBlockInstanceable(world, block))/* || forceInstancedOnly */) {
|
||||||
|
// Check if block should be culled (all faces hidden by neighbors)
|
||||||
|
// TODO validate this
|
||||||
|
if (shouldCullInstancedBlock(world, cursor, block)) {
|
||||||
|
// Block is completely surrounded, skip rendering
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockKey = block.name
|
||||||
|
if (!attr.instancedBlocks[blockKey]) {
|
||||||
|
attr.instancedBlocks[blockKey] = {
|
||||||
|
stateId: block.stateId,
|
||||||
|
blockName: block.name,
|
||||||
|
positions: [],
|
||||||
|
matrices: [] // Add matrices array
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = {
|
||||||
|
x: cursor.x,
|
||||||
|
y: cursor.y,
|
||||||
|
z: cursor.z
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-calculate transformation matrix
|
||||||
|
const offset = instancingMode === InstancingMode.ColorOnly ? 0 : 0.5
|
||||||
|
const matrix = calculateInstanceMatrix(pos, offset)
|
||||||
|
|
||||||
|
attr.instancedBlocks[blockKey].positions.push(pos)
|
||||||
|
attr.instancedBlocks[blockKey].matrices.push(matrix)
|
||||||
|
|
||||||
|
attr.blocksCount++
|
||||||
|
continue // Skip regular geometry generation for instanceable blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip buffer geometry generation if force instanced only mode is enabled
|
||||||
|
if (forceInstancedOnly) {
|
||||||
|
// In force instanced only mode, skip all non-instanceable blocks
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
let { models } = block
|
let { models } = block
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import { BlockType } from '../../../playground/shared'
|
import { BlockType } from '../../../playground/shared'
|
||||||
|
|
||||||
|
export enum InstancingMode {
|
||||||
|
None = 'none',
|
||||||
|
ColorOnly = 'color_only',
|
||||||
|
BlockInstancing = 'block_instancing',
|
||||||
|
BlockInstancingOnly = 'block_instancing_only'
|
||||||
|
}
|
||||||
|
|
||||||
// only here for easier testing
|
// only here for easier testing
|
||||||
export const defaultMesherConfig = {
|
export const defaultMesherConfig = {
|
||||||
version: '',
|
version: '',
|
||||||
|
|
@ -12,7 +19,7 @@ export const defaultMesherConfig = {
|
||||||
// textureSize: 1024, // for testing
|
// textureSize: 1024, // for testing
|
||||||
debugModelVariant: undefined as undefined | number[],
|
debugModelVariant: undefined as undefined | number[],
|
||||||
clipWorldBelowY: undefined as undefined | number,
|
clipWorldBelowY: undefined as undefined | number,
|
||||||
disableSignsMapsSupport: false
|
disableSignsMapsSupport: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomBlockModels = {
|
export type CustomBlockModels = {
|
||||||
|
|
@ -21,6 +28,19 @@ export type CustomBlockModels = {
|
||||||
|
|
||||||
export type MesherConfig = typeof defaultMesherConfig
|
export type MesherConfig = typeof defaultMesherConfig
|
||||||
|
|
||||||
|
export interface InstancedBlockEntry {
|
||||||
|
stateId: number
|
||||||
|
blockName: string
|
||||||
|
positions: Array<{ x: number, y: number, z: number }>
|
||||||
|
matrices: number[][] // Pre-calculated transformation matrices from worker
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstancingMesherData = {
|
||||||
|
blocks: {
|
||||||
|
[stateId: number]: number // instance id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type MesherGeometryOutput = {
|
export type MesherGeometryOutput = {
|
||||||
sx: number,
|
sx: number,
|
||||||
sy: number,
|
sy: number,
|
||||||
|
|
@ -45,6 +65,8 @@ export type MesherGeometryOutput = {
|
||||||
hadErrors: boolean
|
hadErrors: boolean
|
||||||
blocksCount: number
|
blocksCount: number
|
||||||
customBlockModels?: CustomBlockModels
|
customBlockModels?: CustomBlockModels
|
||||||
|
// New instanced blocks data
|
||||||
|
instancedBlocks: Record<string, InstancedBlockEntry>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MesherMainEvents {
|
export interface MesherMainEvents {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
|
||||||
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||||
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
|
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
|
||||||
import legacyJson from '../../../../src/preflatMap.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'
|
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||||
|
|
||||||
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
||||||
|
|
@ -42,12 +42,14 @@ export class World {
|
||||||
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels
|
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels
|
||||||
sentBlockStateModels = new Set<string>()
|
sentBlockStateModels = new Set<string>()
|
||||||
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
|
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
|
||||||
|
instancedBlocks: number[] = []
|
||||||
|
instancedBlockIds = {} as Record<number, number>
|
||||||
|
|
||||||
constructor (version) {
|
constructor (version: string) {
|
||||||
this.Chunk = Chunks(version) as any
|
this.Chunk = Chunks(version) as any
|
||||||
this.biomeCache = mcData(version).biomes
|
this.biomeCache = mcData(version).biomes
|
||||||
this.preflat = !mcData(version).supportFeature('blockStateId')
|
this.preflat = !mcData(version).supportFeature('blockStateId')
|
||||||
this.config.version = version
|
this.config = { ...defaultMesherConfig, version }
|
||||||
}
|
}
|
||||||
|
|
||||||
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
|
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
|
||||||
|
|
@ -121,7 +123,6 @@ export class World {
|
||||||
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
|
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 key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
|
||||||
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
|
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
|
||||||
const modelOverride = this.customBlockModels.get(key)?.[blockPosKey]
|
|
||||||
|
|
||||||
const column = this.columns[key]
|
const column = this.columns[key]
|
||||||
// null column means chunk not loaded
|
// null column means chunk not loaded
|
||||||
|
|
@ -131,6 +132,7 @@ export class World {
|
||||||
const locInChunk = posInChunk(loc)
|
const locInChunk = posInChunk(loc)
|
||||||
const stateId = column.getBlockStateId(locInChunk)
|
const stateId = column.getBlockStateId(locInChunk)
|
||||||
|
|
||||||
|
const modelOverride = stateId ? this.customBlockModels.get(key)?.[blockPosKey] : undefined
|
||||||
const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride)
|
const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride)
|
||||||
|
|
||||||
if (!this.blockCache[cacheKey]) {
|
if (!this.blockCache[cacheKey]) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export type WorldDataEmitterEvents = {
|
||||||
entityMoved: (data: any) => void
|
entityMoved: (data: any) => void
|
||||||
playerEntity: (data: any) => void
|
playerEntity: (data: any) => void
|
||||||
time: (data: number) => void
|
time: (data: number) => void
|
||||||
renderDistance: (viewDistance: number) => void
|
renderDistance: (viewDistance: number, keepChunksDistance: number) => void
|
||||||
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
|
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
|
||||||
markAsLoaded: (data: { x: number, z: number }) => void
|
markAsLoaded: (data: { x: number, z: number }) => void
|
||||||
unloadChunk: (data: { x: number, z: number }) => void
|
unloadChunk: (data: { x: number, z: number }) => void
|
||||||
|
|
@ -79,7 +79,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
||||||
|
|
||||||
updateViewDistance (viewDistance: number) {
|
updateViewDistance (viewDistance: number) {
|
||||||
this.viewDistance = viewDistance
|
this.viewDistance = viewDistance
|
||||||
this.emitter.emit('renderDistance', viewDistance)
|
this.emitter.emit('renderDistance', viewDistance, this.keepChunksDistance)
|
||||||
}
|
}
|
||||||
|
|
||||||
listenToBot (bot: typeof __type_bot) {
|
listenToBot (bot: typeof __type_bot) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
|
||||||
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||||
import { SoundSystem } from '../three/threeJsSound'
|
import { SoundSystem } from '../three/threeJsSound'
|
||||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
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 { chunkPos } from './simpleUtils'
|
||||||
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
|
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
|
||||||
import { WorldDataEmitterWorker } from './worldDataEmitter'
|
import { WorldDataEmitterWorker } from './worldDataEmitter'
|
||||||
|
|
@ -56,7 +56,16 @@ export const defaultWorldRendererConfig = {
|
||||||
enableDebugOverlay: false,
|
enableDebugOverlay: false,
|
||||||
_experimentalSmoothChunkLoading: true,
|
_experimentalSmoothChunkLoading: true,
|
||||||
_renderByChunks: false,
|
_renderByChunks: false,
|
||||||
volume: 1
|
volume: 1,
|
||||||
|
// New instancing options
|
||||||
|
useInstancedRendering: false,
|
||||||
|
forceInstancedOnly: false,
|
||||||
|
dynamicInstancing: false,
|
||||||
|
dynamicInstancingModeDistance: 1, // chunks beyond this distance use instancing only
|
||||||
|
dynamicColorModeDistance: 1, // chunks beyond this distance use color mode only
|
||||||
|
instancedOnlyDistance: 6, // chunks beyond this distance use instancing only
|
||||||
|
enableSingleColorMode: false, // ultra-performance mode with solid colors
|
||||||
|
autoLowerRenderDistance: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||||
|
|
@ -155,6 +164,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
|
|
||||||
abstract changeBackgroundColor (color: [number, number, number]): void
|
abstract changeBackgroundColor (color: [number, number, number]): void
|
||||||
|
|
||||||
|
// Optional method for getting instanced blocks data (implemented by Three.js renderer)
|
||||||
|
getInstancedBlocksData? (): { instanceableBlocks?: Set<number>, allBlocksStateIdToModelIdMap?: Record<number, number> } | undefined
|
||||||
|
|
||||||
worldRendererConfig: WorldRendererConfig
|
worldRendererConfig: WorldRendererConfig
|
||||||
playerStateReactive: PlayerStateReactive
|
playerStateReactive: PlayerStateReactive
|
||||||
playerStateUtils: PlayerStateUtils
|
playerStateUtils: PlayerStateUtils
|
||||||
|
|
@ -476,7 +488,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
this.allChunksFinished = true
|
this.allChunksFinished = true
|
||||||
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
|
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
|
||||||
}
|
}
|
||||||
this.updateChunksStats()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
||||||
|
|
@ -591,6 +602,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
const resources = this.resourcesManager.currentResources
|
const resources = this.resourcesManager.currentResources
|
||||||
|
|
||||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
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()) {
|
for (const [i, worker] of this.workers.entries()) {
|
||||||
const { blockstatesModels } = resources
|
const { blockstatesModels } = resources
|
||||||
|
|
||||||
|
|
@ -602,6 +617,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
},
|
},
|
||||||
blockstatesModels,
|
blockstatesModels,
|
||||||
config: this.getMesherConfig(),
|
config: this.getMesherConfig(),
|
||||||
|
instancedBlocks: instancedBlocksData?.instanceableBlocks ? [...instancedBlocksData.instanceableBlocks] : [],
|
||||||
|
instancedBlockIds: instancedBlocksData?.allBlocksStateIdToModelIdMap || {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -670,6 +687,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
this.checkAllFinished()
|
this.checkAllFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugRemoveCurrentChunk () {
|
||||||
|
const [x, z] = chunkPos(this.viewerChunkPosition!)
|
||||||
|
this.removeColumn(x, z)
|
||||||
|
}
|
||||||
|
|
||||||
removeColumn (x, z) {
|
removeColumn (x, z) {
|
||||||
delete this.loadedChunks[`${x},${z}`]
|
delete this.loadedChunks[`${x},${z}`]
|
||||||
for (const worker of this.workers) {
|
for (const worker of this.workers) {
|
||||||
|
|
@ -915,7 +937,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
return Promise.all(data)
|
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.forceCallFromMesherReplayer && this.mesherLogReader) return
|
||||||
|
|
||||||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||||
|
|
@ -929,7 +951,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
// Dispatch sections to workers based on position
|
// Dispatch sections to workers based on position
|
||||||
// This guarantees uniformity accross workers and that a given section
|
// This guarantees uniformity accross workers and that a given section
|
||||||
// is always dispatched to the same worker
|
// 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)
|
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
|
||||||
if (this.forceCallFromMesherReplayer) {
|
if (this.forceCallFromMesherReplayer) {
|
||||||
this.workers[hash].postMessage({
|
this.workers[hash].postMessage({
|
||||||
|
|
@ -938,17 +960,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
||||||
y: pos.y,
|
y: pos.y,
|
||||||
z: pos.z,
|
z: pos.z,
|
||||||
value,
|
value,
|
||||||
|
instancingMode,
|
||||||
config: this.getMesherConfig(),
|
config: this.getMesherConfig(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.toWorkerMessagesQueue[hash] ??= []
|
this.toWorkerMessagesQueue[hash] ??= []
|
||||||
this.toWorkerMessagesQueue[hash].push({
|
this.toWorkerMessagesQueue[hash].push({
|
||||||
// this.workers[hash].postMessage({
|
|
||||||
type: 'dirty',
|
type: 'dirty',
|
||||||
x: pos.x,
|
x: pos.x,
|
||||||
y: pos.y,
|
y: pos.y,
|
||||||
z: pos.z,
|
z: pos.z,
|
||||||
value,
|
value,
|
||||||
|
instancingMode,
|
||||||
config: this.getMesherConfig(),
|
config: this.getMesherConfig(),
|
||||||
})
|
})
|
||||||
this.dispatchMessages()
|
this.dispatchMessages()
|
||||||
|
|
|
||||||
743
renderer/viewer/three/chunkMeshManager.ts
Normal file
743
renderer/viewer/three/chunkMeshManager.ts
Normal file
|
|
@ -0,0 +1,743 @@
|
||||||
|
import PrismarineChatLoader from 'prismarine-chat'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import * as nbt from 'prismarine-nbt'
|
||||||
|
import { Vec3 } from 'vec3'
|
||||||
|
import { MesherGeometryOutput } from '../lib/mesher/shared'
|
||||||
|
import { chunkPos } from '../lib/simpleUtils'
|
||||||
|
import { renderSign } from '../sign-renderer'
|
||||||
|
import { getMesh } from './entity/EntityMesh'
|
||||||
|
import type { WorldRendererThree } from './worldrendererThree'
|
||||||
|
import { armorModel } from './entity/armorModels'
|
||||||
|
import { disposeObject } from './threeJsUtils'
|
||||||
|
|
||||||
|
export interface ChunkMeshPool {
|
||||||
|
mesh: THREE.Mesh
|
||||||
|
inUse: boolean
|
||||||
|
lastUsedTime: number
|
||||||
|
sectionKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SectionObject extends THREE.Group {
|
||||||
|
mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
|
||||||
|
tilesCount?: number
|
||||||
|
blocksCount?: number
|
||||||
|
|
||||||
|
signsContainer?: THREE.Group
|
||||||
|
headsContainer?: THREE.Group
|
||||||
|
boxHelper?: THREE.BoxHelper
|
||||||
|
fountain?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChunkMeshManager {
|
||||||
|
private readonly meshPool: ChunkMeshPool[] = []
|
||||||
|
private readonly activeSections = new Map<string, ChunkMeshPool>()
|
||||||
|
readonly sectionObjects: Record<string, SectionObject> = {}
|
||||||
|
private poolSize: number
|
||||||
|
private maxPoolSize: number
|
||||||
|
private minPoolSize: number
|
||||||
|
private readonly signHeadsRenderer: SignHeadsRenderer
|
||||||
|
|
||||||
|
// Performance tracking
|
||||||
|
private hits = 0
|
||||||
|
private misses = 0
|
||||||
|
|
||||||
|
// Debug flag to bypass pooling
|
||||||
|
public bypassPooling = false
|
||||||
|
|
||||||
|
// Performance monitoring
|
||||||
|
private readonly renderTimes: number[] = []
|
||||||
|
private readonly maxRenderTimeSamples = 30
|
||||||
|
private _performanceOverrideDistance?: number
|
||||||
|
private lastPerformanceCheck = 0
|
||||||
|
private readonly performanceCheckInterval = 2000 // Check every 2 seconds
|
||||||
|
|
||||||
|
get performanceOverrideDistance () {
|
||||||
|
return this._performanceOverrideDistance ?? 0
|
||||||
|
}
|
||||||
|
set performanceOverrideDistance (value: number | undefined) {
|
||||||
|
this._performanceOverrideDistance = value
|
||||||
|
this.updateSectionsVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
public worldRenderer: WorldRendererThree,
|
||||||
|
public scene: THREE.Group,
|
||||||
|
public material: THREE.Material,
|
||||||
|
public worldHeight: number,
|
||||||
|
viewDistance = 3,
|
||||||
|
) {
|
||||||
|
this.updateViewDistance(viewDistance)
|
||||||
|
this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer)
|
||||||
|
|
||||||
|
this.initializePool()
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializePool () {
|
||||||
|
// Create initial pool
|
||||||
|
for (let i = 0; i < this.poolSize; i++) {
|
||||||
|
const geometry = new THREE.BufferGeometry()
|
||||||
|
const mesh = new THREE.Mesh(geometry, this.material)
|
||||||
|
mesh.visible = false
|
||||||
|
mesh.matrixAutoUpdate = false
|
||||||
|
mesh.name = 'pooled-section-mesh'
|
||||||
|
|
||||||
|
const poolEntry: ChunkMeshPool = {
|
||||||
|
mesh,
|
||||||
|
inUse: false,
|
||||||
|
lastUsedTime: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.meshPool.push(poolEntry)
|
||||||
|
// Don't add to scene here - meshes will be added to containers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update or create a section with new geometry data
|
||||||
|
*/
|
||||||
|
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
|
||||||
|
// Remove existing section object from scene if it exists
|
||||||
|
let sectionObject = this.sectionObjects[sectionKey]
|
||||||
|
if (sectionObject) {
|
||||||
|
this.cleanupSection(sectionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create mesh from pool
|
||||||
|
let poolEntry = this.activeSections.get(sectionKey)
|
||||||
|
if (!poolEntry) {
|
||||||
|
poolEntry = this.acquireMesh()
|
||||||
|
if (!poolEntry) {
|
||||||
|
console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSections.set(sectionKey, poolEntry)
|
||||||
|
poolEntry.sectionKey = sectionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mesh } = poolEntry
|
||||||
|
|
||||||
|
// Update geometry attributes efficiently
|
||||||
|
this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
|
||||||
|
this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
|
||||||
|
this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
|
||||||
|
this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
|
||||||
|
|
||||||
|
// Use direct index assignment for better performance (like before)
|
||||||
|
mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
|
||||||
|
|
||||||
|
// Set bounding box and sphere for the 16x16x16 section
|
||||||
|
mesh.geometry.boundingBox = new THREE.Box3(
|
||||||
|
new THREE.Vector3(-8, -8, -8),
|
||||||
|
new THREE.Vector3(8, 8, 8)
|
||||||
|
)
|
||||||
|
mesh.geometry.boundingSphere = new THREE.Sphere(
|
||||||
|
new THREE.Vector3(0, 0, 0),
|
||||||
|
Math.sqrt(3 * 8 ** 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position the mesh
|
||||||
|
mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
|
||||||
|
mesh.updateMatrix()
|
||||||
|
mesh.visible = true
|
||||||
|
mesh.name = 'mesh'
|
||||||
|
|
||||||
|
poolEntry.lastUsedTime = performance.now()
|
||||||
|
|
||||||
|
// Create or update the section object container
|
||||||
|
sectionObject = new THREE.Group() as SectionObject
|
||||||
|
sectionObject.add(mesh)
|
||||||
|
sectionObject.mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
|
||||||
|
|
||||||
|
// Store metadata
|
||||||
|
sectionObject.tilesCount = geometryData.positions.length / 3 / 4
|
||||||
|
sectionObject.blocksCount = geometryData.blocksCount
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add signs container
|
||||||
|
if (Object.keys(geometryData.signs).length > 0) {
|
||||||
|
const signsContainer = new THREE.Group()
|
||||||
|
signsContainer.name = 'signs'
|
||||||
|
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometryData.signs)) {
|
||||||
|
const signBlockEntity = this.worldRenderer.blockEntities[posKey]
|
||||||
|
if (!signBlockEntity) continue
|
||||||
|
const [x, y, z] = posKey.split(',')
|
||||||
|
const sign = this.signHeadsRenderer.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
|
||||||
|
if (!sign) continue
|
||||||
|
signsContainer.add(sign)
|
||||||
|
}
|
||||||
|
sectionObject.add(signsContainer)
|
||||||
|
sectionObject.signsContainer = signsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add heads container
|
||||||
|
if (Object.keys(geometryData.heads).length > 0) {
|
||||||
|
const headsContainer = new THREE.Group()
|
||||||
|
headsContainer.name = 'heads'
|
||||||
|
for (const [posKey, { isWall, rotation }] of Object.entries(geometryData.heads)) {
|
||||||
|
const headBlockEntity = this.worldRenderer.blockEntities[posKey]
|
||||||
|
if (!headBlockEntity) continue
|
||||||
|
const [x, y, z] = posKey.split(',')
|
||||||
|
const head = this.signHeadsRenderer.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
|
||||||
|
if (!head) continue
|
||||||
|
headsContainer.add(head)
|
||||||
|
}
|
||||||
|
sectionObject.add(headsContainer)
|
||||||
|
sectionObject.headsContainer = headsContainer
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('ChunkMeshManager: Error adding signs or heads to section', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store and add to scene
|
||||||
|
this.sectionObjects[sectionKey] = sectionObject
|
||||||
|
this.scene.add(sectionObject)
|
||||||
|
sectionObject.matrixAutoUpdate = false
|
||||||
|
|
||||||
|
return sectionObject
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupSection (sectionKey: string) {
|
||||||
|
// Remove section object from scene
|
||||||
|
const sectionObject = this.sectionObjects[sectionKey]
|
||||||
|
if (sectionObject) {
|
||||||
|
this.scene.remove(sectionObject)
|
||||||
|
// Dispose signs and heads containers
|
||||||
|
if (sectionObject.signsContainer) {
|
||||||
|
this.disposeContainer(sectionObject.signsContainer)
|
||||||
|
}
|
||||||
|
if (sectionObject.headsContainer) {
|
||||||
|
this.disposeContainer(sectionObject.headsContainer)
|
||||||
|
}
|
||||||
|
delete this.sectionObjects[sectionKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a section and return its mesh to the pool
|
||||||
|
*/
|
||||||
|
releaseSection (sectionKey: string): boolean {
|
||||||
|
this.cleanupSection(sectionKey)
|
||||||
|
|
||||||
|
const poolEntry = this.activeSections.get(sectionKey)
|
||||||
|
if (!poolEntry) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide mesh and mark as available
|
||||||
|
poolEntry.mesh.visible = false
|
||||||
|
poolEntry.inUse = false
|
||||||
|
poolEntry.sectionKey = undefined
|
||||||
|
poolEntry.lastUsedTime = 0
|
||||||
|
|
||||||
|
// Clear geometry to free memory
|
||||||
|
this.clearGeometry(poolEntry.mesh.geometry)
|
||||||
|
|
||||||
|
this.activeSections.delete(sectionKey)
|
||||||
|
|
||||||
|
// Memory cleanup: if pool exceeds max size and we have free meshes, remove one
|
||||||
|
this.cleanupExcessMeshes()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get section object if it exists
|
||||||
|
*/
|
||||||
|
getSectionObject (sectionKey: string): SectionObject | undefined {
|
||||||
|
return this.sectionObjects[sectionKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update box helper for a section
|
||||||
|
*/
|
||||||
|
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
|
||||||
|
const sectionObject = this.sectionObjects[sectionKey]
|
||||||
|
if (!sectionObject?.mesh) return
|
||||||
|
|
||||||
|
if (showChunkBorders) {
|
||||||
|
if (!sectionObject.boxHelper) {
|
||||||
|
// mesh with static dimensions: 16x16x16
|
||||||
|
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
|
||||||
|
staticChunkMesh.position.copy(sectionObject.mesh.position)
|
||||||
|
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
||||||
|
boxHelper.name = 'helper'
|
||||||
|
sectionObject.add(boxHelper)
|
||||||
|
sectionObject.name = 'chunk'
|
||||||
|
sectionObject.boxHelper = boxHelper
|
||||||
|
}
|
||||||
|
sectionObject.boxHelper.visible = true
|
||||||
|
} else if (sectionObject.boxHelper) {
|
||||||
|
sectionObject.boxHelper.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mesh for section if it exists
|
||||||
|
*/
|
||||||
|
getSectionMesh (sectionKey: string): THREE.Mesh | undefined {
|
||||||
|
return this.activeSections.get(sectionKey)?.mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if section is managed by this pool
|
||||||
|
*/
|
||||||
|
hasSection (sectionKey: string): boolean {
|
||||||
|
return this.activeSections.has(sectionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update pool size based on new view distance
|
||||||
|
*/
|
||||||
|
updateViewDistance (maxViewDistance: number) {
|
||||||
|
// Calculate dynamic pool size based on view distance
|
||||||
|
const chunksInView = (maxViewDistance * 2 + 1) ** 2
|
||||||
|
const maxSectionsPerChunk = this.worldHeight / 16
|
||||||
|
const avgSectionsPerChunk = 5
|
||||||
|
this.minPoolSize = Math.floor(chunksInView * avgSectionsPerChunk)
|
||||||
|
this.maxPoolSize = Math.floor(chunksInView * maxSectionsPerChunk) + 1
|
||||||
|
this.poolSize ??= this.minPoolSize
|
||||||
|
|
||||||
|
// Expand pool if needed to reach optimal size
|
||||||
|
if (this.minPoolSize > this.poolSize) {
|
||||||
|
const targetSize = Math.min(this.minPoolSize, this.maxPoolSize)
|
||||||
|
this.expandPool(targetSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`ChunkMeshManager: Updated view max distance to ${maxViewDistance}, pool: ${this.poolSize}/${this.maxPoolSize}, optimal: ${this.minPoolSize}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pool statistics
|
||||||
|
*/
|
||||||
|
getStats () {
|
||||||
|
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
||||||
|
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
|
||||||
|
const memoryUsage = this.getEstimatedMemoryUsage()
|
||||||
|
|
||||||
|
return {
|
||||||
|
poolSize: this.poolSize,
|
||||||
|
activeCount: this.activeSections.size,
|
||||||
|
freeCount,
|
||||||
|
hitRate: `${hitRate}%`,
|
||||||
|
hits: this.hits,
|
||||||
|
misses: this.misses,
|
||||||
|
memoryUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total tiles rendered
|
||||||
|
*/
|
||||||
|
getTotalTiles (): number {
|
||||||
|
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.tilesCount || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total blocks rendered
|
||||||
|
*/
|
||||||
|
getTotalBlocks (): number {
|
||||||
|
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.blocksCount || 0), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate memory usage in MB
|
||||||
|
*/
|
||||||
|
getEstimatedMemoryUsage (): { total: string, breakdown: any } {
|
||||||
|
let totalBytes = 0
|
||||||
|
let positionBytes = 0
|
||||||
|
let normalBytes = 0
|
||||||
|
let colorBytes = 0
|
||||||
|
let uvBytes = 0
|
||||||
|
let indexBytes = 0
|
||||||
|
|
||||||
|
for (const poolEntry of this.meshPool) {
|
||||||
|
if (poolEntry.inUse && poolEntry.mesh.geometry) {
|
||||||
|
const { geometry } = poolEntry.mesh
|
||||||
|
|
||||||
|
const position = geometry.getAttribute('position')
|
||||||
|
if (position) {
|
||||||
|
const bytes = position.array.byteLength
|
||||||
|
positionBytes += bytes
|
||||||
|
totalBytes += bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const normal = geometry.getAttribute('normal')
|
||||||
|
if (normal) {
|
||||||
|
const bytes = normal.array.byteLength
|
||||||
|
normalBytes += bytes
|
||||||
|
totalBytes += bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = geometry.getAttribute('color')
|
||||||
|
if (color) {
|
||||||
|
const bytes = color.array.byteLength
|
||||||
|
colorBytes += bytes
|
||||||
|
totalBytes += bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
const uv = geometry.getAttribute('uv')
|
||||||
|
if (uv) {
|
||||||
|
const bytes = uv.array.byteLength
|
||||||
|
uvBytes += bytes
|
||||||
|
totalBytes += bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geometry.index) {
|
||||||
|
const bytes = geometry.index.array.byteLength
|
||||||
|
indexBytes += bytes
|
||||||
|
totalBytes += bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalMB = (totalBytes / (1024 * 1024)).toFixed(2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: `${totalMB} MB`,
|
||||||
|
breakdown: {
|
||||||
|
position: `${(positionBytes / (1024 * 1024)).toFixed(2)} MB`,
|
||||||
|
normal: `${(normalBytes / (1024 * 1024)).toFixed(2)} MB`,
|
||||||
|
color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
|
||||||
|
uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
|
||||||
|
index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup and dispose resources
|
||||||
|
*/
|
||||||
|
dispose () {
|
||||||
|
// Release all active sections
|
||||||
|
for (const [sectionKey] of this.activeSections) {
|
||||||
|
this.releaseSection(sectionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose all meshes and geometries
|
||||||
|
for (const poolEntry of this.meshPool) {
|
||||||
|
// Meshes will be removed from scene when their parent containers are removed
|
||||||
|
poolEntry.mesh.geometry.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.meshPool.length = 0
|
||||||
|
this.activeSections.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private helper methods
|
||||||
|
|
||||||
|
private acquireMesh (): ChunkMeshPool | undefined {
|
||||||
|
if (this.bypassPooling) {
|
||||||
|
return {
|
||||||
|
mesh: new THREE.Mesh(new THREE.BufferGeometry(), this.material),
|
||||||
|
inUse: true,
|
||||||
|
lastUsedTime: performance.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first available mesh
|
||||||
|
const availableMesh = this.meshPool.find(entry => !entry.inUse)
|
||||||
|
|
||||||
|
if (availableMesh) {
|
||||||
|
availableMesh.inUse = true
|
||||||
|
this.hits++
|
||||||
|
return availableMesh
|
||||||
|
}
|
||||||
|
|
||||||
|
// No available mesh, expand pool to accommodate new sections
|
||||||
|
let newPoolSize = Math.min(this.poolSize + 16, this.maxPoolSize)
|
||||||
|
if (newPoolSize === this.poolSize) {
|
||||||
|
newPoolSize = this.poolSize + 8
|
||||||
|
this.maxPoolSize = newPoolSize
|
||||||
|
console.warn(`ChunkMeshManager: Pool exhausted (${this.poolSize}/${this.maxPoolSize}). Emergency expansion to ${newPoolSize}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.misses++
|
||||||
|
this.expandPool(newPoolSize)
|
||||||
|
return this.acquireMesh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private expandPool (newSize: number) {
|
||||||
|
const oldSize = this.poolSize
|
||||||
|
this.poolSize = newSize
|
||||||
|
|
||||||
|
// console.log(`ChunkMeshManager: Expanding pool from ${oldSize} to ${newSize}`)
|
||||||
|
|
||||||
|
// Add new meshes to pool
|
||||||
|
for (let i = oldSize; i < newSize; i++) {
|
||||||
|
const geometry = new THREE.BufferGeometry()
|
||||||
|
const mesh = new THREE.Mesh(geometry, this.material)
|
||||||
|
mesh.visible = false
|
||||||
|
mesh.matrixAutoUpdate = false
|
||||||
|
mesh.name = 'pooled-section-mesh'
|
||||||
|
|
||||||
|
const poolEntry: ChunkMeshPool = {
|
||||||
|
mesh,
|
||||||
|
inUse: false,
|
||||||
|
lastUsedTime: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.meshPool.push(poolEntry)
|
||||||
|
// Don't add to scene here - meshes will be added to containers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateGeometryAttribute (
|
||||||
|
geometry: THREE.BufferGeometry,
|
||||||
|
name: string,
|
||||||
|
array: Float32Array,
|
||||||
|
itemSize: number
|
||||||
|
) {
|
||||||
|
const attribute = geometry.getAttribute(name)
|
||||||
|
|
||||||
|
if (attribute && attribute.count === array.length / itemSize) {
|
||||||
|
// Reuse existing attribute
|
||||||
|
;(attribute.array as Float32Array).set(array)
|
||||||
|
attribute.needsUpdate = true
|
||||||
|
} else {
|
||||||
|
// Create new attribute (this will dispose the old one automatically)
|
||||||
|
geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearGeometry (geometry: THREE.BufferGeometry) {
|
||||||
|
// Clear attributes but keep the attribute objects for reuse
|
||||||
|
const attributes = ['position', 'normal', 'color', 'uv']
|
||||||
|
for (const name of attributes) {
|
||||||
|
const attr = geometry.getAttribute(name)
|
||||||
|
if (attr) {
|
||||||
|
// Just mark as needing update but don't dispose to avoid recreation costs
|
||||||
|
attr.needsUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geometry.index) {
|
||||||
|
geometry.index.needsUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupExcessMeshes () {
|
||||||
|
// If pool size exceeds max and we have free meshes, remove some
|
||||||
|
if (this.poolSize > this.maxPoolSize) {
|
||||||
|
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
||||||
|
if (freeCount > 0) {
|
||||||
|
const excessCount = Math.min(this.poolSize - this.maxPoolSize, freeCount)
|
||||||
|
for (let i = 0; i < excessCount; i++) {
|
||||||
|
const freeIndex = this.meshPool.findIndex(entry => !entry.inUse)
|
||||||
|
if (freeIndex !== -1) {
|
||||||
|
const poolEntry = this.meshPool[freeIndex]
|
||||||
|
poolEntry.mesh.geometry.dispose()
|
||||||
|
this.meshPool.splice(freeIndex, 1)
|
||||||
|
this.poolSize--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(`ChunkMeshManager: Cleaned up ${excessCount} excess meshes. Pool size: ${this.poolSize}/${this.maxPoolSize}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disposeContainer (container: THREE.Group) {
|
||||||
|
disposeObject(container, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record render time for performance monitoring
|
||||||
|
*/
|
||||||
|
recordRenderTime (renderTime: number): void {
|
||||||
|
this.renderTimes.push(renderTime)
|
||||||
|
if (this.renderTimes.length > this.maxRenderTimeSamples) {
|
||||||
|
this.renderTimes.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check performance periodically
|
||||||
|
const now = performance.now()
|
||||||
|
if (now - this.lastPerformanceCheck > this.performanceCheckInterval) {
|
||||||
|
this.checkPerformance()
|
||||||
|
this.lastPerformanceCheck = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current effective render distance
|
||||||
|
*/
|
||||||
|
getEffectiveRenderDistance (): number {
|
||||||
|
return this.performanceOverrideDistance || this.worldRenderer.viewDistance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force reset performance override
|
||||||
|
*/
|
||||||
|
resetPerformanceOverride (): void {
|
||||||
|
this.performanceOverrideDistance = undefined
|
||||||
|
this.renderTimes.length = 0
|
||||||
|
console.log('ChunkMeshManager: Performance override reset')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get average render time
|
||||||
|
*/
|
||||||
|
getAverageRenderTime (): number {
|
||||||
|
if (this.renderTimes.length === 0) return 0
|
||||||
|
return this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if performance is degraded and adjust render distance
|
||||||
|
*/
|
||||||
|
private checkPerformance (): void {
|
||||||
|
if (this.renderTimes.length < this.maxRenderTimeSamples) return
|
||||||
|
|
||||||
|
const avgRenderTime = this.getAverageRenderTime()
|
||||||
|
const targetRenderTime = 16.67 // 60 FPS target (16.67ms per frame)
|
||||||
|
const performanceThreshold = targetRenderTime * 1.5 // 25ms threshold
|
||||||
|
|
||||||
|
if (avgRenderTime > performanceThreshold) {
|
||||||
|
// Performance is bad, reduce render distance
|
||||||
|
const currentViewDistance = this.worldRenderer.viewDistance
|
||||||
|
const newDistance = Math.max(1, Math.floor(currentViewDistance * 0.8))
|
||||||
|
|
||||||
|
if (!this.performanceOverrideDistance || newDistance < this.performanceOverrideDistance) {
|
||||||
|
this.performanceOverrideDistance = newDistance
|
||||||
|
console.warn(`ChunkMeshManager: Performance degraded (${avgRenderTime.toFixed(2)}ms avg). Reducing effective render distance to ${newDistance}`)
|
||||||
|
}
|
||||||
|
} else if (this.performanceOverrideDistance && avgRenderTime < targetRenderTime * 1.1) {
|
||||||
|
// Performance is good, gradually restore render distance
|
||||||
|
const currentViewDistance = this.worldRenderer.viewDistance
|
||||||
|
const newDistance = Math.min(currentViewDistance, this.performanceOverrideDistance + 1)
|
||||||
|
|
||||||
|
if (newDistance !== this.performanceOverrideDistance) {
|
||||||
|
this.performanceOverrideDistance = newDistance >= currentViewDistance ? undefined : newDistance
|
||||||
|
console.log(`ChunkMeshManager: Performance improved. Restoring render distance to ${newDistance}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide sections beyond performance override distance
|
||||||
|
*/
|
||||||
|
updateSectionsVisibility (): void {
|
||||||
|
const cameraPos = this.worldRenderer.cameraSectionPos
|
||||||
|
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
|
||||||
|
if (!this.performanceOverrideDistance) {
|
||||||
|
sectionObject.visible = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y, z] = sectionKey.split(',').map(Number)
|
||||||
|
const sectionPos = { x: x / 16, y: y / 16, z: z / 16 }
|
||||||
|
|
||||||
|
// Calculate distance using hypot (same as render distance calculation)
|
||||||
|
const dx = sectionPos.x - cameraPos.x
|
||||||
|
const dz = sectionPos.z - cameraPos.z
|
||||||
|
const distance = Math.floor(Math.hypot(dx, dz))
|
||||||
|
|
||||||
|
sectionObject.visible = distance <= this.performanceOverrideDistance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SignHeadsRenderer {
|
||||||
|
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||||
|
|
||||||
|
constructor (public worldRendererThree: WorldRendererThree) {
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||||
|
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
|
||||||
|
if (!textures) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
|
||||||
|
let skinUrl = textureData.textures?.SKIN?.url
|
||||||
|
const { skinTexturesProxy } = this.worldRendererThree.worldRendererConfig
|
||||||
|
if (skinTexturesProxy) {
|
||||||
|
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
||||||
|
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesh = getMesh(this.worldRendererThree, skinUrl, armorModel.head)
|
||||||
|
const group = new THREE.Group()
|
||||||
|
if (isWall) {
|
||||||
|
mesh.position.set(0, 0.3125, 0.3125)
|
||||||
|
}
|
||||||
|
// move head model down as armor have a different offset than blocks
|
||||||
|
mesh.position.y -= 23 / 16
|
||||||
|
group.add(mesh)
|
||||||
|
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
|
||||||
|
group.rotation.set(
|
||||||
|
0,
|
||||||
|
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
group.scale.set(0.8, 0.8, 0.8)
|
||||||
|
return group
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error decoding player texture:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
||||||
|
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
||||||
|
|
||||||
|
if (!tex) return
|
||||||
|
|
||||||
|
// todo implement
|
||||||
|
// const key = JSON.stringify({ position, rotation, isWall })
|
||||||
|
// if (this.signsCache.has(key)) {
|
||||||
|
// console.log('cached', key)
|
||||||
|
// } else {
|
||||||
|
// this.signsCache.set(key, tex)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
||||||
|
mesh.renderOrder = 999
|
||||||
|
|
||||||
|
const lineHeight = 7 / 16
|
||||||
|
const scaleFactor = isHanging ? 1.3 : 1
|
||||||
|
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
|
||||||
|
|
||||||
|
const thickness = (isHanging ? 2 : 1.5) / 16
|
||||||
|
const wallSpacing = 0.25 / 16
|
||||||
|
if (isWall && !isHanging) {
|
||||||
|
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
|
||||||
|
} else {
|
||||||
|
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = new THREE.Group()
|
||||||
|
group.rotation.set(
|
||||||
|
0,
|
||||||
|
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
group.add(mesh)
|
||||||
|
const height = (isHanging ? 10 : 8) / 16
|
||||||
|
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
||||||
|
const textPosition = height / 2 + heightOffset
|
||||||
|
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
||||||
|
const chunk = chunkPos(position)
|
||||||
|
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||||
|
if (!textures) {
|
||||||
|
textures = {}
|
||||||
|
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
||||||
|
}
|
||||||
|
const texturekey = `${position.x},${position.y},${position.z}`
|
||||||
|
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||||
|
if (textures[texturekey]) return textures[texturekey]
|
||||||
|
|
||||||
|
const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version)
|
||||||
|
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
||||||
|
if (!canvas) return
|
||||||
|
const tex = new THREE.Texture(canvas)
|
||||||
|
tex.magFilter = THREE.NearestFilter
|
||||||
|
tex.minFilter = THREE.NearestFilter
|
||||||
|
tex.needsUpdate = true
|
||||||
|
textures[texturekey] = tex
|
||||||
|
return tex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -735,6 +735,7 @@ export class Entities {
|
||||||
outerGroup.add(mesh)
|
outerGroup.add(mesh)
|
||||||
return {
|
return {
|
||||||
mesh: outerGroup,
|
mesh: outerGroup,
|
||||||
|
meshGeometry: mesh.children.find(child => child instanceof THREE.Mesh)?.geometry,
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
itemsTexture: null,
|
itemsTexture: null,
|
||||||
itemsTextureFlipped: null,
|
itemsTextureFlipped: null,
|
||||||
|
|
@ -1345,12 +1346,12 @@ export class Entities {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raycastSceneDebug () {
|
debugRaycastScene () {
|
||||||
// return any object from scene. raycast from camera
|
// return any object from scene. raycast from camera
|
||||||
const raycaster = new THREE.Raycaster()
|
const raycaster = new THREE.Raycaster()
|
||||||
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
||||||
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
|
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
|
||||||
return intersects[0]?.object
|
return intersects.find(intersect => intersect.object.visible)?.object
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {
|
private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {
|
||||||
|
|
|
||||||
30
renderer/viewer/three/getPreflatBlock.ts
Normal file
30
renderer/viewer/three/getPreflatBlock.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -189,7 +189,7 @@ export default class HoldingBlock {
|
||||||
this.swingAnimator?.stopSwing()
|
this.swingAnimator?.stopSwing()
|
||||||
}
|
}
|
||||||
|
|
||||||
render (originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) {
|
render (renderer: THREE.WebGLRenderer) {
|
||||||
if (!this.lastHeldItem) return
|
if (!this.lastHeldItem) return
|
||||||
const now = performance.now()
|
const now = performance.now()
|
||||||
if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick
|
if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick
|
||||||
|
|
@ -205,15 +205,13 @@ export default class HoldingBlock {
|
||||||
|
|
||||||
this.blockSwapAnimation?.switcher.update()
|
this.blockSwapAnimation?.switcher.update()
|
||||||
|
|
||||||
const scene = new THREE.Scene()
|
const scene = this.worldRenderer.templateScene
|
||||||
scene.add(this.cameraGroup)
|
scene.add(this.cameraGroup)
|
||||||
// if (this.camera.aspect !== originalCamera.aspect) {
|
// if (this.camera.aspect !== originalCamera.aspect) {
|
||||||
// this.camera.aspect = originalCamera.aspect
|
// this.camera.aspect = originalCamera.aspect
|
||||||
// this.camera.updateProjectionMatrix()
|
// this.camera.updateProjectionMatrix()
|
||||||
// }
|
// }
|
||||||
this.updateCameraGroup()
|
this.updateCameraGroup()
|
||||||
scene.add(ambientLight.clone())
|
|
||||||
scene.add(directionalLight.clone())
|
|
||||||
|
|
||||||
const viewerSize = renderer.getSize(new THREE.Vector2())
|
const viewerSize = renderer.getSize(new THREE.Vector2())
|
||||||
const minSize = Math.min(viewerSize.width, viewerSize.height)
|
const minSize = Math.min(viewerSize.width, viewerSize.height)
|
||||||
|
|
@ -241,6 +239,8 @@ export default class HoldingBlock {
|
||||||
if (offHandDisplay) {
|
if (offHandDisplay) {
|
||||||
this.cameraGroup.scale.x = 1
|
this.cameraGroup.scale.x = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scene.remove(this.cameraGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
// worldTest () {
|
// worldTest () {
|
||||||
|
|
|
||||||
860
renderer/viewer/three/instancedRenderer.ts
Normal file
860
renderer/viewer/three/instancedRenderer.ts
Normal file
|
|
@ -0,0 +1,860 @@
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { Vec3 } from 'vec3'
|
||||||
|
import { versionToNumber } from 'flying-squid/dist/utils'
|
||||||
|
import PrismarineBlock from 'prismarine-block'
|
||||||
|
import { IndexedBlock } from 'minecraft-data'
|
||||||
|
import moreBlockData from '../lib/moreBlockDataGenerated.json'
|
||||||
|
import { InstancingMode, MesherGeometryOutput } from '../lib/mesher/shared'
|
||||||
|
import { getPreflatBlock } from './getPreflatBlock'
|
||||||
|
import { WorldRendererThree } from './worldrendererThree'
|
||||||
|
|
||||||
|
// Helper function to parse RGB color strings from moreBlockDataGenerated.json
|
||||||
|
function parseRgbColor (rgbString: string): number {
|
||||||
|
const match = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(rgbString)
|
||||||
|
if (!match) return 0x99_99_99 // Default gray
|
||||||
|
|
||||||
|
const r = parseInt(match[1], 10)
|
||||||
|
const g = parseInt(match[2], 10)
|
||||||
|
const b = parseInt(match[3], 10)
|
||||||
|
|
||||||
|
return (r << 16) | (g << 8) | b
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstancedBlockData {
|
||||||
|
stateId: number
|
||||||
|
positions: Vec3[]
|
||||||
|
blockName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstancedSectionData {
|
||||||
|
sectionKey: string
|
||||||
|
instancedBlocks: Map<number, InstancedBlockData>
|
||||||
|
shouldUseInstancedOnly: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstancedBlockModelData {
|
||||||
|
stateId: number
|
||||||
|
// textures: number[]
|
||||||
|
rotation: number[]
|
||||||
|
transparent?: boolean
|
||||||
|
emitLight?: number
|
||||||
|
filterLight?: number
|
||||||
|
textureInfos?: Array<{ u: number, v: number, su: number, sv: number }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstancedBlocksConfig {
|
||||||
|
instanceableBlocks: Set<number>
|
||||||
|
blocksDataModel: Record<number, InstancedBlockModelData>
|
||||||
|
blockNameToStateIdMap: Record<string, number>
|
||||||
|
interestedTextureTiles: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstancedRenderer {
|
||||||
|
isPreflat: boolean
|
||||||
|
|
||||||
|
USE_APP_GEOMETRY = true
|
||||||
|
private readonly instancedMeshes = new Map<number, THREE.InstancedMesh>()
|
||||||
|
private readonly sceneUsedMeshes = new Map<string, THREE.InstancedMesh>()
|
||||||
|
private readonly blockCounts = new Map<number, number>()
|
||||||
|
private readonly sectionInstances = new Map<string, Map<number, number[]>>()
|
||||||
|
private readonly cubeGeometry: THREE.BoxGeometry
|
||||||
|
private readonly tempMatrix = new THREE.Matrix4()
|
||||||
|
private readonly stateIdToName = new Map<number, string>()
|
||||||
|
|
||||||
|
// Cache for single color materials
|
||||||
|
private readonly colorMaterials = new Map<number, THREE.MeshBasicMaterial>()
|
||||||
|
|
||||||
|
// Dynamic instance management
|
||||||
|
private readonly initialInstancesPerBlock = 2000
|
||||||
|
private readonly maxInstancesPerBlock = 100_000
|
||||||
|
private readonly maxTotalInstances = 10_000_000
|
||||||
|
private currentTotalInstances = 0
|
||||||
|
private readonly growthFactor = 1.5 // How much to grow when needed
|
||||||
|
|
||||||
|
// Visibility control
|
||||||
|
private _instancedMeshesVisible = true
|
||||||
|
|
||||||
|
// Memory tracking
|
||||||
|
private totalAllocatedInstances = 0
|
||||||
|
|
||||||
|
private instancedBlocksConfig: InstancedBlocksConfig | null = null
|
||||||
|
private sharedSolidMaterial: THREE.MeshLambertMaterial | null = null
|
||||||
|
|
||||||
|
constructor (private readonly worldRenderer: WorldRendererThree) {
|
||||||
|
this.cubeGeometry = this.createCubeGeometry()
|
||||||
|
this.isPreflat = versionToNumber(this.worldRenderer.version) < versionToNumber('1.13')
|
||||||
|
|
||||||
|
// Create shared solid material with no transparency
|
||||||
|
this.sharedSolidMaterial = new THREE.MeshLambertMaterial({
|
||||||
|
transparent: false,
|
||||||
|
alphaTest: 0.1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStateId (blockName: string): number {
|
||||||
|
if (!this.instancedBlocksConfig) {
|
||||||
|
throw new Error('Instanced blocks config not prepared')
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateId = this.instancedBlocksConfig.blockNameToStateIdMap[blockName]
|
||||||
|
if (stateId === undefined) {
|
||||||
|
throw new Error(`Block ${blockName} not found in blockNameToStateIdMap`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add getter/setter for visibility
|
||||||
|
get instancedMeshesVisible (): boolean {
|
||||||
|
return this._instancedMeshesVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
set instancedMeshesVisible (visible: boolean) {
|
||||||
|
this._instancedMeshesVisible = visible
|
||||||
|
// Update all instanced meshes visibility
|
||||||
|
for (const mesh of this.instancedMeshes.values()) {
|
||||||
|
mesh.visible = visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInitialInstanceCount (blockName: string): number {
|
||||||
|
// Start with small allocation, can grow later if needed
|
||||||
|
return Math.min(this.initialInstancesPerBlock, this.maxInstancesPerBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
debugResizeMesh () {
|
||||||
|
// Debug helper to test resize operation
|
||||||
|
const blockName = 'grass_block'
|
||||||
|
const stateId = this.getStateId(blockName)
|
||||||
|
const mesh = this.instancedMeshes.get(stateId)
|
||||||
|
this.resizeInstancedMesh(stateId, mesh!.instanceMatrix.count * this.growthFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeInstancedMesh (stateId: number, newSize: number): boolean {
|
||||||
|
const mesh = this.instancedMeshes.get(stateId)
|
||||||
|
if (!mesh) return false
|
||||||
|
|
||||||
|
const blockName = this.stateIdToName.get(stateId) || 'unknown'
|
||||||
|
const oldSize = mesh.instanceMatrix.count
|
||||||
|
const actualInstanceCount = this.blockCounts.get(stateId) || 0
|
||||||
|
|
||||||
|
// console.log(`Growing instances for ${blockName}: ${oldSize} -> ${newSize} (${((newSize / oldSize - 1) * 100).toFixed(1)}% increase)`)
|
||||||
|
|
||||||
|
const { geometry } = mesh
|
||||||
|
const { material } = mesh
|
||||||
|
|
||||||
|
// Create new mesh with increased capacity
|
||||||
|
const newMesh = new THREE.InstancedMesh(
|
||||||
|
geometry,
|
||||||
|
material,
|
||||||
|
newSize
|
||||||
|
)
|
||||||
|
newMesh.name = mesh.name
|
||||||
|
newMesh.frustumCulled = false
|
||||||
|
newMesh.visible = this._instancedMeshesVisible
|
||||||
|
|
||||||
|
// Copy ALL existing instances using our tracked count
|
||||||
|
for (let i = 0; i < actualInstanceCount; i++) {
|
||||||
|
this.tempMatrix.identity()
|
||||||
|
mesh.getMatrixAt(i, this.tempMatrix)
|
||||||
|
newMesh.setMatrixAt(i, this.tempMatrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMesh.count = actualInstanceCount
|
||||||
|
newMesh.instanceMatrix.needsUpdate = true
|
||||||
|
|
||||||
|
this.totalAllocatedInstances += (newSize - oldSize)
|
||||||
|
|
||||||
|
this.worldRenderer.scene.add(newMesh)
|
||||||
|
this.instancedMeshes.set(stateId, newMesh)
|
||||||
|
this.worldRenderer.scene.remove(mesh)
|
||||||
|
|
||||||
|
// Clean up old mesh
|
||||||
|
mesh.geometry.dispose()
|
||||||
|
if (Array.isArray(mesh.material)) {
|
||||||
|
for (const m of mesh.material) m.dispose()
|
||||||
|
} else {
|
||||||
|
mesh.material.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify instance count matches
|
||||||
|
// console.log(`Finished growing ${blockName}. Actual instances: ${actualInstanceCount}, New capacity: ${newSize}, Mesh count: ${newMesh.count}`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private canAddMoreInstances (stateId: number, count: number): boolean {
|
||||||
|
const currentForBlock = this.blockCounts.get(stateId) || 0
|
||||||
|
const mesh = this.instancedMeshes.get(stateId)
|
||||||
|
if (!mesh) return false
|
||||||
|
|
||||||
|
const blockName = this.stateIdToName.get(stateId) || 'unknown'
|
||||||
|
|
||||||
|
// If we would exceed current capacity, try to grow
|
||||||
|
if (currentForBlock + count > mesh.instanceMatrix.count) {
|
||||||
|
const currentCapacity = mesh.instanceMatrix.count
|
||||||
|
const neededCapacity = currentForBlock + count
|
||||||
|
const newSize = Math.min(
|
||||||
|
this.maxInstancesPerBlock,
|
||||||
|
Math.ceil(Math.max(
|
||||||
|
neededCapacity,
|
||||||
|
currentCapacity * this.growthFactor
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
// console.log(`Need to grow ${blockName}: current ${currentForBlock}/${currentCapacity}, need ${neededCapacity}, growing to ${newSize}`)
|
||||||
|
|
||||||
|
// Check if growth would exceed total budget
|
||||||
|
const growthAmount = newSize - currentCapacity
|
||||||
|
if (this.totalAllocatedInstances + growthAmount > this.maxTotalInstances) {
|
||||||
|
console.warn(`Cannot grow instances for ${blockName}: would exceed total budget`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to grow
|
||||||
|
if (!this.resizeInstancedMesh(stateId, newSize)) {
|
||||||
|
console.warn(`Failed to grow instances for ${blockName}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check total instance budget
|
||||||
|
if (this.currentTotalInstances + count > this.maxTotalInstances) {
|
||||||
|
console.warn(`Total instance limit reached (${this.currentTotalInstances}/${this.maxTotalInstances})`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareInstancedBlock (stateId: number, name: string, props: Record<string, any>, mcBlockData?: IndexedBlock, defaultState = false) {
|
||||||
|
const config = this.instancedBlocksConfig!
|
||||||
|
|
||||||
|
const possibleIssues = [] as string[]
|
||||||
|
const { currentResources } = this.worldRenderer.resourcesManager
|
||||||
|
if (!currentResources?.worldBlockProvider) return
|
||||||
|
|
||||||
|
const models = currentResources.worldBlockProvider.getAllResolvedModels0_1({
|
||||||
|
name,
|
||||||
|
properties: props
|
||||||
|
}, this.isPreflat, possibleIssues, [], [], true)
|
||||||
|
|
||||||
|
// skipping composite blocks
|
||||||
|
if (models.length !== 1 || !models[0]![0].elements) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const elements = models[0]![0]?.elements
|
||||||
|
if (!elements || (elements.length !== 1 && name !== 'grass_block')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const elem = elements[0]
|
||||||
|
if (elem.from[0] !== 0 || elem.from[1] !== 0 || elem.from[2] !== 0 || elem.to[0] !== 16 || elem.to[1] !== 16 || elem.to[2] !== 16) {
|
||||||
|
// not full block
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const facesMapping = [
|
||||||
|
['front', 'south'],
|
||||||
|
['bottom', 'down'],
|
||||||
|
['top', 'up'],
|
||||||
|
['right', 'east'],
|
||||||
|
['left', 'west'],
|
||||||
|
['back', 'north'],
|
||||||
|
]
|
||||||
|
|
||||||
|
const blockData: InstancedBlockModelData = {
|
||||||
|
stateId,
|
||||||
|
rotation: [0, 0, 0, 0, 0, 0],
|
||||||
|
textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({ u: 0, v: 0, su: 0, sv: 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [face, { texture, cullface, rotation = 0 }] of Object.entries(elem.faces)) {
|
||||||
|
const faceIndex = facesMapping.findIndex(x => x.includes(face))
|
||||||
|
if (faceIndex === -1) {
|
||||||
|
throw new Error(`Unknown face ${face}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockData.rotation[faceIndex] = rotation / 90
|
||||||
|
if (Math.floor(blockData.rotation[faceIndex]) !== blockData.rotation[faceIndex]) {
|
||||||
|
throw new Error(`Invalid rotation ${rotation} ${name}`)
|
||||||
|
}
|
||||||
|
config.interestedTextureTiles.add(texture.debugName)
|
||||||
|
|
||||||
|
// Store texture info for this face
|
||||||
|
blockData.textureInfos![faceIndex] = {
|
||||||
|
u: texture.u,
|
||||||
|
v: texture.v,
|
||||||
|
su: texture.su,
|
||||||
|
sv: texture.sv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.blocksDataModel[stateId] = blockData
|
||||||
|
config.instanceableBlocks.add(stateId)
|
||||||
|
config.blockNameToStateIdMap[name] = stateId
|
||||||
|
|
||||||
|
if (mcBlockData) {
|
||||||
|
blockData.transparent = mcBlockData.transparent
|
||||||
|
blockData.emitLight = mcBlockData.emitLight
|
||||||
|
blockData.filterLight = mcBlockData.filterLight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareInstancedBlocksData () {
|
||||||
|
if (this.sharedSolidMaterial) {
|
||||||
|
this.sharedSolidMaterial.dispose()
|
||||||
|
this.sharedSolidMaterial = null
|
||||||
|
}
|
||||||
|
this.sharedSolidMaterial = new THREE.MeshLambertMaterial({
|
||||||
|
transparent: true,
|
||||||
|
// depthWrite: true,
|
||||||
|
alphaTest: 0.1
|
||||||
|
})
|
||||||
|
this.sharedSolidMaterial.map = this.worldRenderer.material.map
|
||||||
|
// this.sharedTransparentMaterial = new THREE.MeshLambertMaterial({
|
||||||
|
// transparent: true,
|
||||||
|
// // depthWrite: false,
|
||||||
|
// alphaTest: 0.1
|
||||||
|
// })
|
||||||
|
// this.sharedTransparentMaterial.map = this.worldRenderer.material.map
|
||||||
|
|
||||||
|
const { forceInstancedOnly } = this.worldRenderer.worldRendererConfig
|
||||||
|
const debugBlocksMap = forceInstancedOnly ? {
|
||||||
|
'double_stone_slab': 'stone',
|
||||||
|
'stone_slab': 'stone',
|
||||||
|
'oak_stairs': 'planks',
|
||||||
|
'stone_stairs': 'stone',
|
||||||
|
'glass_pane': 'stained_glass',
|
||||||
|
'brick_stairs': 'brick_block',
|
||||||
|
'stone_brick_stairs': 'stonebrick',
|
||||||
|
'nether_brick_stairs': 'nether_brick',
|
||||||
|
'double_wooden_slab': 'planks',
|
||||||
|
'wooden_slab': 'planks',
|
||||||
|
'sandstone_stairs': 'sandstone',
|
||||||
|
'cobblestone_wall': 'cobblestone',
|
||||||
|
'quartz_stairs': 'quartz_block',
|
||||||
|
'stained_glass_pane': 'stained_glass',
|
||||||
|
'red_sandstone_stairs': 'red_sandstone',
|
||||||
|
'stone_slab2': 'stone_slab',
|
||||||
|
'purpur_stairs': 'purpur_block',
|
||||||
|
'purpur_slab': 'purpur_block',
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
const PBlockOriginal = PrismarineBlock(this.worldRenderer.version)
|
||||||
|
|
||||||
|
this.instancedBlocksConfig = {
|
||||||
|
instanceableBlocks: new Set(),
|
||||||
|
blocksDataModel: {},
|
||||||
|
blockNameToStateIdMap: {},
|
||||||
|
interestedTextureTiles: new Set(),
|
||||||
|
} satisfies InstancedBlocksConfig
|
||||||
|
|
||||||
|
// Add unknown block model
|
||||||
|
this.prepareInstancedBlock(-1, 'unknown', {})
|
||||||
|
|
||||||
|
// Handle texture overrides for special blocks
|
||||||
|
const textureOverrideFullBlocks = {
|
||||||
|
water: 'water_still',
|
||||||
|
lava: 'lava_still',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all blocks to find instanceable ones
|
||||||
|
for (const b of loadedData.blocksArray) {
|
||||||
|
for (let stateId = b.minStateId; stateId <= b.maxStateId; stateId++) {
|
||||||
|
const config = this.instancedBlocksConfig
|
||||||
|
|
||||||
|
const mapping = debugBlocksMap[b.name]
|
||||||
|
const block = PBlockOriginal.fromStateId(mapping && loadedData.blocksByName[mapping] ? loadedData.blocksByName[mapping].defaultState : stateId, 0)
|
||||||
|
if (this.isPreflat) {
|
||||||
|
getPreflatBlock(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined
|
||||||
|
if (textureOverride) {
|
||||||
|
const { currentResources } = this.worldRenderer.resourcesManager
|
||||||
|
if (!currentResources?.worldBlockProvider) continue
|
||||||
|
const texture = currentResources.worldBlockProvider.getTextureInfo(textureOverride)
|
||||||
|
if (!texture) {
|
||||||
|
console.warn('Missing texture override for', block.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const texIndex = texture.tileIndex
|
||||||
|
config.blocksDataModel[stateId] = {
|
||||||
|
stateId,
|
||||||
|
rotation: [0, 0, 0, 0, 0, 0],
|
||||||
|
filterLight: b.filterLight,
|
||||||
|
textureInfos: Array.from({ length: 6 }).fill(null).map(() => ({
|
||||||
|
u: texture.u,
|
||||||
|
v: texture.v,
|
||||||
|
su: texture.su,
|
||||||
|
sv: texture.sv
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
config.instanceableBlocks.add(block.stateId)
|
||||||
|
config.interestedTextureTiles.add(textureOverride)
|
||||||
|
config.blockNameToStateIdMap[block.name] = stateId
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if block is a full cube
|
||||||
|
if (block.shapes.length === 0 || !block.shapes.every(shape => {
|
||||||
|
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
||||||
|
})) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepareInstancedBlock(stateId, block.name, block.getProperties(), b, stateId === b.defaultState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrCreateColorMaterial (blockName: string): THREE.Material {
|
||||||
|
const color = this.getBlockColor(blockName)
|
||||||
|
const materialKey = color
|
||||||
|
|
||||||
|
let material = this.colorMaterials.get(materialKey)
|
||||||
|
if (!material) {
|
||||||
|
material = new THREE.MeshBasicMaterial({
|
||||||
|
color,
|
||||||
|
transparent: false
|
||||||
|
})
|
||||||
|
material.name = `instanced_color_${blockName}`
|
||||||
|
this.colorMaterials.set(materialKey, material)
|
||||||
|
}
|
||||||
|
return material
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBlockMaterial (blockName: string, instancingMode: InstancingMode): THREE.Material {
|
||||||
|
if (instancingMode === InstancingMode.ColorOnly) {
|
||||||
|
return this.getOrCreateColorMaterial(blockName)
|
||||||
|
} else {
|
||||||
|
return this.sharedSolidMaterial!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update initializeInstancedMeshes to respect visibility setting
|
||||||
|
initializeInstancedMeshes () {
|
||||||
|
if (!this.instancedBlocksConfig) {
|
||||||
|
console.warn('Instanced blocks config not prepared')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InstancedMesh for each instanceable block type
|
||||||
|
for (const stateId of this.instancedBlocksConfig.instanceableBlocks) {
|
||||||
|
const blockName = this.stateIdToName.get(stateId)
|
||||||
|
if (blockName) {
|
||||||
|
this.initializeInstancedMesh(stateId, blockName, InstancingMode.ColorOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeInstancedMesh (stateId: number, blockName: string, instancingMode: InstancingMode) {
|
||||||
|
if (this.instancedMeshes.has(stateId)) return // Skip if already exists
|
||||||
|
|
||||||
|
if (!this.instancedBlocksConfig!.blocksDataModel) {
|
||||||
|
this.prepareInstancedBlock(stateId, blockName, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockModelData = this.instancedBlocksConfig!.blocksDataModel[stateId]
|
||||||
|
const isTransparent = blockModelData?.transparent ?? false
|
||||||
|
const initialCount = this.getInitialInstanceCount(blockName)
|
||||||
|
|
||||||
|
const geometry = blockModelData ? this.createCustomGeometry(stateId, blockModelData) : this.cubeGeometry
|
||||||
|
const material = this.createBlockMaterial(blockName, instancingMode)
|
||||||
|
|
||||||
|
const mesh = new THREE.InstancedMesh(
|
||||||
|
geometry,
|
||||||
|
material,
|
||||||
|
initialCount
|
||||||
|
)
|
||||||
|
mesh.name = `instanced_${blockName}`
|
||||||
|
mesh.frustumCulled = false
|
||||||
|
mesh.count = 0
|
||||||
|
mesh.visible = this._instancedMeshesVisible // Set initial visibility
|
||||||
|
|
||||||
|
// mesh.renderOrder = isTransparent ? 1 : 0
|
||||||
|
|
||||||
|
this.instancedMeshes.set(stateId, mesh)
|
||||||
|
// Don't add to scene until actually used
|
||||||
|
this.totalAllocatedInstances += initialCount
|
||||||
|
|
||||||
|
if (!blockModelData) {
|
||||||
|
console.warn(`No block model data found for block ${blockName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private debugRaycast () {
|
||||||
|
// get instanced block name
|
||||||
|
const raycaster = new THREE.Raycaster()
|
||||||
|
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
||||||
|
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
|
||||||
|
for (const intersect of intersects) {
|
||||||
|
const mesh = intersect.object as THREE.Mesh
|
||||||
|
if (mesh.name.startsWith('instanced_')) {
|
||||||
|
console.log(`Instanced block name: ${mesh.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCubeGeometry (): THREE.BoxGeometry {
|
||||||
|
// Create a basic cube geometry
|
||||||
|
// For proper texturing, we would need to modify UV coordinates per block type
|
||||||
|
// For now, use default BoxGeometry which works with the texture atlas
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||||
|
return geometry
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCustomGeometry (stateId: number, blockModelData: InstancedBlockModelData): THREE.BufferGeometry {
|
||||||
|
if (this.USE_APP_GEOMETRY) {
|
||||||
|
const itemMesh = this.worldRenderer.entities.getItemMesh(stateId === -1 ? {
|
||||||
|
name: 'unknown'
|
||||||
|
} : {
|
||||||
|
blockState: stateId
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return itemMesh?.meshGeometry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create custom geometry with specific UV coordinates per face
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||||
|
|
||||||
|
// Get UV attribute
|
||||||
|
const uvAttribute = geometry.getAttribute('uv') as THREE.BufferAttribute
|
||||||
|
const uvs = uvAttribute.array as Float32Array
|
||||||
|
|
||||||
|
if (!blockModelData.textureInfos) {
|
||||||
|
console.warn('No texture infos available for block model')
|
||||||
|
return geometry
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoxGeometry has 6 faces, each with 4 vertices (8 UV values)
|
||||||
|
// Three.js BoxGeometry face order: +X, -X, +Y, -Y, +Z, -Z
|
||||||
|
// Our face mapping: [front, bottom, top, right, left, back] = [south, down, up, east, west, north]
|
||||||
|
// Map to Three.js indices: [+Z, -Y, +Y, +X, -X, -Z] = [4, 3, 2, 0, 1, 5]
|
||||||
|
|
||||||
|
interface UVVertex {
|
||||||
|
u: number
|
||||||
|
v: number
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let faceIndex = 0; faceIndex < 6; faceIndex++) {
|
||||||
|
// Map Three.js face index to our face index
|
||||||
|
let ourFaceIndex: number
|
||||||
|
switch (faceIndex) {
|
||||||
|
case 0: ourFaceIndex = 3; break // +X -> right (east)
|
||||||
|
case 1: ourFaceIndex = 4; break // -X -> left (west)
|
||||||
|
case 2: ourFaceIndex = 2; break // +Y -> top (up)
|
||||||
|
case 3: ourFaceIndex = 1; break // -Y -> bottom (down)
|
||||||
|
case 4: ourFaceIndex = 0; break // +Z -> front (south)
|
||||||
|
case 5: ourFaceIndex = 5; break // -Z -> back (north)
|
||||||
|
default: continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const textureInfo = blockModelData.textureInfos[ourFaceIndex]
|
||||||
|
const rotation = blockModelData.rotation[ourFaceIndex]
|
||||||
|
|
||||||
|
if (!textureInfo) {
|
||||||
|
console.warn(`No texture info found for face ${ourFaceIndex}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { u, v, su, sv } = textureInfo
|
||||||
|
const faceUvStart = faceIndex * 8
|
||||||
|
|
||||||
|
// Get original UVs for this face
|
||||||
|
const faceUVs = uvs.slice(faceUvStart, faceUvStart + 8)
|
||||||
|
|
||||||
|
// Apply rotation if needed (0=0°, 1=90°, 2=180°, 3=270°)
|
||||||
|
// Add base 180° rotation (2) to all faces
|
||||||
|
const totalRotation = (rotation + 2) % 4
|
||||||
|
if (totalRotation > 0) {
|
||||||
|
// Each vertex has 2 UV coordinates (u,v)
|
||||||
|
// We need to rotate the 4 vertices as a group
|
||||||
|
const vertices: UVVertex[] = []
|
||||||
|
for (let i = 0; i < 8; i += 2) {
|
||||||
|
vertices.push({
|
||||||
|
u: faceUVs[i],
|
||||||
|
v: faceUVs[i + 1]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate vertices
|
||||||
|
const rotatedVertices: UVVertex[] = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const srcIndex = (i + totalRotation) % 4
|
||||||
|
rotatedVertices.push(vertices[srcIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back rotated coordinates
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
faceUVs[i * 2] = rotatedVertices[i].u
|
||||||
|
faceUVs[i * 2 + 1] = rotatedVertices[i].v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply texture atlas coordinates to the potentially rotated UVs
|
||||||
|
for (let i = 0; i < 8; i += 2) {
|
||||||
|
uvs[faceUvStart + i] = u + faceUVs[i] * su
|
||||||
|
uvs[faceUvStart + i + 1] = v + faceUVs[i + 1] * sv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uvAttribute.needsUpdate = true
|
||||||
|
return geometry
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBlockColor (blockName: string): number {
|
||||||
|
// Get color from moreBlockDataGenerated.json
|
||||||
|
const rgbString = moreBlockData.colors[blockName]
|
||||||
|
if (rgbString) {
|
||||||
|
return parseRgbColor(rgbString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: Log when color is not found
|
||||||
|
console.warn(`No color found for block: ${blockName}, using default gray`)
|
||||||
|
|
||||||
|
// Fallback to default gray if color not found
|
||||||
|
return 0x99_99_99
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInstancedBlocksFromWorker (instancedBlocks: MesherGeometryOutput['instancedBlocks'], sectionKey: string, instancingMode: InstancingMode) {
|
||||||
|
// Initialize section tracking if not exists
|
||||||
|
if (!this.sectionInstances.has(sectionKey)) {
|
||||||
|
this.sectionInstances.set(sectionKey, new Map())
|
||||||
|
}
|
||||||
|
const sectionMap = this.sectionInstances.get(sectionKey)!
|
||||||
|
|
||||||
|
// Remove old instances for blocks that are being updated
|
||||||
|
const previousStateIds = [...sectionMap.keys()]
|
||||||
|
for (const stateId of previousStateIds) {
|
||||||
|
const instanceIndices = sectionMap.get(stateId)
|
||||||
|
if (instanceIndices) {
|
||||||
|
this.removeInstancesFromBlock(stateId, instanceIndices)
|
||||||
|
sectionMap.delete(stateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of blocks that were updated this frame
|
||||||
|
for (const [blockName, blockData] of Object.entries(instancedBlocks)) {
|
||||||
|
const { stateId, positions, matrices } = blockData
|
||||||
|
this.stateIdToName.set(stateId, blockName)
|
||||||
|
|
||||||
|
if (this.USE_APP_GEOMETRY) {
|
||||||
|
this.initializeInstancedMesh(stateId, blockName, instancingMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceIndices: number[] = []
|
||||||
|
const currentCount = this.blockCounts.get(stateId) || 0
|
||||||
|
|
||||||
|
// Check if we can add all positions at once
|
||||||
|
const neededInstances = positions.length
|
||||||
|
if (!this.canAddMoreInstances(stateId, neededInstances)) {
|
||||||
|
console.warn(`Cannot add ${neededInstances} instances for block ${blockName} (current: ${currentCount}, max: ${this.maxInstancesPerBlock})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesh = this.instancedMeshes.get(stateId)!
|
||||||
|
|
||||||
|
// Add new instances for this section using pre-calculated matrices from worker
|
||||||
|
for (let i = 0; i < positions.length; i++) {
|
||||||
|
const instanceIndex = currentCount + instanceIndices.length
|
||||||
|
mesh.setMatrixAt(instanceIndex, new THREE.Matrix4().fromArray(matrices[i]))
|
||||||
|
instanceIndices.push(instanceIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tracking
|
||||||
|
if (instanceIndices.length > 0) {
|
||||||
|
sectionMap.set(stateId, instanceIndices)
|
||||||
|
const newCount = currentCount + instanceIndices.length
|
||||||
|
this.blockCounts.set(stateId, newCount)
|
||||||
|
this.currentTotalInstances += instanceIndices.length
|
||||||
|
mesh.count = newCount
|
||||||
|
mesh.instanceMatrix.needsUpdate = true
|
||||||
|
|
||||||
|
// Only add mesh to scene when it's first used
|
||||||
|
if (newCount === instanceIndices.length) {
|
||||||
|
this.worldRenderer.scene.add(mesh)
|
||||||
|
}
|
||||||
|
this.sceneUsedMeshes.set(blockName, mesh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSectionInstances (sectionKey: string) {
|
||||||
|
const sectionMap = this.sectionInstances.get(sectionKey)
|
||||||
|
if (!sectionMap) return // Section not tracked
|
||||||
|
|
||||||
|
// Remove instances for each block type in this section
|
||||||
|
for (const [stateId, instanceIndices] of sectionMap) {
|
||||||
|
this.removeInstancesFromBlock(stateId, instanceIndices)
|
||||||
|
|
||||||
|
// Remove from sceneUsedMeshes if no instances left
|
||||||
|
const blockName = this.stateIdToName.get(stateId)
|
||||||
|
if (blockName && (this.blockCounts.get(stateId) || 0) === 0) {
|
||||||
|
this.sceneUsedMeshes.delete(blockName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove section from tracking
|
||||||
|
this.sectionInstances.delete(sectionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeInstancesFromBlock (stateId: number, indicesToRemove: number[]) {
|
||||||
|
const mesh = this.instancedMeshes.get(stateId)
|
||||||
|
if (!mesh || indicesToRemove.length === 0) return
|
||||||
|
|
||||||
|
const currentCount = this.blockCounts.get(stateId) || 0
|
||||||
|
const removeSet = new Set(indicesToRemove)
|
||||||
|
|
||||||
|
// Update total instance count
|
||||||
|
this.currentTotalInstances -= indicesToRemove.length
|
||||||
|
|
||||||
|
// Create mapping from old indices to new indices
|
||||||
|
const indexMapping = new Map<number, number>()
|
||||||
|
let writeIndex = 0
|
||||||
|
const tempMatrix = new THREE.Matrix4()
|
||||||
|
|
||||||
|
// Compact the instance matrix by removing gaps
|
||||||
|
for (let readIndex = 0; readIndex < currentCount; readIndex++) {
|
||||||
|
if (!removeSet.has(readIndex)) {
|
||||||
|
indexMapping.set(readIndex, writeIndex)
|
||||||
|
if (writeIndex !== readIndex) {
|
||||||
|
mesh.getMatrixAt(readIndex, tempMatrix)
|
||||||
|
mesh.setMatrixAt(writeIndex, tempMatrix)
|
||||||
|
}
|
||||||
|
writeIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
const newCount = writeIndex
|
||||||
|
this.blockCounts.set(stateId, newCount)
|
||||||
|
mesh.count = newCount
|
||||||
|
mesh.instanceMatrix.needsUpdate = true
|
||||||
|
|
||||||
|
// Update all section tracking to reflect new indices
|
||||||
|
for (const [sectionKey, sectionMap] of this.sectionInstances) {
|
||||||
|
const sectionIndices = sectionMap.get(stateId)
|
||||||
|
if (sectionIndices) {
|
||||||
|
const updatedIndices = sectionIndices
|
||||||
|
.map(index => indexMapping.get(index))
|
||||||
|
.filter(index => index !== undefined)
|
||||||
|
|
||||||
|
if (updatedIndices.length > 0) {
|
||||||
|
sectionMap.set(stateId, updatedIndices)
|
||||||
|
} else {
|
||||||
|
sectionMap.delete(stateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sceneUsedMeshes if no instances left
|
||||||
|
if (newCount === 0) {
|
||||||
|
const blockName = this.stateIdToName.get(stateId)
|
||||||
|
if (blockName) {
|
||||||
|
this.sceneUsedMeshes.delete(blockName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeOldMeshes () {
|
||||||
|
// Reset total instance count since we're clearing everything
|
||||||
|
this.currentTotalInstances = 0
|
||||||
|
|
||||||
|
for (const [stateId, mesh] of this.instancedMeshes) {
|
||||||
|
if (mesh.material instanceof THREE.Material && mesh.material.name.startsWith('instanced_color_')) {
|
||||||
|
mesh.material.dispose()
|
||||||
|
}
|
||||||
|
mesh.geometry.dispose()
|
||||||
|
this.instancedMeshes.delete(stateId)
|
||||||
|
this.worldRenderer.scene.remove(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear counts
|
||||||
|
this.blockCounts.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
// Clean up resources
|
||||||
|
for (const [stateId, mesh] of this.instancedMeshes) {
|
||||||
|
this.worldRenderer.scene.remove(mesh)
|
||||||
|
mesh.geometry.dispose()
|
||||||
|
if (mesh.material instanceof THREE.Material) {
|
||||||
|
mesh.material.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up materials
|
||||||
|
if (this.sharedSolidMaterial) {
|
||||||
|
this.sharedSolidMaterial.dispose()
|
||||||
|
this.sharedSolidMaterial = null
|
||||||
|
}
|
||||||
|
for (const material of this.colorMaterials.values()) {
|
||||||
|
material.dispose()
|
||||||
|
}
|
||||||
|
this.colorMaterials.clear()
|
||||||
|
|
||||||
|
this.instancedMeshes.clear()
|
||||||
|
this.blockCounts.clear()
|
||||||
|
this.sectionInstances.clear()
|
||||||
|
this.stateIdToName.clear()
|
||||||
|
this.sceneUsedMeshes.clear()
|
||||||
|
this.cubeGeometry.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add visibility info to stats
|
||||||
|
getStats () {
|
||||||
|
let totalInstances = 0
|
||||||
|
let activeBlockTypes = 0
|
||||||
|
let totalWastedMemory = 0
|
||||||
|
|
||||||
|
for (const [stateId, mesh] of this.instancedMeshes) {
|
||||||
|
const allocated = mesh.instanceMatrix.count
|
||||||
|
const used = mesh.count
|
||||||
|
totalWastedMemory += (allocated - used) * 64 // 64 bytes per instance (approximate)
|
||||||
|
|
||||||
|
if (used > 0) {
|
||||||
|
totalInstances += used
|
||||||
|
activeBlockTypes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPerBlock = this.maxInstancesPerBlock
|
||||||
|
const renderDistance = this.worldRenderer.viewDistance
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalInstances,
|
||||||
|
activeBlockTypes,
|
||||||
|
drawCalls: this._instancedMeshesVisible ? activeBlockTypes : 0,
|
||||||
|
memoryStats: {
|
||||||
|
totalAllocatedInstances: this.totalAllocatedInstances,
|
||||||
|
usedInstances: totalInstances,
|
||||||
|
wastedInstances: this.totalAllocatedInstances - totalInstances,
|
||||||
|
estimatedMemoryUsage: this.totalAllocatedInstances * 64,
|
||||||
|
estimatedWastedMemory: totalWastedMemory,
|
||||||
|
utilizationPercent: ((totalInstances / this.totalAllocatedInstances) * 100).toFixed(1) + '%'
|
||||||
|
},
|
||||||
|
maxInstancesPerBlock: maxPerBlock,
|
||||||
|
totalInstanceBudget: this.maxTotalInstances,
|
||||||
|
renderDistance,
|
||||||
|
instanceUtilization: totalInstances / this.maxTotalInstances,
|
||||||
|
instancedMeshesVisible: this._instancedMeshesVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to prepare and initialize everything
|
||||||
|
prepareAndInitialize () {
|
||||||
|
console.log('Preparing instanced blocks data...')
|
||||||
|
this.prepareInstancedBlocksData()
|
||||||
|
const config = this.instancedBlocksConfig!
|
||||||
|
console.log(`Found ${config.instanceableBlocks.size} instanceable blocks`)
|
||||||
|
|
||||||
|
this.disposeOldMeshes()
|
||||||
|
this.initializeInstancedMeshes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get the current configuration
|
||||||
|
getInstancedBlocksConfig (): InstancedBlocksConfig | null {
|
||||||
|
return this.instancedBlocksConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -204,7 +204,7 @@ export class PanoramaRenderer {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (this.worldRenderer instanceof WorldRendererThree) {
|
if (this.worldRenderer instanceof WorldRendererThree) {
|
||||||
this.scene = this.worldRenderer.scene
|
this.scene = this.worldRenderer.realScene
|
||||||
}
|
}
|
||||||
void worldView.init(initPos)
|
void worldView.init(initPos)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -565,7 +565,7 @@ export class ThreeJsMedia {
|
||||||
raycaster.setFromCamera(mouse, camera)
|
raycaster.setFromCamera(mouse, camera)
|
||||||
|
|
||||||
// Check intersection with all objects in scene
|
// 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) {
|
if (intersects.length > 0) {
|
||||||
const intersection = intersects[0]
|
const intersection = intersects[0]
|
||||||
const intersectedObject = intersection.object
|
const intersectedObject = intersection.object
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
import nbt from 'prismarine-nbt'
|
|
||||||
import PrismarineChatLoader from 'prismarine-chat'
|
|
||||||
import * as tweenJs from '@tweenjs/tween.js'
|
import * as tweenJs from '@tweenjs/tween.js'
|
||||||
import { renderSign } from '../sign-renderer'
|
|
||||||
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
|
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
|
||||||
import { chunkPos, sectionPos } from '../lib/simpleUtils'
|
import { sectionPos } from '../lib/simpleUtils'
|
||||||
import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
||||||
|
import { WorldDataEmitterWorker } from '../lib/worldDataEmitter'
|
||||||
import { addNewStat } from '../lib/ui/newStats'
|
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 { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
||||||
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||||
import { getMyHand } from './hand'
|
import { getMyHand } from './hand'
|
||||||
import HoldingBlock from './holdingBlock'
|
import HoldingBlock from './holdingBlock'
|
||||||
import { getMesh } from './entity/EntityMesh'
|
import { loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
||||||
import { armorModel } from './entity/armorModels'
|
|
||||||
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
|
||||||
import { CursorBlock } from './world/cursorBlock'
|
import { CursorBlock } from './world/cursorBlock'
|
||||||
import { getItemUv } from './appShared'
|
import { getItemUv } from './appShared'
|
||||||
import { Entities } from './entities'
|
import { Entities } from './entities'
|
||||||
|
|
@ -23,19 +19,23 @@ import { ThreeJsSound } from './threeJsSound'
|
||||||
import { CameraShake } from './cameraShake'
|
import { CameraShake } from './cameraShake'
|
||||||
import { ThreeJsMedia } from './threeJsMedia'
|
import { ThreeJsMedia } from './threeJsMedia'
|
||||||
import { Fountain } from './threeJsParticles'
|
import { Fountain } from './threeJsParticles'
|
||||||
|
import { InstancedRenderer } from './instancedRenderer'
|
||||||
|
import { ChunkMeshManager } from './chunkMeshManager'
|
||||||
|
|
||||||
type SectionKey = string
|
type SectionKey = string
|
||||||
|
|
||||||
export class WorldRendererThree extends WorldRendererCommon {
|
export class WorldRendererThree extends WorldRendererCommon {
|
||||||
outputFormat = 'threeJs' as const
|
outputFormat = 'threeJs' as const
|
||||||
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {}
|
sectionInstancingMode: Record<string, InstancingMode> = {}
|
||||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||||
signsCache = new Map<string, any>()
|
signsCache = new Map<string, any>()
|
||||||
starField: StarField
|
starField: StarField
|
||||||
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
||||||
holdingBlock: HoldingBlock
|
holdingBlock: HoldingBlock | undefined
|
||||||
holdingBlockLeft: HoldingBlock
|
holdingBlockLeft: HoldingBlock | undefined
|
||||||
scene = new THREE.Scene()
|
realScene = new THREE.Scene()
|
||||||
|
scene = new THREE.Group()
|
||||||
|
templateScene = new THREE.Scene()
|
||||||
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
||||||
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
||||||
entities = new Entities(this)
|
entities = new Entities(this)
|
||||||
|
|
@ -47,9 +47,12 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
cameraShake: CameraShake
|
cameraShake: CameraShake
|
||||||
cameraContainer: THREE.Object3D
|
cameraContainer: THREE.Object3D
|
||||||
media: ThreeJsMedia
|
media: ThreeJsMedia
|
||||||
|
instancedRenderer: InstancedRenderer | undefined
|
||||||
|
chunkMeshManager: ChunkMeshManager
|
||||||
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
|
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
|
||||||
camera: THREE.PerspectiveCamera
|
camera: THREE.PerspectiveCamera
|
||||||
renderTimeAvg = 0
|
renderTimeAvg = 0
|
||||||
|
chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
|
||||||
sectionsOffsetsAnimations = {} as {
|
sectionsOffsetsAnimations = {} as {
|
||||||
[chunkKey: string]: {
|
[chunkKey: string]: {
|
||||||
time: number,
|
time: number,
|
||||||
|
|
@ -72,13 +75,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
|
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
|
||||||
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
|
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
|
||||||
|
private readonly worldOffset = new THREE.Vector3()
|
||||||
|
|
||||||
get tilesRendered () {
|
get tilesRendered () {
|
||||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
return this.chunkMeshManager.getTotalTiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
get blocksRendered () {
|
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) {
|
constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
|
||||||
|
|
@ -94,11 +98,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
this.addDebugOverlay()
|
this.addDebugOverlay()
|
||||||
this.resetScene()
|
this.resetScene()
|
||||||
void this.init()
|
|
||||||
|
|
||||||
this.soundSystem = new ThreeJsSound(this)
|
this.soundSystem = new ThreeJsSound(this)
|
||||||
this.cameraShake = new CameraShake(this, this.onRender)
|
this.cameraShake = new CameraShake(this, this.onRender)
|
||||||
this.media = new ThreeJsMedia(this)
|
this.media = new ThreeJsMedia(this)
|
||||||
|
this.instancedRenderer = new InstancedRenderer(this)
|
||||||
|
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
|
||||||
|
|
||||||
|
// Enable bypass pooling for debugging if URL param is present
|
||||||
|
if (new URLSearchParams(location.search).get('bypassMeshPooling') === 'true') {
|
||||||
|
this.chunkMeshManager.bypassPooling = true
|
||||||
|
console.log('ChunkMeshManager: Bypassing pooling for debugging')
|
||||||
|
}
|
||||||
|
|
||||||
// this.fountain = new Fountain(this.scene, this.scene, {
|
// this.fountain = new Fountain(this.scene, this.scene, {
|
||||||
// position: new THREE.Vector3(0, 10, 0),
|
// position: new THREE.Vector3(0, 10, 0),
|
||||||
// })
|
// })
|
||||||
|
|
@ -107,6 +118,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.finishChunk(chunkKey)
|
this.finishChunk(chunkKey)
|
||||||
})
|
})
|
||||||
this.worldSwitchActions()
|
this.worldSwitchActions()
|
||||||
|
|
||||||
|
void this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this method to update world origin
|
||||||
|
private updateWorldOrigin (pos: THREE.Vector3) {
|
||||||
|
// this.worldOffset.copy(pos)
|
||||||
|
// this.scene.position.copy(this.worldOffset).multiplyScalar(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
get cameraObject () {
|
get cameraObject () {
|
||||||
|
|
@ -122,6 +141,15 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override connect (worldView: WorldDataEmitterWorker) {
|
||||||
|
super.connect(worldView)
|
||||||
|
|
||||||
|
// Add additional renderDistance handling for mesh pool updates
|
||||||
|
worldView.on('renderDistance', (viewDistance) => {
|
||||||
|
this.chunkMeshManager.updateViewDistance(viewDistance)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
updateEntity (e, isPosUpdate = false) {
|
updateEntity (e, isPosUpdate = false) {
|
||||||
const overrides = {
|
const overrides = {
|
||||||
rotation: {
|
rotation: {
|
||||||
|
|
@ -143,33 +171,44 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
this.entities.handlePlayerEntity(e)
|
this.entities.handlePlayerEntity(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetTemplateScene () {
|
||||||
|
this.templateScene = new THREE.Scene()
|
||||||
|
this.templateScene.add(this.ambientLight.clone())
|
||||||
|
this.templateScene.add(this.directionalLight.clone())
|
||||||
|
}
|
||||||
|
|
||||||
resetScene () {
|
resetScene () {
|
||||||
this.scene.matrixAutoUpdate = false // for perf
|
this.scene.matrixAutoUpdate = false // for perf
|
||||||
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
||||||
this.scene.add(this.ambientLight)
|
this.realScene.add(this.ambientLight)
|
||||||
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
||||||
this.directionalLight.castShadow = true
|
this.directionalLight.castShadow = true
|
||||||
this.scene.add(this.directionalLight)
|
this.realScene.add(this.directionalLight)
|
||||||
|
|
||||||
const size = this.renderer.getSize(new THREE.Vector2())
|
const size = this.renderer.getSize(new THREE.Vector2())
|
||||||
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
||||||
this.cameraContainer = new THREE.Object3D()
|
this.cameraContainer = new THREE.Object3D()
|
||||||
this.cameraContainer.add(this.camera)
|
this.cameraContainer.add(this.camera)
|
||||||
this.scene.add(this.cameraContainer)
|
this.realScene.add(this.cameraContainer)
|
||||||
|
this.realScene.add(this.scene)
|
||||||
|
|
||||||
|
this.resetTemplateScene()
|
||||||
}
|
}
|
||||||
|
|
||||||
override watchReactivePlayerState () {
|
override watchReactivePlayerState () {
|
||||||
super.watchReactivePlayerState()
|
super.watchReactivePlayerState()
|
||||||
this.onReactivePlayerStateUpdated('inWater', (value) => {
|
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) => {
|
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
this.ambientLight.intensity = value
|
this.ambientLight.intensity = value
|
||||||
|
this.resetTemplateScene()
|
||||||
})
|
})
|
||||||
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
|
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
this.directionalLight.intensity = value
|
this.directionalLight.intensity = value
|
||||||
|
this.resetTemplateScene()
|
||||||
})
|
})
|
||||||
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
|
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
|
||||||
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
|
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
|
||||||
|
|
@ -185,19 +224,34 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInstancedBlocksData () {
|
||||||
|
const config = this.instancedRenderer?.getInstancedBlocksConfig()
|
||||||
|
if (!config) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
instanceableBlocks: config.instanceableBlocks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override watchReactiveConfig () {
|
override watchReactiveConfig () {
|
||||||
super.watchReactiveConfig()
|
super.watchReactiveConfig()
|
||||||
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
||||||
this.updateShowChunksBorder(value)
|
this.updateShowChunksBorder()
|
||||||
|
})
|
||||||
|
this.onReactiveConfigUpdated('enableDebugOverlay', (value) => {
|
||||||
|
if (!value) {
|
||||||
|
// restore visibility
|
||||||
|
this.chunkMeshManager.updateSectionsVisibility()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||||
if (isAnimationPlaying) {
|
if (isAnimationPlaying) {
|
||||||
holdingBlock.startSwing()
|
holdingBlock?.startSwing()
|
||||||
} else {
|
} else {
|
||||||
holdingBlock.stopSwing()
|
holdingBlock?.stopSwing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,8 +278,12 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
oldItemsTexture.dispose()
|
oldItemsTexture.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare and initialize instanced renderer with dynamic block detection
|
||||||
|
this.instancedRenderer?.prepareAndInitialize()
|
||||||
|
|
||||||
await super.updateAssetsData()
|
await super.updateAssetsData()
|
||||||
this.onAllTexturesLoaded()
|
this.onAllTexturesLoaded()
|
||||||
|
|
||||||
if (Object.keys(this.loadedChunks).length > 0) {
|
if (Object.keys(this.loadedChunks).length > 0) {
|
||||||
console.log('rerendering chunks because of texture update')
|
console.log('rerendering chunks because of texture update')
|
||||||
this.rerenderAllChunks()
|
this.rerenderAllChunks()
|
||||||
|
|
@ -233,14 +291,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAllTexturesLoaded () {
|
onAllTexturesLoaded () {
|
||||||
this.holdingBlock.ready = true
|
if (this.holdingBlock) {
|
||||||
this.holdingBlock.updateItem()
|
this.holdingBlock.ready = true
|
||||||
this.holdingBlockLeft.ready = true
|
this.holdingBlock.updateItem()
|
||||||
this.holdingBlockLeft.updateItem()
|
}
|
||||||
|
if (this.holdingBlockLeft) {
|
||||||
|
this.holdingBlockLeft.ready = true
|
||||||
|
this.holdingBlockLeft.updateItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBackgroundColor (color: [number, number, number]): void {
|
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 {
|
timeUpdated (newTime: number): void {
|
||||||
|
|
@ -294,12 +356,20 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
const formatBigNumber = (num: number) => {
|
const formatBigNumber = (num: number) => {
|
||||||
return new Intl.NumberFormat('en-US', {}).format(num)
|
return new Intl.NumberFormat('en-US', {}).format(num)
|
||||||
}
|
}
|
||||||
|
const instancedStats = this.instancedRenderer?.getStats()
|
||||||
let text = ''
|
let text = ''
|
||||||
text += `C: ${formatBigNumber(this.renderer.info.render.calls)} `
|
text += `C: ${formatBigNumber(this.renderer.info.render.calls)} `
|
||||||
text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} `
|
text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} `
|
||||||
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
|
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
|
||||||
text += `F: ${formatBigNumber(this.tilesRendered)} `
|
text += `F: ${formatBigNumber(this.tilesRendered)} `
|
||||||
text += `B: ${formatBigNumber(this.blocksRendered)}`
|
text += `B: ${formatBigNumber(this.blocksRendered)} `
|
||||||
|
if (instancedStats) {
|
||||||
|
text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t `
|
||||||
|
text += `DC: ${formatBigNumber(instancedStats.drawCalls)} `
|
||||||
|
}
|
||||||
|
const poolStats = this.chunkMeshManager.getStats()
|
||||||
|
const poolMode = this.chunkMeshManager.bypassPooling ? 'BYPASS' : poolStats.hitRate
|
||||||
|
text += `MP: ${poolStats.activeCount}/${poolStats.poolSize} ${poolMode}`
|
||||||
pane.updateText(text)
|
pane.updateText(text)
|
||||||
this.backendInfoReport = text
|
this.backendInfoReport = text
|
||||||
}
|
}
|
||||||
|
|
@ -313,8 +383,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
||||||
// sum of distances: x + y + z
|
// 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 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 sectionObject = this.chunkMeshManager.getSectionObject(key)!
|
||||||
section.renderOrder = 500 - chunkDistance
|
sectionObject.mesh!.renderOrder = 500 - chunkDistance
|
||||||
}
|
}
|
||||||
|
|
||||||
override updateViewerPosition (pos: Vec3): void {
|
override updateViewerPosition (pos: Vec3): void {
|
||||||
|
|
@ -323,10 +393,22 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
cameraSectionPositionUpdate () {
|
cameraSectionPositionUpdate () {
|
||||||
// eslint-disable-next-line guard-for-in
|
// eslint-disable-next-line guard-for-in
|
||||||
for (const key in this.sectionObjects) {
|
for (const key in this.sectionInstancingMode) {
|
||||||
const value = this.sectionObjects[key]
|
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
|
||||||
if (!value) continue
|
if (sectionObject) {
|
||||||
this.updatePosDataChunk(key)
|
this.updatePosDataChunk(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.worldRendererConfig.dynamicInstancing) {
|
||||||
|
const [x, y, z] = key.split(',').map(x => +x)
|
||||||
|
const pos = new Vec3(x, y, z)
|
||||||
|
const instancingMode = this.getInstancingMode(pos)
|
||||||
|
if (instancingMode !== this.sectionInstancingMode[key]) {
|
||||||
|
// console.log('update section', key, this.sectionInstancingMode[key], '->', instancingMode)
|
||||||
|
// update section
|
||||||
|
this.setSectionDirty(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,123 +419,68 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
finishChunk (chunkKey: string) {
|
finishChunk (chunkKey: string) {
|
||||||
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
|
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]
|
delete this.waitingChunksToDisplay[chunkKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
// debugRecomputedDeletedObjects = 0
|
|
||||||
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
|
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
|
||||||
if (data.type !== 'geometry') return
|
if (data.type !== 'geometry') return
|
||||||
let object: THREE.Object3D = this.sectionObjects[data.key]
|
|
||||||
if (object) {
|
|
||||||
this.scene.remove(object)
|
|
||||||
disposeObject(object)
|
|
||||||
delete this.sectionObjects[data.key]
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunkCoords = data.key.split(',')
|
const chunkCoords = data.key.split(',')
|
||||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
const chunkKey = chunkCoords[0] + ',' + chunkCoords[2]
|
||||||
|
|
||||||
// if (object) {
|
const hasInstancedBlocks = data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0
|
||||||
// this.debugRecomputedDeletedObjects++
|
|
||||||
// }
|
|
||||||
|
|
||||||
const geometry = new THREE.BufferGeometry()
|
this.instancedRenderer?.removeSectionInstances(data.key)
|
||||||
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 mesh = new THREE.Mesh(geometry, this.material)
|
// Handle instanced blocks data from worker
|
||||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
if (hasInstancedBlocks) {
|
||||||
mesh.name = 'mesh'
|
this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key, this.getInstancingMode(new Vec3(chunkCoords[0], chunkCoords[1], chunkCoords[2])))
|
||||||
object = new THREE.Group()
|
|
||||||
object.add(mesh)
|
|
||||||
// mesh with static dimensions: 16x16x16
|
|
||||||
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 }))
|
|
||||||
staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
|
||||||
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
|
||||||
boxHelper.name = 'helper'
|
|
||||||
object.add(boxHelper)
|
|
||||||
object.name = 'chunk';
|
|
||||||
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
|
|
||||||
(object as any).blocksCount = data.geometry.blocksCount
|
|
||||||
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
|
|
||||||
boxHelper.visible = false
|
|
||||||
}
|
}
|
||||||
// should not compute it once
|
|
||||||
if (Object.keys(data.geometry.signs).length) {
|
// Check if chunk should be loaded and has geometry
|
||||||
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
|
if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) {
|
||||||
const signBlockEntity = this.blockEntities[posKey]
|
// Release any existing section from the pool
|
||||||
if (!signBlockEntity) continue
|
this.chunkMeshManager.releaseSection(data.key)
|
||||||
const [x, y, z] = posKey.split(',')
|
return
|
||||||
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
|
|
||||||
if (!sign) continue
|
|
||||||
object.add(sign)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (Object.keys(data.geometry.heads).length) {
|
|
||||||
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
|
// Use ChunkMeshManager for optimized mesh handling
|
||||||
const headBlockEntity = this.blockEntities[posKey]
|
const sectionObject = this.chunkMeshManager.updateSection(data.key, data.geometry)
|
||||||
if (!headBlockEntity) continue
|
|
||||||
const [x, y, z] = posKey.split(',')
|
if (!sectionObject) {
|
||||||
const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
|
return
|
||||||
if (!head) continue
|
|
||||||
object.add(head)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.sectionObjects[data.key] = object
|
|
||||||
|
|
||||||
|
this.updateBoxHelper(data.key)
|
||||||
|
|
||||||
|
// Handle chunk-based rendering
|
||||||
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
|
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
|
||||||
object.visible = false
|
sectionObject.visible = false
|
||||||
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
||||||
this.waitingChunksToDisplay[chunkKey] ??= []
|
this.waitingChunksToDisplay[chunkKey] ??= []
|
||||||
this.waitingChunksToDisplay[chunkKey].push(data.key)
|
this.waitingChunksToDisplay[chunkKey].push(data.key)
|
||||||
if (this.finishedChunks[chunkKey]) {
|
if (this.finishedChunks[chunkKey]) {
|
||||||
// todo it might happen even when it was not an update
|
|
||||||
this.finishChunk(chunkKey)
|
this.finishChunk(chunkKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updatePosDataChunk(data.key)
|
this.updatePosDataChunk(data.key)
|
||||||
object.matrixAutoUpdate = false
|
|
||||||
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
|
|
||||||
// mesh.matrixAutoUpdate = false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scene.add(object)
|
|
||||||
}
|
|
||||||
|
|
||||||
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
|
||||||
const chunk = chunkPos(position)
|
|
||||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
|
||||||
if (!textures) {
|
|
||||||
textures = {}
|
|
||||||
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
|
||||||
}
|
|
||||||
const texturekey = `${position.x},${position.y},${position.z}`
|
|
||||||
// todo investigate bug and remove this so don't need to clean in section dirty
|
|
||||||
if (textures[texturekey]) return textures[texturekey]
|
|
||||||
|
|
||||||
const PrismarineChat = PrismarineChatLoader(this.version)
|
|
||||||
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
|
||||||
if (!canvas) return
|
|
||||||
const tex = new THREE.Texture(canvas)
|
|
||||||
tex.magFilter = THREE.NearestFilter
|
|
||||||
tex.minFilter = THREE.NearestFilter
|
|
||||||
tex.needsUpdate = true
|
|
||||||
textures[texturekey] = tex
|
|
||||||
return tex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCameraPosition () {
|
getCameraPosition () {
|
||||||
const worldPos = new THREE.Vector3()
|
const worldPos = new THREE.Vector3()
|
||||||
this.camera.getWorldPosition(worldPos)
|
this.camera.getWorldPosition(worldPos)
|
||||||
return worldPos
|
// Add world offset to get true world position
|
||||||
|
return worldPos.add(this.worldOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
getWorldCameraPosition () {
|
getSectionCameraPosition () {
|
||||||
const pos = this.getCameraPosition()
|
const pos = this.getCameraPosition()
|
||||||
return new Vec3(
|
return new Vec3(
|
||||||
Math.floor(pos.x / 16),
|
Math.floor(pos.x / 16),
|
||||||
|
|
@ -463,7 +490,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCameraSectionPos () {
|
updateCameraSectionPos () {
|
||||||
const newSectionPos = this.getWorldCameraPosition()
|
const newSectionPos = this.getSectionCameraPosition()
|
||||||
if (!this.cameraSectionPos.equals(newSectionPos)) {
|
if (!this.cameraSectionPos.equals(newSectionPos)) {
|
||||||
this.cameraSectionPos = newSectionPos
|
this.cameraSectionPos = newSectionPos
|
||||||
this.cameraSectionPositionUpdate()
|
this.cameraSectionPositionUpdate()
|
||||||
|
|
@ -473,6 +500,17 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||||
const yOffset = this.playerStateReactive.eyeHeight
|
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.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||||
this.media.tryIntersectMedia()
|
this.media.tryIntersectMedia()
|
||||||
this.updateCameraSectionPos()
|
this.updateCameraSectionPos()
|
||||||
|
|
@ -517,7 +555,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
raycaster.far = distance // Limit raycast distance
|
raycaster.far = distance // Limit raycast distance
|
||||||
|
|
||||||
// Filter to only nearby chunks for performance
|
// 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 => obj.name === 'chunk' && obj.visible)
|
||||||
.filter(obj => {
|
.filter(obj => {
|
||||||
// Get the mesh child which has the actual geometry
|
// Get the mesh child which has the actual geometry
|
||||||
|
|
@ -675,7 +713,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
chunksRenderBelowOverride !== undefined ||
|
chunksRenderBelowOverride !== undefined ||
|
||||||
chunksRenderDistanceOverride !== 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 [x, y, z] = key.split(',').map(Number)
|
||||||
const isVisible =
|
const isVisible =
|
||||||
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
|
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
|
||||||
|
|
@ -687,10 +725,6 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
object.visible = isVisible
|
object.visible = isVisible
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for (const object of Object.values(this.sectionObjects)) {
|
|
||||||
object.visible = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -716,7 +750,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
// 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
|
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 (
|
if (
|
||||||
this.displayOptions.inWorldRenderingConfig.showHand &&
|
this.displayOptions.inWorldRenderingConfig.showHand &&
|
||||||
|
|
@ -725,14 +759,15 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
// !this.freeFlyMode &&
|
// !this.freeFlyMode &&
|
||||||
!this.renderer.xr.isPresenting
|
!this.renderer.xr.isPresenting
|
||||||
) {
|
) {
|
||||||
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
this.holdingBlock?.render(this.renderer)
|
||||||
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
this.holdingBlockLeft?.render(this.renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const fountain of this.fountains) {
|
for (const fountain of this.fountains) {
|
||||||
if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) {
|
const sectionObject = this.chunkMeshManager.getSectionObject(fountain.sectionId)
|
||||||
fountain.createParticles(this.sectionObjects[fountain.sectionId])
|
if (sectionObject && !sectionObject.fountain) {
|
||||||
this.sectionObjects[fountain.sectionId].foutain = true
|
fountain.createParticles(sectionObject)
|
||||||
|
sectionObject.fountain = true
|
||||||
}
|
}
|
||||||
fountain.render()
|
fountain.render()
|
||||||
}
|
}
|
||||||
|
|
@ -742,87 +777,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
const end = performance.now()
|
const end = performance.now()
|
||||||
const totalTime = end - start
|
const totalTime = end - start
|
||||||
|
|
||||||
|
if (this.worldRendererConfig.autoLowerRenderDistance) {
|
||||||
|
// Record render time for performance monitoring
|
||||||
|
this.chunkMeshManager.recordRenderTime(totalTime)
|
||||||
|
}
|
||||||
|
|
||||||
this.renderTimeAvgCount++
|
this.renderTimeAvgCount++
|
||||||
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
|
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
|
||||||
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
|
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
|
||||||
this.currentRenderedFrames++
|
this.currentRenderedFrames++
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
|
||||||
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
|
|
||||||
if (!textures) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
|
|
||||||
let skinUrl = textureData.textures?.SKIN?.url
|
|
||||||
const { skinTexturesProxy } = this.worldRendererConfig
|
|
||||||
if (skinTexturesProxy) {
|
|
||||||
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
|
||||||
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mesh = getMesh(this, skinUrl, armorModel.head)
|
|
||||||
const group = new THREE.Group()
|
|
||||||
if (isWall) {
|
|
||||||
mesh.position.set(0, 0.3125, 0.3125)
|
|
||||||
}
|
|
||||||
// move head model down as armor have a different offset than blocks
|
|
||||||
mesh.position.y -= 23 / 16
|
|
||||||
group.add(mesh)
|
|
||||||
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
|
|
||||||
group.rotation.set(
|
|
||||||
0,
|
|
||||||
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
group.scale.set(0.8, 0.8, 0.8)
|
|
||||||
return group
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error decoding player texture:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
|
||||||
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
|
||||||
|
|
||||||
if (!tex) return
|
|
||||||
|
|
||||||
// todo implement
|
|
||||||
// const key = JSON.stringify({ position, rotation, isWall })
|
|
||||||
// if (this.signsCache.has(key)) {
|
|
||||||
// console.log('cached', key)
|
|
||||||
// } else {
|
|
||||||
// this.signsCache.set(key, tex)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
|
||||||
mesh.renderOrder = 999
|
|
||||||
|
|
||||||
const lineHeight = 7 / 16
|
|
||||||
const scaleFactor = isHanging ? 1.3 : 1
|
|
||||||
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
|
|
||||||
|
|
||||||
const thickness = (isHanging ? 2 : 1.5) / 16
|
|
||||||
const wallSpacing = 0.25 / 16
|
|
||||||
if (isWall && !isHanging) {
|
|
||||||
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
|
|
||||||
} else {
|
|
||||||
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = new THREE.Group()
|
|
||||||
group.rotation.set(
|
|
||||||
0,
|
|
||||||
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
group.add(mesh)
|
|
||||||
const height = (isHanging ? 10 : 8) / 16
|
|
||||||
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
|
||||||
const textPosition = height / 2 + heightOffset
|
|
||||||
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
|
||||||
return group
|
|
||||||
}
|
|
||||||
|
|
||||||
lightUpdate (chunkX: number, chunkZ: number) {
|
lightUpdate (chunkX: number, chunkZ: number) {
|
||||||
// set all sections in the chunk dirty
|
// set all sections in the chunk dirty
|
||||||
|
|
@ -832,26 +798,27 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
rerenderAllChunks () { // todo not clear what to do with loading chunks
|
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)
|
const [x, y, z] = key.split(',').map(Number)
|
||||||
this.setSectionDirty(new Vec3(x, y, z))
|
this.setSectionDirty(new Vec3(x, y, z))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateShowChunksBorder (value: boolean) {
|
updateShowChunksBorder () {
|
||||||
for (const object of Object.values(this.sectionObjects)) {
|
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
|
||||||
for (const child of object.children) {
|
this.updateBoxHelper(key)
|
||||||
if (child.name === 'helper') {
|
|
||||||
child.visible = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateBoxHelper (key: string) {
|
||||||
|
const { showChunkBorders } = this.worldRendererConfig
|
||||||
|
this.chunkMeshManager.updateBoxHelper(key, showChunkBorders, this.chunkBoxMaterial)
|
||||||
|
}
|
||||||
|
|
||||||
resetWorld () {
|
resetWorld () {
|
||||||
super.resetWorld()
|
super.resetWorld()
|
||||||
|
|
||||||
for (const mesh of Object.values(this.sectionObjects)) {
|
for (const mesh of Object.values(this.chunkMeshManager.sectionObjects)) {
|
||||||
this.scene.remove(mesh)
|
this.scene.remove(mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -868,7 +835,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
|
|
||||||
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
||||||
const [currentX, currentY, currentZ] = sectionPos(pos)
|
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 [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
||||||
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
||||||
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
||||||
|
|
@ -885,12 +852,13 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
readdChunks () {
|
readdChunks () {
|
||||||
for (const key of Object.keys(this.sectionObjects)) {
|
const { sectionObjects } = this.chunkMeshManager
|
||||||
this.scene.remove(this.sectionObjects[key])
|
for (const key of Object.keys(sectionObjects)) {
|
||||||
|
this.scene.remove(sectionObjects[key])
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
for (const key of Object.keys(this.sectionObjects)) {
|
for (const key of Object.keys(sectionObjects)) {
|
||||||
this.scene.add(this.sectionObjects[key])
|
this.scene.add(sectionObjects[key])
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
@ -902,6 +870,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeCurrentChunk () {
|
||||||
|
const currentChunk = this.cameraSectionPos
|
||||||
|
this.removeColumn(currentChunk.x * 16, currentChunk.z * 16)
|
||||||
|
}
|
||||||
|
|
||||||
removeColumn (x, z) {
|
removeColumn (x, z) {
|
||||||
super.removeColumn(x, z)
|
super.removeColumn(x, z)
|
||||||
|
|
||||||
|
|
@ -909,19 +882,47 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||||
const key = `${x},${y},${z}`
|
const key = `${x},${y},${z}`
|
||||||
const mesh = this.sectionObjects[key]
|
|
||||||
if (mesh) {
|
// Remove instanced blocks for this section
|
||||||
this.scene.remove(mesh)
|
this.instancedRenderer?.removeSectionInstances(key)
|
||||||
disposeObject(mesh)
|
|
||||||
}
|
// Release section from mesh pool (this will also remove from scene)
|
||||||
delete this.sectionObjects[key]
|
this.chunkMeshManager.releaseSection(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSectionDirty (...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
|
getInstancingMode (pos: Vec3) {
|
||||||
const [pos] = args
|
const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly, dynamicInstancing, dynamicInstancingModeDistance, dynamicColorModeDistance } = this.worldRendererConfig
|
||||||
|
let instancingMode = InstancingMode.None
|
||||||
|
|
||||||
|
if (useInstancedRendering || enableSingleColorMode) {
|
||||||
|
instancingMode = enableSingleColorMode
|
||||||
|
? InstancingMode.ColorOnly
|
||||||
|
: forceInstancedOnly
|
||||||
|
? InstancingMode.BlockInstancingOnly
|
||||||
|
: InstancingMode.BlockInstancing
|
||||||
|
} else if (dynamicInstancing) {
|
||||||
|
const dx = pos.x / 16 - this.cameraSectionPos.x
|
||||||
|
const dz = pos.z / 16 - this.cameraSectionPos.z
|
||||||
|
const distance = Math.floor(Math.hypot(dx, dz))
|
||||||
|
// console.log('distance', distance, `${pos.x},${pos.y},${pos.z}`)
|
||||||
|
if (distance > dynamicColorModeDistance) {
|
||||||
|
instancingMode = InstancingMode.ColorOnly
|
||||||
|
} else if (distance > dynamicInstancingModeDistance) {
|
||||||
|
instancingMode = InstancingMode.BlockInstancingOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instancingMode
|
||||||
|
}
|
||||||
|
|
||||||
|
setSectionDirty (pos: Vec3, value = true) {
|
||||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||||
super.setSectionDirty(...args)
|
const instancingMode = this.getInstancingMode(pos)
|
||||||
|
super.setSectionDirty(pos, value, undefined, instancingMode)
|
||||||
|
if (value) {
|
||||||
|
this.sectionInstancingMode[`${pos.x},${pos.y},${pos.z}`] = instancingMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getRendererInfo (renderer: THREE.WebGLRenderer) {
|
static getRendererInfo (renderer: THREE.WebGLRenderer) {
|
||||||
|
|
@ -938,6 +939,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy (): void {
|
destroy (): void {
|
||||||
|
this.instancedRenderer?.destroy()
|
||||||
|
this.chunkMeshManager.dispose()
|
||||||
super.destroy()
|
super.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -950,7 +953,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
const chunkKey = `${chunkX},${chunkZ}`
|
const chunkKey = `${chunkX},${chunkZ}`
|
||||||
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
|
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
|
||||||
|
|
||||||
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
|
return !!this.finishedChunks[chunkKey] || !!this.chunkMeshManager.sectionObjects[sectionKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSectionOffsets () {
|
updateSectionOffsets () {
|
||||||
|
|
@ -988,7 +991,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the offset to the section object
|
// Apply the offset to the section object
|
||||||
const section = this.sectionObjects[key]
|
const section = this.chunkMeshManager.sectionObjects[key]
|
||||||
if (section) {
|
if (section) {
|
||||||
section.position.set(
|
section.position.set(
|
||||||
anim.currentOffsetX,
|
anim.currentOffsetX,
|
||||||
|
|
|
||||||
|
|
@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
|
||||||
lockUrl()
|
lockUrl()
|
||||||
}
|
}
|
||||||
if (command === 'communication.toggleMicrophone') {
|
if (command === 'communication.toggleMicrophone') {
|
||||||
toggleMicrophoneMuted?.()
|
if (typeof toggleMicrophoneMuted === 'function') {
|
||||||
|
toggleMicrophoneMuted()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,28 @@ export default () => {
|
||||||
registerMediaChannels()
|
registerMediaChannels()
|
||||||
registerSectionAnimationChannels()
|
registerSectionAnimationChannels()
|
||||||
registeredJeiChannel()
|
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') ?? ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ export const defaultOptions = {
|
||||||
starfieldRendering: true,
|
starfieldRendering: true,
|
||||||
enabledResourcepack: null as string | null,
|
enabledResourcepack: null as string | null,
|
||||||
useVersionsTextures: 'latest',
|
useVersionsTextures: 'latest',
|
||||||
|
// Instanced rendering options
|
||||||
|
useInstancedRendering: false,
|
||||||
|
autoLowerRenderDistance: false,
|
||||||
|
forceInstancedOnly: false,
|
||||||
|
instancedOnlyDistance: 6,
|
||||||
|
enableSingleColorMode: false,
|
||||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||||
showHand: true,
|
showHand: true,
|
||||||
viewBobbing: true,
|
viewBobbing: true,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { versionToNumber } from 'mc-assets/dist/utils'
|
||||||
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
||||||
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
|
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
|
||||||
import Button from './react/Button'
|
import Button from './react/Button'
|
||||||
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
import { OptionButton, OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||||
import Slider from './react/Slider'
|
import Slider from './react/Slider'
|
||||||
import { getScreenRefreshRate } from './utils'
|
import { getScreenRefreshRate } from './utils'
|
||||||
import { setLoadingScreenStatus } from './appStatus'
|
import { setLoadingScreenStatus } from './appStatus'
|
||||||
|
|
@ -114,6 +114,52 @@ export const guiOptionsScheme: {
|
||||||
text: 'Performance Debug',
|
text: 'Performance Debug',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
custom () {
|
||||||
|
let status = 'OFF'
|
||||||
|
const { useInstancedRendering, forceInstancedOnly, enableSingleColorMode } = useSnapshot(options)
|
||||||
|
if (useInstancedRendering) {
|
||||||
|
status = 'ON'
|
||||||
|
if (enableSingleColorMode) {
|
||||||
|
status = 'ON (single color)'
|
||||||
|
} else if (forceInstancedOnly) {
|
||||||
|
status = 'ON (force)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <OptionButton
|
||||||
|
item={{
|
||||||
|
type: 'toggle',
|
||||||
|
text: 'Instacing',
|
||||||
|
requiresChunksReload: true,
|
||||||
|
}}
|
||||||
|
cacheKey='instacing'
|
||||||
|
valueText={status}
|
||||||
|
onClick={() => {
|
||||||
|
// cycle
|
||||||
|
if (useInstancedRendering) {
|
||||||
|
if (enableSingleColorMode) {
|
||||||
|
options.useInstancedRendering = false
|
||||||
|
options.enableSingleColorMode = false
|
||||||
|
options.forceInstancedOnly = false
|
||||||
|
} else if (forceInstancedOnly) {
|
||||||
|
options.useInstancedRendering = true
|
||||||
|
options.enableSingleColorMode = true
|
||||||
|
options.forceInstancedOnly = false
|
||||||
|
} else {
|
||||||
|
options.useInstancedRendering = true
|
||||||
|
options.enableSingleColorMode = false
|
||||||
|
options.forceInstancedOnly = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
options.useInstancedRendering = true
|
||||||
|
options.enableSingleColorMode = false
|
||||||
|
options.forceInstancedOnly = false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
custom () {
|
custom () {
|
||||||
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions
|
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
|
||||||
const { reactiveDebugParams } = worldRenderer
|
const { reactiveDebugParams } = worldRenderer
|
||||||
const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams)
|
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
|
// Helper to round values to nearest step
|
||||||
const roundToStep = (value: number, step: number) => Math.round(value / step) * step
|
const roundToStep = (value: number, step: number) => Math.round(value / step) * step
|
||||||
|
|
@ -115,5 +115,36 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.column}>
|
||||||
|
<h3>Instanced Rendering</h3>
|
||||||
|
<Button
|
||||||
|
label={useInstancedRendering ? 'Disable Instanced Rendering' : 'Enable Instanced Rendering'}
|
||||||
|
onClick={() => { options.useInstancedRendering = !options.useInstancedRendering }}
|
||||||
|
overlayColor={useInstancedRendering ? 'green' : undefined}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={forceInstancedOnly ? 'Disable Force Instanced Only' : 'Enable Force Instanced Only'}
|
||||||
|
onClick={() => { options.forceInstancedOnly = !options.forceInstancedOnly }}
|
||||||
|
overlayColor={forceInstancedOnly ? 'orange' : undefined}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={enableSingleColorMode ? 'Disable Single Color Mode' : 'Enable Single Color Mode'}
|
||||||
|
onClick={() => { options.enableSingleColorMode = !options.enableSingleColorMode }}
|
||||||
|
overlayColor={enableSingleColorMode ? 'yellow' : undefined}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
label="Instanced Distance"
|
||||||
|
min={1}
|
||||||
|
max={16}
|
||||||
|
style={{ width: '100%', }}
|
||||||
|
value={instancedOnlyDistance}
|
||||||
|
updateValue={(value) => {
|
||||||
|
options.instancedOnlyDistance = Math.round(value)
|
||||||
|
}}
|
||||||
|
unit=""
|
||||||
|
valueDisplay={instancedOnlyDistance}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,18 @@ export const watchOptionsAfterViewerInit = () => {
|
||||||
watchValue(options, o => {
|
watchValue(options, o => {
|
||||||
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
|
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watchValue(options, o => {
|
||||||
|
appViewer.inWorldRenderingConfig.autoLowerRenderDistance = o.autoLowerRenderDistance
|
||||||
|
})
|
||||||
|
|
||||||
|
// Instanced rendering options
|
||||||
|
watchValue(options, o => {
|
||||||
|
appViewer.inWorldRenderingConfig.useInstancedRendering = o.useInstancedRendering
|
||||||
|
appViewer.inWorldRenderingConfig.forceInstancedOnly = o.forceInstancedOnly
|
||||||
|
appViewer.inWorldRenderingConfig.instancedOnlyDistance = o.instancedOnlyDistance
|
||||||
|
appViewer.inWorldRenderingConfig.enableSingleColorMode = o.enableSingleColorMode
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
|
export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue