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 -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
||||
- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?)
|
||||
- Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default)
|
||||
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import * as THREE from 'three'
|
||||
import globalTexture from 'mc-assets/dist/blocksAtlasLegacy.png'
|
||||
|
||||
// Import the renderBlockThree function
|
||||
import { renderBlockThree } from '../renderer/viewer/lib/mesher/standaloneRenderer'
|
||||
|
||||
// Create scene, camera and renderer
|
||||
const scene = new THREE.Scene()
|
||||
|
|
@ -8,53 +12,292 @@ renderer.setSize(window.innerWidth, window.innerHeight)
|
|||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Position camera
|
||||
camera.position.z = 5
|
||||
camera.position.set(3, 3, 3)
|
||||
camera.lookAt(0, 0, 0)
|
||||
|
||||
// Create a canvas with some content
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
const ctx = canvas.getContext('2d')
|
||||
// Dark background
|
||||
scene.background = new THREE.Color(0x333333)
|
||||
|
||||
scene.background = new THREE.Color(0x444444)
|
||||
// Add some lighting
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
|
||||
scene.add(ambientLight)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)
|
||||
directionalLight.position.set(1, 1, 1)
|
||||
scene.add(directionalLight)
|
||||
|
||||
// Draw something on the canvas
|
||||
ctx.fillStyle = '#444444'
|
||||
// ctx.fillRect(0, 0, 256, 256)
|
||||
ctx.fillStyle = 'red'
|
||||
ctx.font = '48px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText('Hello!', 128, 128)
|
||||
// Add grid helper for orientation
|
||||
const gridHelper = new THREE.GridHelper(10, 10)
|
||||
scene.add(gridHelper)
|
||||
|
||||
// Create bitmap and texture
|
||||
async function createTexturedBox() {
|
||||
const canvas2 = new OffscreenCanvas(256, 256)
|
||||
const ctx2 = canvas2.getContext('2d')!
|
||||
ctx2.drawImage(canvas, 0, 0)
|
||||
const texture = new THREE.Texture(canvas2)
|
||||
// Create shared material that will be used by all blocks
|
||||
const sharedMaterial = new THREE.MeshLambertMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
// wireframe: true // Add wireframe for debugging
|
||||
})
|
||||
|
||||
// Create simple block models for testing
|
||||
function createFullBlockModel(textureObj: any): any {
|
||||
return [[{
|
||||
elements: [{
|
||||
from: [0, 0, 0],
|
||||
to: [16, 16, 16],
|
||||
faces: {
|
||||
up: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
down: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
north: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
south: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
east: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
west: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]]
|
||||
}
|
||||
|
||||
function createHalfBlockModel(textureObj: any): any {
|
||||
return [[{
|
||||
elements: [{
|
||||
from: [0, 0, 0],
|
||||
to: [16, 8, 16], // Half height (8 instead of 16)
|
||||
faces: {
|
||||
up: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
down: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 16]
|
||||
},
|
||||
north: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 8] // Half height UV
|
||||
},
|
||||
south: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 8] // Half height UV
|
||||
},
|
||||
east: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 8] // Half height UV
|
||||
},
|
||||
west: {
|
||||
texture: textureObj,
|
||||
uv: [0, 0, 16, 8] // Half height UV
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]]
|
||||
}
|
||||
|
||||
let currentFullBlockInstancedMesh: THREE.InstancedMesh | null = null
|
||||
let currentHalfBlockInstancedMesh: THREE.InstancedMesh | null = null
|
||||
|
||||
async function createInstancedBlock() {
|
||||
try {
|
||||
// Clean up previous meshes if they exist
|
||||
if (currentFullBlockInstancedMesh) {
|
||||
scene.remove(currentFullBlockInstancedMesh)
|
||||
currentFullBlockInstancedMesh.geometry.dispose()
|
||||
}
|
||||
if (currentHalfBlockInstancedMesh) {
|
||||
scene.remove(currentHalfBlockInstancedMesh)
|
||||
currentHalfBlockInstancedMesh.geometry.dispose()
|
||||
}
|
||||
|
||||
// Load the blocks atlas texture
|
||||
const textureLoader = new THREE.TextureLoader()
|
||||
const texture = await textureLoader.loadAsync(globalTexture)
|
||||
|
||||
// Configure texture for pixel art
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.needsUpdate = true
|
||||
texture.generateMipmaps = false
|
||||
texture.flipY = false
|
||||
|
||||
// Create box with texture
|
||||
const geometry = new THREE.BoxGeometry(2, 2, 2)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide,
|
||||
premultipliedAlpha: false,
|
||||
})
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
scene.add(cube)
|
||||
// Set the texture on our shared material
|
||||
sharedMaterial.map = texture
|
||||
sharedMaterial.needsUpdate = true
|
||||
|
||||
console.log('Texture loaded:', texture.image.width, 'x', texture.image.height)
|
||||
|
||||
// Calculate UV coordinates for the first tile (top-left, 16x16)
|
||||
const atlasWidth = texture.image.width
|
||||
const atlasHeight = texture.image.height
|
||||
const tileSize = 16
|
||||
|
||||
const textureInfo = {
|
||||
u: 0 / atlasWidth, // Left edge (first column)
|
||||
v: 2 * tileSize / atlasHeight, // Top edge (third row)
|
||||
su: tileSize / atlasWidth, // Width of one tile
|
||||
sv: tileSize / atlasHeight // Height of one tile
|
||||
}
|
||||
|
||||
console.log('Atlas size:', atlasWidth, 'x', atlasHeight)
|
||||
console.log('Calculated texture info:', textureInfo)
|
||||
|
||||
// Create mock texture object that matches what the renderer expects
|
||||
const mockTexture = {
|
||||
u: textureInfo.u,
|
||||
v: textureInfo.v,
|
||||
su: textureInfo.su,
|
||||
sv: textureInfo.sv,
|
||||
debugName: 'test_texture'
|
||||
}
|
||||
|
||||
// Create block models with the mock texture
|
||||
const fullBlockModel = createFullBlockModel(mockTexture)
|
||||
const halfBlockModel = createHalfBlockModel(mockTexture)
|
||||
|
||||
// Mock data for the renderBlockThree function
|
||||
const mockBlock = undefined // No specific block data needed for this test
|
||||
const mockBiome = 'plains'
|
||||
const mockMcData = {} as any
|
||||
const mockVariants = []
|
||||
const mockNeighbors = {}
|
||||
|
||||
// Render the full block
|
||||
const fullBlockGeometry = renderBlockThree(
|
||||
fullBlockModel,
|
||||
mockBlock,
|
||||
mockBiome,
|
||||
mockMcData,
|
||||
mockVariants,
|
||||
mockNeighbors
|
||||
)
|
||||
|
||||
// Render the half block
|
||||
const halfBlockGeometry = renderBlockThree(
|
||||
halfBlockModel,
|
||||
mockBlock,
|
||||
mockBiome,
|
||||
mockMcData,
|
||||
mockVariants,
|
||||
mockNeighbors
|
||||
)
|
||||
|
||||
// Create instanced mesh for full blocks
|
||||
currentFullBlockInstancedMesh = new THREE.InstancedMesh(fullBlockGeometry, sharedMaterial, 2) // Support 2 instances
|
||||
const matrix = new THREE.Matrix4()
|
||||
|
||||
// First instance (full block)
|
||||
matrix.setPosition(-1.5, 0.5, 0.5)
|
||||
currentFullBlockInstancedMesh.setMatrixAt(0, matrix)
|
||||
|
||||
// Second instance (full block)
|
||||
matrix.setPosition(1.5, 0.5, 0.5)
|
||||
currentFullBlockInstancedMesh.setMatrixAt(1, matrix)
|
||||
|
||||
currentFullBlockInstancedMesh.count = 2
|
||||
currentFullBlockInstancedMesh.instanceMatrix.needsUpdate = true
|
||||
scene.add(currentFullBlockInstancedMesh)
|
||||
|
||||
// Create instanced mesh for half blocks
|
||||
currentHalfBlockInstancedMesh = new THREE.InstancedMesh(halfBlockGeometry, sharedMaterial, 1) // Support 1 instance
|
||||
const halfMatrix = new THREE.Matrix4()
|
||||
|
||||
// Half block instance
|
||||
halfMatrix.setPosition(0, 0.75, 0.5) // Positioned higher so top aligns with full blocks
|
||||
currentHalfBlockInstancedMesh.setMatrixAt(0, halfMatrix)
|
||||
|
||||
currentHalfBlockInstancedMesh.count = 1
|
||||
currentHalfBlockInstancedMesh.instanceMatrix.needsUpdate = true
|
||||
scene.add(currentHalfBlockInstancedMesh)
|
||||
|
||||
console.log('Instanced blocks created successfully')
|
||||
console.log('Full block geometry:', fullBlockGeometry)
|
||||
console.log('Half block geometry:', halfBlockGeometry)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating instanced blocks:', error)
|
||||
|
||||
// Fallback: create colored cubes
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||
const material = new THREE.MeshLambertMaterial({ color: 0xff0000, wireframe: true })
|
||||
const fallbackMesh = new THREE.Mesh(geometry, material)
|
||||
fallbackMesh.position.set(0, 0.5, 0.5)
|
||||
scene.add(fallbackMesh)
|
||||
|
||||
console.log('Created fallback colored cube')
|
||||
}
|
||||
}
|
||||
|
||||
// Create the textured box
|
||||
createTexturedBox()
|
||||
// Create the instanced block
|
||||
createInstancedBlock().then(() => {
|
||||
render()
|
||||
})
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
renderer.render(scene, camera)
|
||||
// Simple render loop (no animation)
|
||||
function render() {
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
animate()
|
||||
|
||||
// Add mouse controls for better viewing
|
||||
let mouseDown = false
|
||||
let mouseX = 0
|
||||
let mouseY = 0
|
||||
|
||||
renderer.domElement.addEventListener('mousedown', (event) => {
|
||||
mouseDown = true
|
||||
mouseX = event.clientX
|
||||
mouseY = event.clientY
|
||||
})
|
||||
|
||||
renderer.domElement.addEventListener('mousemove', (event) => {
|
||||
if (!mouseDown) return
|
||||
|
||||
const deltaX = event.clientX - mouseX
|
||||
const deltaY = event.clientY - mouseY
|
||||
|
||||
// Rotate camera around the center
|
||||
const spherical = new THREE.Spherical()
|
||||
spherical.setFromVector3(camera.position)
|
||||
spherical.theta -= deltaX * 0.01
|
||||
spherical.phi += deltaY * 0.01
|
||||
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
|
||||
|
||||
camera.position.setFromSpherical(spherical)
|
||||
camera.lookAt(0, 0, 0)
|
||||
|
||||
mouseX = event.clientX
|
||||
mouseY = event.clientY
|
||||
|
||||
render()
|
||||
})
|
||||
|
||||
renderer.domElement.addEventListener('mouseup', () => {
|
||||
mouseDown = false
|
||||
})
|
||||
|
||||
// Add button to recreate blocks (for testing)
|
||||
const button = document.createElement('button')
|
||||
button.textContent = 'Recreate Blocks'
|
||||
button.style.position = 'fixed'
|
||||
button.style.top = '10px'
|
||||
button.style.left = '10px'
|
||||
button.addEventListener('click', () => {
|
||||
createInstancedBlock()
|
||||
render()
|
||||
})
|
||||
document.body.appendChild(button)
|
||||
|
||||
// Initial render
|
||||
render()
|
||||
|
|
|
|||
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 { World } from './world'
|
||||
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
||||
import { BlockStateModelInfo } from './shared'
|
||||
import { BlockStateModelInfo, InstancingMode } from './shared'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
|
||||
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
||||
|
|
@ -17,7 +17,7 @@ if (module.require) {
|
|||
|
||||
let workerIndex = 0
|
||||
let world: World
|
||||
let dirtySections = new Map<string, number>()
|
||||
let dirtySections = new Map<string, { key: string, instancingMode: InstancingMode, times: number }>()
|
||||
let allDataReady = false
|
||||
|
||||
function sectionKey (x, y, z) {
|
||||
|
|
@ -47,7 +47,7 @@ function drainQueue (from, to) {
|
|||
queuedMessages = queuedMessages.slice(to)
|
||||
}
|
||||
|
||||
function setSectionDirty (pos, value = true) {
|
||||
function setSectionDirty (pos, value = true, instancingMode = InstancingMode.None) {
|
||||
const x = Math.floor(pos.x / 16) * 16
|
||||
const y = Math.floor(pos.y / 16) * 16
|
||||
const z = Math.floor(pos.z / 16) * 16
|
||||
|
|
@ -60,7 +60,7 @@ function setSectionDirty (pos, value = true) {
|
|||
|
||||
const chunk = world.getColumn(x, z)
|
||||
if (chunk?.getSection(pos)) {
|
||||
dirtySections.set(key, (dirtySections.get(key) || 0) + 1)
|
||||
dirtySections.set(key, { key, instancingMode, times: (dirtySections.get(key)?.times || 0) + 1 })
|
||||
} else {
|
||||
postMessage({ type: 'sectionFinished', key, workerIndex })
|
||||
}
|
||||
|
|
@ -68,8 +68,7 @@ function setSectionDirty (pos, value = true) {
|
|||
|
||||
const softCleanup = () => {
|
||||
// clean block cache and loaded chunks
|
||||
world = new World(world.config.version)
|
||||
globalThis.world = world
|
||||
world.blockCache = {}
|
||||
}
|
||||
|
||||
const handleMessage = data => {
|
||||
|
|
@ -98,12 +97,14 @@ const handleMessage = data => {
|
|||
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
||||
allDataReady = true
|
||||
workerIndex = data.workerIndex
|
||||
world.instancedBlocks = data.instancedBlocks
|
||||
world.instancedBlockIds = data.instancedBlockIds || {}
|
||||
|
||||
break
|
||||
}
|
||||
case 'dirty': {
|
||||
const loc = new Vec3(data.x, data.y, data.z)
|
||||
setSectionDirty(loc, data.value)
|
||||
setSectionDirty(loc, data.value, data.instancingMode || InstancingMode.None)
|
||||
|
||||
break
|
||||
}
|
||||
|
|
@ -190,6 +191,18 @@ self.onmessage = ({ data }) => {
|
|||
handleMessage(data)
|
||||
}
|
||||
|
||||
// Debug flag to spam last geometry output
|
||||
globalThis.DEBUG_GEOMETRY_SPAM = false // set to true to enable geometry spam for performance testing
|
||||
globalThis.lastGeometryKey = null
|
||||
|
||||
// Track last 50 unique geometry objects with their respective keys for aggressive debugging
|
||||
interface GeometryEntry {
|
||||
key: string
|
||||
geometry: any
|
||||
}
|
||||
const lastGeometryEntries: GeometryEntry[] = []
|
||||
const MAX_GEOMETRY_ENTRIES = 50
|
||||
|
||||
setInterval(() => {
|
||||
if (world === null || !allDataReady) return
|
||||
|
||||
|
|
@ -197,23 +210,37 @@ setInterval(() => {
|
|||
// console.log(sections.length + ' dirty sections')
|
||||
|
||||
// const start = performance.now()
|
||||
for (const key of dirtySections.keys()) {
|
||||
for (const [key, { instancingMode }] of dirtySections.entries()) {
|
||||
const [x, y, z] = key.split(',').map(v => parseInt(v, 10))
|
||||
const chunk = world.getColumn(x, z)
|
||||
let processTime = 0
|
||||
if (chunk?.getSection(new Vec3(x, y, z))) {
|
||||
const start = performance.now()
|
||||
const geometry = getSectionGeometry(x, y, z, world)
|
||||
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
|
||||
//@ts-expect-error
|
||||
postMessage({ type: 'geometry', key, geometry, workerIndex }, transferable)
|
||||
const geometry = getSectionGeometry(x, y, z, world, instancingMode)
|
||||
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean) as any
|
||||
postMessage({ type: 'geometry', key, geometry, workerIndex }/* , transferable */)
|
||||
processTime = performance.now() - start
|
||||
// Store last geometry for debug spam
|
||||
globalThis.lastGeometryKey = key
|
||||
|
||||
// Track unique geometry entries for aggressive debugging
|
||||
const existingIndex = lastGeometryEntries.findIndex(entry => entry.key === key)
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing entry with new geometry
|
||||
lastGeometryEntries[existingIndex].geometry = geometry
|
||||
} else {
|
||||
// Add new entry
|
||||
lastGeometryEntries.push({ key, geometry })
|
||||
if (lastGeometryEntries.length > MAX_GEOMETRY_ENTRIES) {
|
||||
lastGeometryEntries.shift() // Remove oldest
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// console.info('[mesher] Missing section', x, y, z)
|
||||
}
|
||||
const dirtyTimes = dirtySections.get(key)
|
||||
if (!dirtyTimes) throw new Error('dirtySections.get(key) is falsy')
|
||||
for (let i = 0; i < dirtyTimes; i++) {
|
||||
for (let i = 0; i < dirtyTimes.times; i++) {
|
||||
postMessage({ type: 'sectionFinished', key, workerIndex, processTime })
|
||||
processTime = 0
|
||||
}
|
||||
|
|
@ -237,3 +264,24 @@ setInterval(() => {
|
|||
// const time = performance.now() - start
|
||||
// console.log(`Processed ${sections.length} sections in ${time} ms (${time / sections.length} ms/section)`)
|
||||
}, 50)
|
||||
|
||||
// Debug spam: repeatedly send last geometry output every 100ms
|
||||
setInterval(() => {
|
||||
if (globalThis.DEBUG_GEOMETRY_SPAM) {
|
||||
// Send the last geometry
|
||||
|
||||
// Aggressive debugging: send all tracked geometry entries with their respective geometries
|
||||
// console.log(`[DEBUG] Sending ${lastGeometryEntries.length} unique geometry entries:`, lastGeometryEntries.map(e => e.key))
|
||||
|
||||
// Send each unique geometry entry with its respective geometry for maximum stress testing
|
||||
for (const entry of lastGeometryEntries) {
|
||||
postMessage({
|
||||
type: 'geometry',
|
||||
key: entry.key,
|
||||
geometry: entry.geometry,
|
||||
workerIndex,
|
||||
debug: true // Mark as debug message
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 20)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { BlockType } from '../../../playground/shared'
|
|||
import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock } from './world'
|
||||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
import { MesherGeometryOutput, HighestBlockInfo } from './shared'
|
||||
|
||||
import { MesherGeometryOutput, InstancingMode } from './shared'
|
||||
import { isBlockInstanceable } from './instancingUtils'
|
||||
|
||||
let blockProvider: WorldBlockProvider
|
||||
|
||||
|
|
@ -132,7 +132,11 @@ const getVec = (v: Vec3, dir: Vec3) => {
|
|||
return v.plus(dir)
|
||||
}
|
||||
|
||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) {
|
||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean, instancingEnabled: boolean) {
|
||||
if (instancingEnabled) {
|
||||
return // todo
|
||||
}
|
||||
|
||||
const heights: number[] = []
|
||||
for (let z = -1; z <= 1; z++) {
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
|
|
@ -518,10 +522,64 @@ const isBlockWaterlogged = (block: Block) => {
|
|||
return block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block.name)
|
||||
}
|
||||
|
||||
const shouldCullInstancedBlock = (world: World, cursor: Vec3, block: Block): boolean => {
|
||||
// Early return for blocks that should never be culled
|
||||
if (block.transparent) return false
|
||||
|
||||
// Check if all 6 faces would be culled (hidden by neighbors)
|
||||
const cullIfIdentical = block.name.includes('glass') || block.name.includes('ice')
|
||||
|
||||
// Cache cursor offset to avoid creating new Vec3 instances
|
||||
const offsetCursor = new Vec3(0, 0, 0)
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const face in elemFaces) {
|
||||
const { dir } = elemFaces[face]
|
||||
offsetCursor.set(cursor.x + dir[0], cursor.y + dir[1], cursor.z + dir[2])
|
||||
const neighbor = world.getBlock(offsetCursor, blockProvider, {})
|
||||
|
||||
// Face is exposed to air/void - block must be rendered
|
||||
if (!neighbor) return false
|
||||
|
||||
// Handle special case for identical blocks (glass/ice)
|
||||
if (cullIfIdentical && neighbor.stateId === block.stateId) continue
|
||||
|
||||
// If neighbor is not a full opaque cube, face is visible
|
||||
if (neighbor.transparent || !isCube(neighbor)) return false
|
||||
}
|
||||
|
||||
// All faces are culled, block should not be rendered
|
||||
return true
|
||||
}
|
||||
|
||||
// Add matrix calculation helper
|
||||
function calculateInstanceMatrix (pos: { x: number, y: number, z: number }, offset = 0.5): number[] {
|
||||
// Create a 4x4 matrix array (16 elements)
|
||||
const matrix = Array.from({ length: 16 }).fill(0) as number[]
|
||||
|
||||
// Set identity matrix
|
||||
matrix[0] = 1 // m11
|
||||
matrix[5] = 1 // m22
|
||||
matrix[10] = 1 // m33
|
||||
matrix[15] = 1 // m44
|
||||
|
||||
// Set translation (position)
|
||||
matrix[12] = pos.x + offset // tx
|
||||
matrix[13] = pos.y + offset // ty
|
||||
matrix[14] = pos.z + offset // tz
|
||||
|
||||
return matrix
|
||||
}
|
||||
|
||||
let unknownBlockModel: BlockModelPartsResolved
|
||||
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World) {
|
||||
|
||||
export function getSectionGeometry (sx: number, sy: number, sz: number, world: World, instancingMode = InstancingMode.None): MesherGeometryOutput {
|
||||
let delayedRender = [] as Array<() => void>
|
||||
|
||||
// Check if instanced rendering is enabled for this section
|
||||
const enableInstancedRendering = instancingMode !== InstancingMode.None
|
||||
const forceInstancedOnly = instancingMode === InstancingMode.BlockInstancingOnly || instancingMode === InstancingMode.ColorOnly
|
||||
|
||||
const attr: MesherGeometryOutput = {
|
||||
sx: sx + 8,
|
||||
sy: sy + 8,
|
||||
|
|
@ -543,7 +601,8 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
signs: {},
|
||||
// isFull: true,
|
||||
hadErrors: false,
|
||||
blocksCount: 0
|
||||
blocksCount: 0,
|
||||
instancedBlocks: {}
|
||||
}
|
||||
|
||||
const cursor = new Vec3(0, 0, 0)
|
||||
|
|
@ -606,14 +665,56 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
const pos = cursor.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
delayedRender.push(() => {
|
||||
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged)
|
||||
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged, forceInstancedOnly)
|
||||
})
|
||||
attr.blocksCount++
|
||||
} else if (block.name === 'lava') {
|
||||
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false)
|
||||
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false, forceInstancedOnly)
|
||||
attr.blocksCount++
|
||||
}
|
||||
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
|
||||
// Check if this block can use instanced rendering
|
||||
if ((enableInstancedRendering && isBlockInstanceable(world, block))/* || forceInstancedOnly */) {
|
||||
// Check if block should be culled (all faces hidden by neighbors)
|
||||
// TODO validate this
|
||||
if (shouldCullInstancedBlock(world, cursor, block)) {
|
||||
// Block is completely surrounded, skip rendering
|
||||
continue
|
||||
}
|
||||
|
||||
const blockKey = block.name
|
||||
if (!attr.instancedBlocks[blockKey]) {
|
||||
attr.instancedBlocks[blockKey] = {
|
||||
stateId: block.stateId,
|
||||
blockName: block.name,
|
||||
positions: [],
|
||||
matrices: [] // Add matrices array
|
||||
}
|
||||
}
|
||||
|
||||
const pos = {
|
||||
x: cursor.x,
|
||||
y: cursor.y,
|
||||
z: cursor.z
|
||||
}
|
||||
|
||||
// Pre-calculate transformation matrix
|
||||
const offset = instancingMode === InstancingMode.ColorOnly ? 0 : 0.5
|
||||
const matrix = calculateInstanceMatrix(pos, offset)
|
||||
|
||||
attr.instancedBlocks[blockKey].positions.push(pos)
|
||||
attr.instancedBlocks[blockKey].matrices.push(matrix)
|
||||
|
||||
attr.blocksCount++
|
||||
continue // Skip regular geometry generation for instanceable blocks
|
||||
}
|
||||
|
||||
// Skip buffer geometry generation if force instanced only mode is enabled
|
||||
if (forceInstancedOnly) {
|
||||
// In force instanced only mode, skip all non-instanceable blocks
|
||||
continue
|
||||
}
|
||||
|
||||
// cache
|
||||
let { models } = block
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { BlockType } from '../../../playground/shared'
|
||||
|
||||
export enum InstancingMode {
|
||||
None = 'none',
|
||||
ColorOnly = 'color_only',
|
||||
BlockInstancing = 'block_instancing',
|
||||
BlockInstancingOnly = 'block_instancing_only'
|
||||
}
|
||||
|
||||
// only here for easier testing
|
||||
export const defaultMesherConfig = {
|
||||
version: '',
|
||||
|
|
@ -12,7 +19,7 @@ export const defaultMesherConfig = {
|
|||
// textureSize: 1024, // for testing
|
||||
debugModelVariant: undefined as undefined | number[],
|
||||
clipWorldBelowY: undefined as undefined | number,
|
||||
disableSignsMapsSupport: false
|
||||
disableSignsMapsSupport: false,
|
||||
}
|
||||
|
||||
export type CustomBlockModels = {
|
||||
|
|
@ -21,6 +28,19 @@ export type CustomBlockModels = {
|
|||
|
||||
export type MesherConfig = typeof defaultMesherConfig
|
||||
|
||||
export interface InstancedBlockEntry {
|
||||
stateId: number
|
||||
blockName: string
|
||||
positions: Array<{ x: number, y: number, z: number }>
|
||||
matrices: number[][] // Pre-calculated transformation matrices from worker
|
||||
}
|
||||
|
||||
export type InstancingMesherData = {
|
||||
blocks: {
|
||||
[stateId: number]: number // instance id
|
||||
}
|
||||
}
|
||||
|
||||
export type MesherGeometryOutput = {
|
||||
sx: number,
|
||||
sy: number,
|
||||
|
|
@ -45,6 +65,8 @@ export type MesherGeometryOutput = {
|
|||
hadErrors: boolean
|
||||
blocksCount: number
|
||||
customBlockModels?: CustomBlockModels
|
||||
// New instanced blocks data
|
||||
instancedBlocks: Record<string, InstancedBlockEntry>
|
||||
}
|
||||
|
||||
export interface MesherMainEvents {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
|
|||
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
|
||||
import legacyJson from '../../../../src/preflatMap.json'
|
||||
import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey } from './shared'
|
||||
import { defaultMesherConfig, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig } from './shared'
|
||||
import { INVISIBLE_BLOCKS } from './worldConstants'
|
||||
|
||||
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
||||
|
|
@ -42,12 +42,14 @@ export class World {
|
|||
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels
|
||||
sentBlockStateModels = new Set<string>()
|
||||
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
|
||||
instancedBlocks: number[] = []
|
||||
instancedBlockIds = {} as Record<number, number>
|
||||
|
||||
constructor (version) {
|
||||
constructor (version: string) {
|
||||
this.Chunk = Chunks(version) as any
|
||||
this.biomeCache = mcData(version).biomes
|
||||
this.preflat = !mcData(version).supportFeature('blockStateId')
|
||||
this.config.version = version
|
||||
this.config = { ...defaultMesherConfig, version }
|
||||
}
|
||||
|
||||
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
|
||||
|
|
@ -121,7 +123,6 @@ export class World {
|
|||
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
|
||||
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
|
||||
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
|
||||
const modelOverride = this.customBlockModels.get(key)?.[blockPosKey]
|
||||
|
||||
const column = this.columns[key]
|
||||
// null column means chunk not loaded
|
||||
|
|
@ -131,6 +132,7 @@ export class World {
|
|||
const locInChunk = posInChunk(loc)
|
||||
const stateId = column.getBlockStateId(locInChunk)
|
||||
|
||||
const modelOverride = stateId ? this.customBlockModels.get(key)?.[blockPosKey] : undefined
|
||||
const cacheKey = getBlockAssetsCacheKey(stateId, modelOverride)
|
||||
|
||||
if (!this.blockCache[cacheKey]) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export type WorldDataEmitterEvents = {
|
|||
entityMoved: (data: any) => void
|
||||
playerEntity: (data: any) => void
|
||||
time: (data: number) => void
|
||||
renderDistance: (viewDistance: number) => void
|
||||
renderDistance: (viewDistance: number, keepChunksDistance: number) => void
|
||||
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
|
||||
markAsLoaded: (data: { x: number, z: number }) => void
|
||||
unloadChunk: (data: { x: number, z: number }) => void
|
||||
|
|
@ -79,7 +79,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
|
|||
|
||||
updateViewDistance (viewDistance: number) {
|
||||
this.viewDistance = viewDistance
|
||||
this.emitter.emit('renderDistance', viewDistance)
|
||||
this.emitter.emit('renderDistance', viewDistance, this.keepChunksDistance)
|
||||
}
|
||||
|
||||
listenToBot (bot: typeof __type_bot) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { ResourcesManagerTransferred } from '../../../src/resourcesManager'
|
|||
import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState } from '../../../src/appViewer'
|
||||
import { SoundSystem } from '../three/threeJsSound'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent } from './mesher/shared'
|
||||
import { HighestBlockInfo, CustomBlockModels, BlockStateModelInfo, getBlockAssetsCacheKey, MesherConfig, MesherMainEvent, InstancingMode } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { addNewStat, removeAllStats, updatePanesVisibility, updateStatText } from './ui/newStats'
|
||||
import { WorldDataEmitterWorker } from './worldDataEmitter'
|
||||
|
|
@ -56,7 +56,16 @@ export const defaultWorldRendererConfig = {
|
|||
enableDebugOverlay: false,
|
||||
_experimentalSmoothChunkLoading: true,
|
||||
_renderByChunks: false,
|
||||
volume: 1
|
||||
volume: 1,
|
||||
// New instancing options
|
||||
useInstancedRendering: false,
|
||||
forceInstancedOnly: false,
|
||||
dynamicInstancing: false,
|
||||
dynamicInstancingModeDistance: 1, // chunks beyond this distance use instancing only
|
||||
dynamicColorModeDistance: 1, // chunks beyond this distance use color mode only
|
||||
instancedOnlyDistance: 6, // chunks beyond this distance use instancing only
|
||||
enableSingleColorMode: false, // ultra-performance mode with solid colors
|
||||
autoLowerRenderDistance: false,
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
|
@ -155,6 +164,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
abstract changeBackgroundColor (color: [number, number, number]): void
|
||||
|
||||
// Optional method for getting instanced blocks data (implemented by Three.js renderer)
|
||||
getInstancedBlocksData? (): { instanceableBlocks?: Set<number>, allBlocksStateIdToModelIdMap?: Record<number, number> } | undefined
|
||||
|
||||
worldRendererConfig: WorldRendererConfig
|
||||
playerStateReactive: PlayerStateReactive
|
||||
playerStateUtils: PlayerStateUtils
|
||||
|
|
@ -476,7 +488,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.allChunksFinished = true
|
||||
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
|
||||
}
|
||||
this.updateChunksStats()
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
||||
|
|
@ -591,6 +602,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
const resources = this.resourcesManager.currentResources
|
||||
|
||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
||||
|
||||
// Get instanceable blocks data if available (Three.js specific)
|
||||
const instancedBlocksData = this.getInstancedBlocksData?.()
|
||||
|
||||
for (const [i, worker] of this.workers.entries()) {
|
||||
const { blockstatesModels } = resources
|
||||
|
||||
|
|
@ -602,6 +617,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
},
|
||||
blockstatesModels,
|
||||
config: this.getMesherConfig(),
|
||||
instancedBlocks: instancedBlocksData?.instanceableBlocks ? [...instancedBlocksData.instanceableBlocks] : [],
|
||||
instancedBlockIds: instancedBlocksData?.allBlocksStateIdToModelIdMap || {}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -670,6 +687,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.checkAllFinished()
|
||||
}
|
||||
|
||||
debugRemoveCurrentChunk () {
|
||||
const [x, z] = chunkPos(this.viewerChunkPosition!)
|
||||
this.removeColumn(x, z)
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
|
|
@ -915,7 +937,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
return Promise.all(data)
|
||||
}
|
||||
|
||||
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
|
||||
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false, instancingMode = InstancingMode.None) { // value false is used for unloading chunks
|
||||
if (!this.forceCallFromMesherReplayer && this.mesherLogReader) return
|
||||
|
||||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||
|
|
@ -929,7 +951,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
// Dispatch sections to workers based on position
|
||||
// This guarantees uniformity accross workers and that a given section
|
||||
// is always dispatched to the same worker
|
||||
const hash = this.getWorkerNumber(pos, useChangeWorker && this.mesherLogger.active)
|
||||
const hash = this.getWorkerNumber(pos, this.mesherLogger.active)
|
||||
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
|
||||
if (this.forceCallFromMesherReplayer) {
|
||||
this.workers[hash].postMessage({
|
||||
|
|
@ -938,17 +960,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
y: pos.y,
|
||||
z: pos.z,
|
||||
value,
|
||||
instancingMode,
|
||||
config: this.getMesherConfig(),
|
||||
})
|
||||
} else {
|
||||
this.toWorkerMessagesQueue[hash] ??= []
|
||||
this.toWorkerMessagesQueue[hash].push({
|
||||
// this.workers[hash].postMessage({
|
||||
type: 'dirty',
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
z: pos.z,
|
||||
value,
|
||||
instancingMode,
|
||||
config: this.getMesherConfig(),
|
||||
})
|
||||
this.dispatchMessages()
|
||||
|
|
|
|||
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)
|
||||
return {
|
||||
mesh: outerGroup,
|
||||
meshGeometry: mesh.children.find(child => child instanceof THREE.Mesh)?.geometry,
|
||||
isBlock: true,
|
||||
itemsTexture: null,
|
||||
itemsTextureFlipped: null,
|
||||
|
|
@ -1345,12 +1346,12 @@ export class Entities {
|
|||
}
|
||||
}
|
||||
|
||||
raycastSceneDebug () {
|
||||
debugRaycastScene () {
|
||||
// return any object from scene. raycast from camera
|
||||
const raycaster = new THREE.Raycaster()
|
||||
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
||||
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
|
||||
return intersects[0]?.object
|
||||
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children.filter(child => child.visible))
|
||||
return intersects.find(intersect => intersect.object.visible)?.object
|
||||
}
|
||||
|
||||
private setupPlayerObject (entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {
|
||||
|
|
|
|||
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()
|
||||
}
|
||||
|
||||
render (originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) {
|
||||
render (renderer: THREE.WebGLRenderer) {
|
||||
if (!this.lastHeldItem) return
|
||||
const now = performance.now()
|
||||
if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick
|
||||
|
|
@ -205,15 +205,13 @@ export default class HoldingBlock {
|
|||
|
||||
this.blockSwapAnimation?.switcher.update()
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
const scene = this.worldRenderer.templateScene
|
||||
scene.add(this.cameraGroup)
|
||||
// if (this.camera.aspect !== originalCamera.aspect) {
|
||||
// this.camera.aspect = originalCamera.aspect
|
||||
// this.camera.updateProjectionMatrix()
|
||||
// }
|
||||
this.updateCameraGroup()
|
||||
scene.add(ambientLight.clone())
|
||||
scene.add(directionalLight.clone())
|
||||
|
||||
const viewerSize = renderer.getSize(new THREE.Vector2())
|
||||
const minSize = Math.min(viewerSize.width, viewerSize.height)
|
||||
|
|
@ -241,6 +239,8 @@ export default class HoldingBlock {
|
|||
if (offHandDisplay) {
|
||||
this.cameraGroup.scale.x = 1
|
||||
}
|
||||
|
||||
scene.remove(this.cameraGroup)
|
||||
}
|
||||
|
||||
// worldTest () {
|
||||
|
|
|
|||
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) {
|
||||
this.scene = this.worldRenderer.scene
|
||||
this.scene = this.worldRenderer.realScene
|
||||
}
|
||||
void worldView.init(initPos)
|
||||
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ export class ThreeJsMedia {
|
|||
raycaster.setFromCamera(mouse, camera)
|
||||
|
||||
// Check intersection with all objects in scene
|
||||
const intersects = raycaster.intersectObjects(scene.children, true)
|
||||
const intersects = raycaster.intersectObjects(scene.children.filter(child => child.visible), true)
|
||||
if (intersects.length > 0) {
|
||||
const intersection = intersects[0]
|
||||
const intersectedObject = intersection.object
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { renderSign } from '../sign-renderer'
|
||||
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
|
||||
import { chunkPos, sectionPos } from '../lib/simpleUtils'
|
||||
import { sectionPos } from '../lib/simpleUtils'
|
||||
import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
||||
import { WorldDataEmitterWorker } from '../lib/worldDataEmitter'
|
||||
import { addNewStat } from '../lib/ui/newStats'
|
||||
import { MesherGeometryOutput } from '../lib/mesher/shared'
|
||||
import { MesherGeometryOutput, InstancingMode } from '../lib/mesher/shared'
|
||||
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
||||
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||
import { getMyHand } from './hand'
|
||||
import HoldingBlock from './holdingBlock'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { armorModel } from './entity/armorModels'
|
||||
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
||||
import { loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
||||
import { CursorBlock } from './world/cursorBlock'
|
||||
import { getItemUv } from './appShared'
|
||||
import { Entities } from './entities'
|
||||
|
|
@ -23,19 +19,23 @@ import { ThreeJsSound } from './threeJsSound'
|
|||
import { CameraShake } from './cameraShake'
|
||||
import { ThreeJsMedia } from './threeJsMedia'
|
||||
import { Fountain } from './threeJsParticles'
|
||||
import { InstancedRenderer } from './instancedRenderer'
|
||||
import { ChunkMeshManager } from './chunkMeshManager'
|
||||
|
||||
type SectionKey = string
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
outputFormat = 'threeJs' as const
|
||||
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean }> = {}
|
||||
sectionInstancingMode: Record<string, InstancingMode> = {}
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
signsCache = new Map<string, any>()
|
||||
starField: StarField
|
||||
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
||||
holdingBlock: HoldingBlock
|
||||
holdingBlockLeft: HoldingBlock
|
||||
scene = new THREE.Scene()
|
||||
holdingBlock: HoldingBlock | undefined
|
||||
holdingBlockLeft: HoldingBlock | undefined
|
||||
realScene = new THREE.Scene()
|
||||
scene = new THREE.Group()
|
||||
templateScene = new THREE.Scene()
|
||||
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
||||
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
||||
entities = new Entities(this)
|
||||
|
|
@ -47,9 +47,12 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
cameraShake: CameraShake
|
||||
cameraContainer: THREE.Object3D
|
||||
media: ThreeJsMedia
|
||||
instancedRenderer: InstancedRenderer | undefined
|
||||
chunkMeshManager: ChunkMeshManager
|
||||
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
|
||||
camera: THREE.PerspectiveCamera
|
||||
renderTimeAvg = 0
|
||||
chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
|
||||
sectionsOffsetsAnimations = {} as {
|
||||
[chunkKey: string]: {
|
||||
time: number,
|
||||
|
|
@ -72,13 +75,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
private currentPosTween?: tweenJs.Tween<THREE.Vector3>
|
||||
private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
|
||||
private readonly worldOffset = new THREE.Vector3()
|
||||
|
||||
get tilesRendered () {
|
||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
||||
return this.chunkMeshManager.getTotalTiles()
|
||||
}
|
||||
|
||||
get blocksRendered () {
|
||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
|
||||
return this.chunkMeshManager.getTotalBlocks()
|
||||
}
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
|
||||
|
|
@ -94,11 +98,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
this.addDebugOverlay()
|
||||
this.resetScene()
|
||||
void this.init()
|
||||
|
||||
this.soundSystem = new ThreeJsSound(this)
|
||||
this.cameraShake = new CameraShake(this, this.onRender)
|
||||
this.media = new ThreeJsMedia(this)
|
||||
this.instancedRenderer = new InstancedRenderer(this)
|
||||
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
|
||||
|
||||
// Enable bypass pooling for debugging if URL param is present
|
||||
if (new URLSearchParams(location.search).get('bypassMeshPooling') === 'true') {
|
||||
this.chunkMeshManager.bypassPooling = true
|
||||
console.log('ChunkMeshManager: Bypassing pooling for debugging')
|
||||
}
|
||||
|
||||
// this.fountain = new Fountain(this.scene, this.scene, {
|
||||
// position: new THREE.Vector3(0, 10, 0),
|
||||
// })
|
||||
|
|
@ -107,6 +118,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
this.finishChunk(chunkKey)
|
||||
})
|
||||
this.worldSwitchActions()
|
||||
|
||||
void this.init()
|
||||
}
|
||||
|
||||
// Add this method to update world origin
|
||||
private updateWorldOrigin (pos: THREE.Vector3) {
|
||||
// this.worldOffset.copy(pos)
|
||||
// this.scene.position.copy(this.worldOffset).multiplyScalar(-1)
|
||||
}
|
||||
|
||||
get cameraObject () {
|
||||
|
|
@ -122,6 +141,15 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
})
|
||||
}
|
||||
|
||||
override connect (worldView: WorldDataEmitterWorker) {
|
||||
super.connect(worldView)
|
||||
|
||||
// Add additional renderDistance handling for mesh pool updates
|
||||
worldView.on('renderDistance', (viewDistance) => {
|
||||
this.chunkMeshManager.updateViewDistance(viewDistance)
|
||||
})
|
||||
}
|
||||
|
||||
updateEntity (e, isPosUpdate = false) {
|
||||
const overrides = {
|
||||
rotation: {
|
||||
|
|
@ -143,33 +171,44 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
this.entities.handlePlayerEntity(e)
|
||||
}
|
||||
|
||||
resetTemplateScene () {
|
||||
this.templateScene = new THREE.Scene()
|
||||
this.templateScene.add(this.ambientLight.clone())
|
||||
this.templateScene.add(this.directionalLight.clone())
|
||||
}
|
||||
|
||||
resetScene () {
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
||||
this.scene.add(this.ambientLight)
|
||||
this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
||||
this.realScene.add(this.ambientLight)
|
||||
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
||||
this.directionalLight.castShadow = true
|
||||
this.scene.add(this.directionalLight)
|
||||
this.realScene.add(this.directionalLight)
|
||||
|
||||
const size = this.renderer.getSize(new THREE.Vector2())
|
||||
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
||||
this.cameraContainer = new THREE.Object3D()
|
||||
this.cameraContainer.add(this.camera)
|
||||
this.scene.add(this.cameraContainer)
|
||||
this.realScene.add(this.cameraContainer)
|
||||
this.realScene.add(this.scene)
|
||||
|
||||
this.resetTemplateScene()
|
||||
}
|
||||
|
||||
override watchReactivePlayerState () {
|
||||
super.watchReactivePlayerState()
|
||||
this.onReactivePlayerStateUpdated('inWater', (value) => {
|
||||
this.scene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
|
||||
this.realScene.fog = value ? new THREE.Fog(0x00_00_ff, 0.1, this.playerStateReactive.waterBreathing ? 100 : 20) : null
|
||||
})
|
||||
this.onReactivePlayerStateUpdated('ambientLight', (value) => {
|
||||
if (!value) return
|
||||
this.ambientLight.intensity = value
|
||||
this.resetTemplateScene()
|
||||
})
|
||||
this.onReactivePlayerStateUpdated('directionalLight', (value) => {
|
||||
if (!value) return
|
||||
this.directionalLight.intensity = value
|
||||
this.resetTemplateScene()
|
||||
})
|
||||
this.onReactivePlayerStateUpdated('lookingAtBlock', (value) => {
|
||||
this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
|
||||
|
|
@ -185,19 +224,34 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
})
|
||||
}
|
||||
|
||||
getInstancedBlocksData () {
|
||||
const config = this.instancedRenderer?.getInstancedBlocksConfig()
|
||||
if (!config) return undefined
|
||||
|
||||
return {
|
||||
instanceableBlocks: config.instanceableBlocks,
|
||||
}
|
||||
}
|
||||
|
||||
override watchReactiveConfig () {
|
||||
super.watchReactiveConfig()
|
||||
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
|
||||
this.updateShowChunksBorder(value)
|
||||
this.updateShowChunksBorder()
|
||||
})
|
||||
this.onReactiveConfigUpdated('enableDebugOverlay', (value) => {
|
||||
if (!value) {
|
||||
// restore visibility
|
||||
this.chunkMeshManager.updateSectionsVisibility()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (isAnimationPlaying) {
|
||||
holdingBlock.startSwing()
|
||||
holdingBlock?.startSwing()
|
||||
} else {
|
||||
holdingBlock.stopSwing()
|
||||
holdingBlock?.stopSwing()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,8 +278,12 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
oldItemsTexture.dispose()
|
||||
}
|
||||
|
||||
// Prepare and initialize instanced renderer with dynamic block detection
|
||||
this.instancedRenderer?.prepareAndInitialize()
|
||||
|
||||
await super.updateAssetsData()
|
||||
this.onAllTexturesLoaded()
|
||||
|
||||
if (Object.keys(this.loadedChunks).length > 0) {
|
||||
console.log('rerendering chunks because of texture update')
|
||||
this.rerenderAllChunks()
|
||||
|
|
@ -233,14 +291,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
onAllTexturesLoaded () {
|
||||
this.holdingBlock.ready = true
|
||||
this.holdingBlock.updateItem()
|
||||
this.holdingBlockLeft.ready = true
|
||||
this.holdingBlockLeft.updateItem()
|
||||
if (this.holdingBlock) {
|
||||
this.holdingBlock.ready = true
|
||||
this.holdingBlock.updateItem()
|
||||
}
|
||||
if (this.holdingBlockLeft) {
|
||||
this.holdingBlockLeft.ready = true
|
||||
this.holdingBlockLeft.updateItem()
|
||||
}
|
||||
}
|
||||
|
||||
changeBackgroundColor (color: [number, number, number]): void {
|
||||
this.scene.background = new THREE.Color(color[0], color[1], color[2])
|
||||
this.realScene.background = new THREE.Color(color[0], color[1], color[2])
|
||||
}
|
||||
|
||||
timeUpdated (newTime: number): void {
|
||||
|
|
@ -294,12 +356,20 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
const formatBigNumber = (num: number) => {
|
||||
return new Intl.NumberFormat('en-US', {}).format(num)
|
||||
}
|
||||
const instancedStats = this.instancedRenderer?.getStats()
|
||||
let text = ''
|
||||
text += `C: ${formatBigNumber(this.renderer.info.render.calls)} `
|
||||
text += `TR: ${formatBigNumber(this.renderer.info.render.triangles)} `
|
||||
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
|
||||
text += `F: ${formatBigNumber(this.tilesRendered)} `
|
||||
text += `B: ${formatBigNumber(this.blocksRendered)}`
|
||||
text += `B: ${formatBigNumber(this.blocksRendered)} `
|
||||
if (instancedStats) {
|
||||
text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t `
|
||||
text += `DC: ${formatBigNumber(instancedStats.drawCalls)} `
|
||||
}
|
||||
const poolStats = this.chunkMeshManager.getStats()
|
||||
const poolMode = this.chunkMeshManager.bypassPooling ? 'BYPASS' : poolStats.hitRate
|
||||
text += `MP: ${poolStats.activeCount}/${poolStats.poolSize} ${poolMode}`
|
||||
pane.updateText(text)
|
||||
this.backendInfoReport = text
|
||||
}
|
||||
|
|
@ -313,8 +383,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
||||
// sum of distances: x + y + z
|
||||
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
|
||||
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
|
||||
section.renderOrder = 500 - chunkDistance
|
||||
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
|
||||
sectionObject.mesh!.renderOrder = 500 - chunkDistance
|
||||
}
|
||||
|
||||
override updateViewerPosition (pos: Vec3): void {
|
||||
|
|
@ -323,10 +393,22 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
cameraSectionPositionUpdate () {
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in this.sectionObjects) {
|
||||
const value = this.sectionObjects[key]
|
||||
if (!value) continue
|
||||
this.updatePosDataChunk(key)
|
||||
for (const key in this.sectionInstancingMode) {
|
||||
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
|
||||
if (sectionObject) {
|
||||
this.updatePosDataChunk(key)
|
||||
}
|
||||
|
||||
if (this.worldRendererConfig.dynamicInstancing) {
|
||||
const [x, y, z] = key.split(',').map(x => +x)
|
||||
const pos = new Vec3(x, y, z)
|
||||
const instancingMode = this.getInstancingMode(pos)
|
||||
if (instancingMode !== this.sectionInstancingMode[key]) {
|
||||
// console.log('update section', key, this.sectionInstancingMode[key], '->', instancingMode)
|
||||
// update section
|
||||
this.setSectionDirty(pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,123 +419,68 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
finishChunk (chunkKey: string) {
|
||||
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
|
||||
this.sectionObjects[sectionKey].visible = true
|
||||
const sectionObject = this.chunkMeshManager.getSectionObject(sectionKey)
|
||||
if (sectionObject) {
|
||||
sectionObject.visible = true
|
||||
}
|
||||
}
|
||||
delete this.waitingChunksToDisplay[chunkKey]
|
||||
}
|
||||
|
||||
// debugRecomputedDeletedObjects = 0
|
||||
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
|
||||
if (data.type !== 'geometry') return
|
||||
let object: THREE.Object3D = this.sectionObjects[data.key]
|
||||
if (object) {
|
||||
this.scene.remove(object)
|
||||
disposeObject(object)
|
||||
delete this.sectionObjects[data.key]
|
||||
}
|
||||
|
||||
const chunkCoords = data.key.split(',')
|
||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
||||
const chunkKey = chunkCoords[0] + ',' + chunkCoords[2]
|
||||
|
||||
// if (object) {
|
||||
// this.debugRecomputedDeletedObjects++
|
||||
// }
|
||||
const hasInstancedBlocks = data.geometry.instancedBlocks && Object.keys(data.geometry.instancedBlocks).length > 0
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
|
||||
geometry.index = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1)
|
||||
this.instancedRenderer?.removeSectionInstances(data.key)
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, this.material)
|
||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
mesh.name = 'mesh'
|
||||
object = new THREE.Group()
|
||||
object.add(mesh)
|
||||
// mesh with static dimensions: 16x16x16
|
||||
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 }))
|
||||
staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
||||
boxHelper.name = 'helper'
|
||||
object.add(boxHelper)
|
||||
object.name = 'chunk';
|
||||
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
|
||||
(object as any).blocksCount = data.geometry.blocksCount
|
||||
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
// Handle instanced blocks data from worker
|
||||
if (hasInstancedBlocks) {
|
||||
this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key, this.getInstancingMode(new Vec3(chunkCoords[0], chunkCoords[1], chunkCoords[2])))
|
||||
}
|
||||
// should not compute it once
|
||||
if (Object.keys(data.geometry.signs).length) {
|
||||
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
|
||||
const signBlockEntity = this.blockEntities[posKey]
|
||||
if (!signBlockEntity) continue
|
||||
const [x, y, z] = posKey.split(',')
|
||||
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
|
||||
if (!sign) continue
|
||||
object.add(sign)
|
||||
}
|
||||
|
||||
// Check if chunk should be loaded and has geometry
|
||||
if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) {
|
||||
// Release any existing section from the pool
|
||||
this.chunkMeshManager.releaseSection(data.key)
|
||||
return
|
||||
}
|
||||
if (Object.keys(data.geometry.heads).length) {
|
||||
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
|
||||
const headBlockEntity = this.blockEntities[posKey]
|
||||
if (!headBlockEntity) continue
|
||||
const [x, y, z] = posKey.split(',')
|
||||
const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
|
||||
if (!head) continue
|
||||
object.add(head)
|
||||
}
|
||||
|
||||
// Use ChunkMeshManager for optimized mesh handling
|
||||
const sectionObject = this.chunkMeshManager.updateSection(data.key, data.geometry)
|
||||
|
||||
if (!sectionObject) {
|
||||
return
|
||||
}
|
||||
this.sectionObjects[data.key] = object
|
||||
|
||||
|
||||
this.updateBoxHelper(data.key)
|
||||
|
||||
// Handle chunk-based rendering
|
||||
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
|
||||
object.visible = false
|
||||
sectionObject.visible = false
|
||||
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
||||
this.waitingChunksToDisplay[chunkKey] ??= []
|
||||
this.waitingChunksToDisplay[chunkKey].push(data.key)
|
||||
if (this.finishedChunks[chunkKey]) {
|
||||
// todo it might happen even when it was not an update
|
||||
this.finishChunk(chunkKey)
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePosDataChunk(data.key)
|
||||
object.matrixAutoUpdate = false
|
||||
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
|
||||
// mesh.matrixAutoUpdate = false
|
||||
}
|
||||
|
||||
this.scene.add(object)
|
||||
}
|
||||
|
||||
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
||||
const chunk = chunkPos(position)
|
||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||
if (!textures) {
|
||||
textures = {}
|
||||
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
||||
}
|
||||
const texturekey = `${position.x},${position.y},${position.z}`
|
||||
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version)
|
||||
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
tex.minFilter = THREE.NearestFilter
|
||||
tex.needsUpdate = true
|
||||
textures[texturekey] = tex
|
||||
return tex
|
||||
}
|
||||
|
||||
getCameraPosition () {
|
||||
const worldPos = new THREE.Vector3()
|
||||
this.camera.getWorldPosition(worldPos)
|
||||
return worldPos
|
||||
// Add world offset to get true world position
|
||||
return worldPos.add(this.worldOffset)
|
||||
}
|
||||
|
||||
getWorldCameraPosition () {
|
||||
getSectionCameraPosition () {
|
||||
const pos = this.getCameraPosition()
|
||||
return new Vec3(
|
||||
Math.floor(pos.x / 16),
|
||||
|
|
@ -463,7 +490,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
updateCameraSectionPos () {
|
||||
const newSectionPos = this.getWorldCameraPosition()
|
||||
const newSectionPos = this.getSectionCameraPosition()
|
||||
if (!this.cameraSectionPos.equals(newSectionPos)) {
|
||||
this.cameraSectionPos = newSectionPos
|
||||
this.cameraSectionPositionUpdate()
|
||||
|
|
@ -473,6 +500,17 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
const yOffset = this.playerStateReactive.eyeHeight
|
||||
|
||||
if (pos) {
|
||||
// Convert Vec3 to THREE.Vector3
|
||||
const worldPos = new THREE.Vector3(pos.x, pos.y + yOffset, pos.z)
|
||||
|
||||
// Update world origin before updating camera
|
||||
this.updateWorldOrigin(worldPos)
|
||||
|
||||
// Keep camera at origin and move world instead
|
||||
// this.cameraObject.position.set(pos.x, pos.y + yOffset, pos.z)
|
||||
}
|
||||
|
||||
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
this.media.tryIntersectMedia()
|
||||
this.updateCameraSectionPos()
|
||||
|
|
@ -517,7 +555,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
raycaster.far = distance // Limit raycast distance
|
||||
|
||||
// Filter to only nearby chunks for performance
|
||||
const nearbyChunks = Object.values(this.sectionObjects)
|
||||
const nearbyChunks = Object.values(this.chunkMeshManager.sectionObjects)
|
||||
.filter(obj => obj.name === 'chunk' && obj.visible)
|
||||
.filter(obj => {
|
||||
// Get the mesh child which has the actual geometry
|
||||
|
|
@ -675,7 +713,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
chunksRenderBelowOverride !== undefined ||
|
||||
chunksRenderDistanceOverride !== undefined
|
||||
) {
|
||||
for (const [key, object] of Object.entries(this.sectionObjects)) {
|
||||
for (const [key, object] of Object.entries(this.chunkMeshManager.sectionObjects)) {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
const isVisible =
|
||||
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
|
||||
|
|
@ -687,10 +725,6 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
object.visible = isVisible
|
||||
}
|
||||
} else {
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
object.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -716,7 +750,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||
this.renderer.render(this.scene, cam)
|
||||
this.renderer.render(this.realScene, cam)
|
||||
|
||||
if (
|
||||
this.displayOptions.inWorldRenderingConfig.showHand &&
|
||||
|
|
@ -725,14 +759,15 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
// !this.freeFlyMode &&
|
||||
!this.renderer.xr.isPresenting
|
||||
) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, this.ambientLight, this.directionalLight)
|
||||
this.holdingBlock?.render(this.renderer)
|
||||
this.holdingBlockLeft?.render(this.renderer)
|
||||
}
|
||||
|
||||
for (const fountain of this.fountains) {
|
||||
if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) {
|
||||
fountain.createParticles(this.sectionObjects[fountain.sectionId])
|
||||
this.sectionObjects[fountain.sectionId].foutain = true
|
||||
const sectionObject = this.chunkMeshManager.getSectionObject(fountain.sectionId)
|
||||
if (sectionObject && !sectionObject.fountain) {
|
||||
fountain.createParticles(sectionObject)
|
||||
sectionObject.fountain = true
|
||||
}
|
||||
fountain.render()
|
||||
}
|
||||
|
|
@ -742,87 +777,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
const end = performance.now()
|
||||
const totalTime = end - start
|
||||
|
||||
if (this.worldRendererConfig.autoLowerRenderDistance) {
|
||||
// Record render time for performance monitoring
|
||||
this.chunkMeshManager.recordRenderTime(totalTime)
|
||||
}
|
||||
|
||||
this.renderTimeAvgCount++
|
||||
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
|
||||
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
|
||||
this.currentRenderedFrames++
|
||||
}
|
||||
|
||||
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
|
||||
if (!textures) return
|
||||
|
||||
try {
|
||||
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
|
||||
let skinUrl = textureData.textures?.SKIN?.url
|
||||
const { skinTexturesProxy } = this.worldRendererConfig
|
||||
if (skinTexturesProxy) {
|
||||
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
||||
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
||||
}
|
||||
|
||||
const mesh = getMesh(this, skinUrl, armorModel.head)
|
||||
const group = new THREE.Group()
|
||||
if (isWall) {
|
||||
mesh.position.set(0, 0.3125, 0.3125)
|
||||
}
|
||||
// move head model down as armor have a different offset than blocks
|
||||
mesh.position.y -= 23 / 16
|
||||
group.add(mesh)
|
||||
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
|
||||
group.rotation.set(
|
||||
0,
|
||||
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
||||
0
|
||||
)
|
||||
group.scale.set(0.8, 0.8, 0.8)
|
||||
return group
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
}
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
||||
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
||||
|
||||
if (!tex) return
|
||||
|
||||
// todo implement
|
||||
// const key = JSON.stringify({ position, rotation, isWall })
|
||||
// if (this.signsCache.has(key)) {
|
||||
// console.log('cached', key)
|
||||
// } else {
|
||||
// this.signsCache.set(key, tex)
|
||||
// }
|
||||
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
||||
mesh.renderOrder = 999
|
||||
|
||||
const lineHeight = 7 / 16
|
||||
const scaleFactor = isHanging ? 1.3 : 1
|
||||
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
|
||||
|
||||
const thickness = (isHanging ? 2 : 1.5) / 16
|
||||
const wallSpacing = 0.25 / 16
|
||||
if (isWall && !isHanging) {
|
||||
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
|
||||
} else {
|
||||
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.rotation.set(
|
||||
0,
|
||||
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
||||
0
|
||||
)
|
||||
group.add(mesh)
|
||||
const height = (isHanging ? 10 : 8) / 16
|
||||
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
||||
const textPosition = height / 2 + heightOffset
|
||||
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
||||
return group
|
||||
}
|
||||
|
||||
lightUpdate (chunkX: number, chunkZ: number) {
|
||||
// set all sections in the chunk dirty
|
||||
|
|
@ -832,26 +798,27 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
rerenderAllChunks () { // todo not clear what to do with loading chunks
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
this.setSectionDirty(new Vec3(x, y, z))
|
||||
}
|
||||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
child.visible = value
|
||||
}
|
||||
}
|
||||
updateShowChunksBorder () {
|
||||
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
|
||||
this.updateBoxHelper(key)
|
||||
}
|
||||
}
|
||||
|
||||
updateBoxHelper (key: string) {
|
||||
const { showChunkBorders } = this.worldRendererConfig
|
||||
this.chunkMeshManager.updateBoxHelper(key, showChunkBorders, this.chunkBoxMaterial)
|
||||
}
|
||||
|
||||
resetWorld () {
|
||||
super.resetWorld()
|
||||
|
||||
for (const mesh of Object.values(this.sectionObjects)) {
|
||||
for (const mesh of Object.values(this.chunkMeshManager.sectionObjects)) {
|
||||
this.scene.remove(mesh)
|
||||
}
|
||||
|
||||
|
|
@ -868,7 +835,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
|
||||
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
||||
const [currentX, currentY, currentZ] = sectionPos(pos)
|
||||
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
|
||||
return Object.fromEntries(Object.entries(this.chunkMeshManager.sectionObjects).map(([key, o]) => {
|
||||
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
||||
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
||||
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
||||
|
|
@ -885,12 +852,13 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
readdChunks () {
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
this.scene.remove(this.sectionObjects[key])
|
||||
const { sectionObjects } = this.chunkMeshManager
|
||||
for (const key of Object.keys(sectionObjects)) {
|
||||
this.scene.remove(sectionObjects[key])
|
||||
}
|
||||
setTimeout(() => {
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
this.scene.add(this.sectionObjects[key])
|
||||
for (const key of Object.keys(sectionObjects)) {
|
||||
this.scene.add(sectionObjects[key])
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
|
@ -902,6 +870,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
}
|
||||
|
||||
removeCurrentChunk () {
|
||||
const currentChunk = this.cameraSectionPos
|
||||
this.removeColumn(currentChunk.x * 16, currentChunk.z * 16)
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
super.removeColumn(x, z)
|
||||
|
||||
|
|
@ -909,19 +882,47 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
const key = `${x},${y},${z}`
|
||||
const mesh = this.sectionObjects[key]
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh)
|
||||
disposeObject(mesh)
|
||||
}
|
||||
delete this.sectionObjects[key]
|
||||
|
||||
// Remove instanced blocks for this section
|
||||
this.instancedRenderer?.removeSectionInstances(key)
|
||||
|
||||
// Release section from mesh pool (this will also remove from scene)
|
||||
this.chunkMeshManager.releaseSection(key)
|
||||
}
|
||||
}
|
||||
|
||||
setSectionDirty (...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
|
||||
const [pos] = args
|
||||
getInstancingMode (pos: Vec3) {
|
||||
const { useInstancedRendering, enableSingleColorMode, forceInstancedOnly, dynamicInstancing, dynamicInstancingModeDistance, dynamicColorModeDistance } = this.worldRendererConfig
|
||||
let instancingMode = InstancingMode.None
|
||||
|
||||
if (useInstancedRendering || enableSingleColorMode) {
|
||||
instancingMode = enableSingleColorMode
|
||||
? InstancingMode.ColorOnly
|
||||
: forceInstancedOnly
|
||||
? InstancingMode.BlockInstancingOnly
|
||||
: InstancingMode.BlockInstancing
|
||||
} else if (dynamicInstancing) {
|
||||
const dx = pos.x / 16 - this.cameraSectionPos.x
|
||||
const dz = pos.z / 16 - this.cameraSectionPos.z
|
||||
const distance = Math.floor(Math.hypot(dx, dz))
|
||||
// console.log('distance', distance, `${pos.x},${pos.y},${pos.z}`)
|
||||
if (distance > dynamicColorModeDistance) {
|
||||
instancingMode = InstancingMode.ColorOnly
|
||||
} else if (distance > dynamicInstancingModeDistance) {
|
||||
instancingMode = InstancingMode.BlockInstancingOnly
|
||||
}
|
||||
}
|
||||
|
||||
return instancingMode
|
||||
}
|
||||
|
||||
setSectionDirty (pos: Vec3, value = true) {
|
||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||
super.setSectionDirty(...args)
|
||||
const instancingMode = this.getInstancingMode(pos)
|
||||
super.setSectionDirty(pos, value, undefined, instancingMode)
|
||||
if (value) {
|
||||
this.sectionInstancingMode[`${pos.x},${pos.y},${pos.z}`] = instancingMode
|
||||
}
|
||||
}
|
||||
|
||||
static getRendererInfo (renderer: THREE.WebGLRenderer) {
|
||||
|
|
@ -938,6 +939,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
destroy (): void {
|
||||
this.instancedRenderer?.destroy()
|
||||
this.chunkMeshManager.dispose()
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
|
|
@ -950,7 +953,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
const chunkKey = `${chunkX},${chunkZ}`
|
||||
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
|
||||
|
||||
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
|
||||
return !!this.finishedChunks[chunkKey] || !!this.chunkMeshManager.sectionObjects[sectionKey]
|
||||
}
|
||||
|
||||
updateSectionOffsets () {
|
||||
|
|
@ -988,7 +991,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
// Apply the offset to the section object
|
||||
const section = this.sectionObjects[key]
|
||||
const section = this.chunkMeshManager.sectionObjects[key]
|
||||
if (section) {
|
||||
section.position.set(
|
||||
anim.currentOffsetX,
|
||||
|
|
|
|||
|
|
@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
|
|||
lockUrl()
|
||||
}
|
||||
if (command === 'communication.toggleMicrophone') {
|
||||
toggleMicrophoneMuted?.()
|
||||
if (typeof toggleMicrophoneMuted === 'function') {
|
||||
toggleMicrophoneMuted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,28 @@ export default () => {
|
|||
registerMediaChannels()
|
||||
registerSectionAnimationChannels()
|
||||
registeredJeiChannel()
|
||||
sendTokenChannel()
|
||||
})
|
||||
}
|
||||
|
||||
const sendTokenChannel = () => {
|
||||
const CHANNEL_NAME = 'minecraft-web-client:token'
|
||||
bot._client.registerChannel(CHANNEL_NAME, [
|
||||
'container',
|
||||
[
|
||||
{
|
||||
name: 'token',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
},
|
||||
{
|
||||
name: 'userId',
|
||||
type: ['pstring', { countType: 'i16' }]
|
||||
},
|
||||
]
|
||||
], true)
|
||||
bot._client.writeChannel(CHANNEL_NAME, {
|
||||
token: new URLSearchParams(window.location.search).get('token') ?? '',
|
||||
userId: new URLSearchParams(window.location.search).get('userId') ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ export const defaultOptions = {
|
|||
starfieldRendering: true,
|
||||
enabledResourcepack: null as string | null,
|
||||
useVersionsTextures: 'latest',
|
||||
// Instanced rendering options
|
||||
useInstancedRendering: false,
|
||||
autoLowerRenderDistance: false,
|
||||
forceInstancedOnly: false,
|
||||
instancedOnlyDistance: 6,
|
||||
enableSingleColorMode: false,
|
||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||
showHand: true,
|
||||
viewBobbing: true,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { versionToNumber } from 'mc-assets/dist/utils'
|
|||
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
||||
import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage'
|
||||
import Button from './react/Button'
|
||||
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||
import { OptionButton, OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||
import Slider from './react/Slider'
|
||||
import { getScreenRefreshRate } from './utils'
|
||||
import { setLoadingScreenStatus } from './appStatus'
|
||||
|
|
@ -114,6 +114,52 @@ export const guiOptionsScheme: {
|
|||
text: 'Performance Debug',
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
let status = 'OFF'
|
||||
const { useInstancedRendering, forceInstancedOnly, enableSingleColorMode } = useSnapshot(options)
|
||||
if (useInstancedRendering) {
|
||||
status = 'ON'
|
||||
if (enableSingleColorMode) {
|
||||
status = 'ON (single color)'
|
||||
} else if (forceInstancedOnly) {
|
||||
status = 'ON (force)'
|
||||
}
|
||||
}
|
||||
|
||||
return <OptionButton
|
||||
item={{
|
||||
type: 'toggle',
|
||||
text: 'Instacing',
|
||||
requiresChunksReload: true,
|
||||
}}
|
||||
cacheKey='instacing'
|
||||
valueText={status}
|
||||
onClick={() => {
|
||||
// cycle
|
||||
if (useInstancedRendering) {
|
||||
if (enableSingleColorMode) {
|
||||
options.useInstancedRendering = false
|
||||
options.enableSingleColorMode = false
|
||||
options.forceInstancedOnly = false
|
||||
} else if (forceInstancedOnly) {
|
||||
options.useInstancedRendering = true
|
||||
options.enableSingleColorMode = true
|
||||
options.forceInstancedOnly = false
|
||||
} else {
|
||||
options.useInstancedRendering = true
|
||||
options.enableSingleColorMode = false
|
||||
options.forceInstancedOnly = true
|
||||
}
|
||||
} else {
|
||||
options.useInstancedRendering = true
|
||||
options.enableSingleColorMode = false
|
||||
options.forceInstancedOnly = false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
const { _renderByChunks } = useSnapshot(options).rendererSharedOptions
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
|
|||
const { reactiveDebugParams } = worldRenderer
|
||||
const { chunksRenderAboveEnabled, chunksRenderBelowEnabled, chunksRenderDistanceEnabled, chunksRenderAboveOverride, chunksRenderBelowOverride, chunksRenderDistanceOverride, stopRendering, disableEntities } = useSnapshot(reactiveDebugParams)
|
||||
|
||||
const { rendererPerfDebugOverlay } = useSnapshot(options)
|
||||
const { rendererPerfDebugOverlay, useInstancedRendering, forceInstancedOnly, instancedOnlyDistance, enableSingleColorMode } = useSnapshot(options)
|
||||
|
||||
// Helper to round values to nearest step
|
||||
const roundToStep = (value: number, step: number) => Math.round(value / step) * step
|
||||
|
|
@ -115,5 +115,36 @@ const RendererDebugMenu = ({ worldRenderer }: { worldRenderer: WorldRendererComm
|
|||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className={styles.column}>
|
||||
<h3>Instanced Rendering</h3>
|
||||
<Button
|
||||
label={useInstancedRendering ? 'Disable Instanced Rendering' : 'Enable Instanced Rendering'}
|
||||
onClick={() => { options.useInstancedRendering = !options.useInstancedRendering }}
|
||||
overlayColor={useInstancedRendering ? 'green' : undefined}
|
||||
/>
|
||||
<Button
|
||||
label={forceInstancedOnly ? 'Disable Force Instanced Only' : 'Enable Force Instanced Only'}
|
||||
onClick={() => { options.forceInstancedOnly = !options.forceInstancedOnly }}
|
||||
overlayColor={forceInstancedOnly ? 'orange' : undefined}
|
||||
/>
|
||||
<Button
|
||||
label={enableSingleColorMode ? 'Disable Single Color Mode' : 'Enable Single Color Mode'}
|
||||
onClick={() => { options.enableSingleColorMode = !options.enableSingleColorMode }}
|
||||
overlayColor={enableSingleColorMode ? 'yellow' : undefined}
|
||||
/>
|
||||
<Slider
|
||||
label="Instanced Distance"
|
||||
min={1}
|
||||
max={16}
|
||||
style={{ width: '100%', }}
|
||||
value={instancedOnlyDistance}
|
||||
updateValue={(value) => {
|
||||
options.instancedOnlyDistance = Math.round(value)
|
||||
}}
|
||||
unit=""
|
||||
valueDisplay={instancedOnlyDistance}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,18 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
watchValue(options, o => {
|
||||
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
appViewer.inWorldRenderingConfig.autoLowerRenderDistance = o.autoLowerRenderDistance
|
||||
})
|
||||
|
||||
// Instanced rendering options
|
||||
watchValue(options, o => {
|
||||
appViewer.inWorldRenderingConfig.useInstancedRendering = o.useInstancedRendering
|
||||
appViewer.inWorldRenderingConfig.forceInstancedOnly = o.forceInstancedOnly
|
||||
appViewer.inWorldRenderingConfig.instancedOnlyDistance = o.instancedOnlyDistance
|
||||
appViewer.inWorldRenderingConfig.enableSingleColorMode = o.enableSingleColorMode
|
||||
})
|
||||
}
|
||||
|
||||
export const watchOptionsAfterWorldViewInit = (worldView: WorldDataEmitter) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue