finish manager!

This commit is contained in:
Vitaly Turovsky 2025-07-20 09:24:57 +03:00
commit 2f49cbb35b
7 changed files with 699 additions and 279 deletions

View file

@ -1,5 +1,5 @@
import * as THREE from 'three'
import { loadThreeJsTextureFromBitmap } from '../renderer/viewer/lib/utils/skins'
import globalTexture from 'mc-assets/dist/blocksAtlasLegacy.png'
// Create scene, camera and renderer
const scene = new THREE.Scene()
@ -9,53 +9,193 @@ renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Position camera
camera.position.z = 5
camera.position.set(3, 3, 3)
camera.lookAt(0, 0, 0)
// Create a canvas with some content
const canvas = document.createElement('canvas')
canvas.width = 256
canvas.height = 256
const ctx = canvas.getContext('2d')
// Dark background
scene.background = new THREE.Color(0x333333)
scene.background = new THREE.Color(0x444444)
// Add some lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)
// Draw something on the canvas
ctx.fillStyle = '#444444'
// ctx.fillRect(0, 0, 256, 256)
ctx.fillStyle = 'red'
ctx.font = '48px Arial'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Hello!', 128, 128)
// Create shared material that will be used by all blocks
const sharedMaterial = new THREE.MeshLambertMaterial({
vertexColors: true,
transparent: true,
alphaTest: 0.1
})
// Create bitmap and texture
async function createTexturedBox() {
const canvas2 = new OffscreenCanvas(256, 256)
const ctx2 = canvas2.getContext('2d')!
ctx2.drawImage(canvas, 0, 0)
const texture = new THREE.Texture(canvas2)
function createCustomGeometry(textureInfo: { u: number, v: number, su: number, sv: number }): THREE.BufferGeometry {
// Create custom geometry with specific UV coordinates for this block type
const geometry = new THREE.BoxGeometry(1, 1, 1)
// Get UV attribute
const uvAttribute = geometry.getAttribute('uv') as THREE.BufferAttribute
const uvs = uvAttribute.array as Float32Array
console.log('Original UVs:', Array.from(uvs))
console.log('Texture info:', textureInfo)
// BoxGeometry has 6 faces, each with 2 triangles (4 vertices), so 24 UV pairs total
// Apply the same texture to all faces for simplicity
for (let i = 0; i < uvs.length; i += 2) {
const u = uvs[i]
const v = uvs[i + 1]
// Map from 0-1 to the specific texture region in the atlas
uvs[i] = textureInfo.u + u * textureInfo.su
uvs[i + 1] = textureInfo.v + v * textureInfo.sv
}
console.log('Modified UVs:', Array.from(uvs))
uvAttribute.needsUpdate = true
return geometry
}
let currentInstancedMesh: THREE.InstancedMesh | null = null
let currentRefCube: THREE.Mesh | null = null
async function createInstancedBlock() {
try {
// Clean up previous meshes if they exist
if (currentInstancedMesh) {
scene.remove(currentInstancedMesh)
currentInstancedMesh.geometry.dispose()
}
if (currentRefCube) {
scene.remove(currentRefCube)
currentRefCube.geometry.dispose()
}
// Load the blocks atlas texture
const textureLoader = new THREE.TextureLoader()
const texture = await new Promise<THREE.Texture>((resolve, reject) => {
textureLoader.load(
globalTexture,
resolve,
undefined,
reject
)
})
// Configure texture for pixel art
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.needsUpdate = true
texture.generateMipmaps = false
texture.flipY = false
// Create box with texture
const geometry = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
premultipliedAlpha: false,
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// Set the texture on our shared material
sharedMaterial.map = texture
sharedMaterial.needsUpdate = true
console.log('Texture loaded:', texture.image.width, 'x', texture.image.height)
// Calculate UV coordinates for the first tile (top-left, 16x16)
const atlasWidth = texture.image.width
const atlasHeight = texture.image.height
const tileSize = 16
const textureInfo = {
u: 0 / atlasWidth, // Left edge (first column)
v: 2 * tileSize / atlasHeight, // Top edge (first row)
su: tileSize / atlasWidth, // Width of one tile
sv: tileSize / atlasHeight // Height of one tile
}
console.log('Atlas size:', atlasWidth, 'x', atlasHeight)
console.log('Calculated texture info:', textureInfo)
// Create custom geometry with proper UV mapping
const geometry = createCustomGeometry(textureInfo)
// Create instanced mesh using shared material
currentInstancedMesh = new THREE.InstancedMesh(geometry, sharedMaterial, 1)
const matrix = new THREE.Matrix4()
matrix.setPosition(0.5, 0.5, 0.5) // Offset by +0.5 on each axis
currentInstancedMesh.setMatrixAt(0, matrix)
currentInstancedMesh.count = 1
currentInstancedMesh.instanceMatrix.needsUpdate = true
scene.add(currentInstancedMesh)
// Reference non-instanced cube using same material
currentRefCube = new THREE.Mesh(geometry, sharedMaterial)
currentRefCube.position.set(2.5, 0.5, 0.5) // Offset by +0.5 on each axis
scene.add(currentRefCube)
console.log('Instanced block created successfully')
} catch (error) {
console.error('Error creating instanced block:', error)
// Fallback: create a colored cube
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshLambertMaterial({ color: 0xff0000 })
currentRefCube = new THREE.Mesh(geometry, material)
scene.add(currentRefCube)
console.log('Created fallback colored cube')
}
}
// Create the textured box
createTexturedBox()
// Create the instanced block
createInstancedBlock()
// Animation loop
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
// Simple render loop (no animation)
function render() {
renderer.render(scene, camera)
}
animate()
// Add mouse controls for better viewing
let mouseDown = false
let mouseX = 0
let mouseY = 0
renderer.domElement.addEventListener('mousedown', (event) => {
mouseDown = true
mouseX = event.clientX
mouseY = event.clientY
})
renderer.domElement.addEventListener('mousemove', (event) => {
if (!mouseDown) return
const deltaX = event.clientX - mouseX
const deltaY = event.clientY - mouseY
// Rotate camera around the cube
const spherical = new THREE.Spherical()
spherical.setFromVector3(camera.position)
spherical.theta -= deltaX * 0.01
spherical.phi += deltaY * 0.01
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
camera.position.setFromSpherical(spherical)
camera.lookAt(0, 0, 0)
mouseX = event.clientX
mouseY = event.clientY
render()
})
renderer.domElement.addEventListener('mouseup', () => {
mouseDown = false
})
// Add button to recreate blocks (for testing)
const button = document.createElement('button')
button.textContent = 'Recreate Blocks'
button.style.position = 'fixed'
button.style.top = '10px'
button.style.left = '10px'
button.addEventListener('click', () => {
createInstancedBlock()
render()
})
document.body.appendChild(button)
// Initial render
render()

View file

@ -65,6 +65,7 @@ export const defaultWorldRendererConfig = {
dynamicColorModeDistance: 1, // chunks beyond this distance use color mode only
instancedOnlyDistance: 6, // chunks beyond this distance use instancing only
enableSingleColorMode: false, // ultra-performance mode with solid colors
autoLowerRenderDistance: false,
}
export type WorldRendererConfig = typeof defaultWorldRendererConfig

View file

@ -1,5 +1,14 @@
import PrismarineChatLoader from 'prismarine-chat'
import * as THREE from 'three'
import * as nbt from 'prismarine-nbt'
import { Vec3 } from 'vec3'
import { MesherGeometryOutput } from '../lib/mesher/shared'
import { chunkPos } from '../lib/simpleUtils'
import { renderSign } from '../sign-renderer'
import { getMesh } from './entity/EntityMesh'
import type { WorldRendererThree } from './worldrendererThree'
import { armorModel } from './entity/armorModels'
import { disposeObject } from './threeJsUtils'
export interface ChunkMeshPool {
mesh: THREE.Mesh
@ -8,12 +17,25 @@ export interface ChunkMeshPool {
sectionKey?: string
}
export interface SectionObject extends THREE.Group {
mesh?: THREE.Mesh<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
@ -22,14 +44,30 @@ export class ChunkMeshManager {
// Debug flag to bypass pooling
public bypassPooling = false
// Performance monitoring
private readonly renderTimes: number[] = []
private readonly maxRenderTimeSamples = 30
private _performanceOverrideDistance?: number
private lastPerformanceCheck = 0
private readonly performanceCheckInterval = 2000 // Check every 2 seconds
get performanceOverrideDistance () {
return this._performanceOverrideDistance ?? 0
}
set performanceOverrideDistance (value: number | undefined) {
this._performanceOverrideDistance = value
this.updateSectionsVisibility()
}
constructor (
public worldRenderer: WorldRendererThree,
public scene: THREE.Scene,
public material: THREE.Material,
public worldHeight: number,
viewDistance = 3,
) {
this.updateViewDistance(viewDistance)
console.log(`ChunkMeshManager: Initializing with pool size ${this.poolSize} (min: ${this.minPoolSize}, max: ${this.maxPoolSize})`)
this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer)
this.initializePool()
}
@ -57,13 +95,16 @@ export class ChunkMeshManager {
/**
* Update or create a section with new geometry data
*/
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): THREE.Mesh | null {
// Normal pooling mode
// Check if section already exists
let poolEntry = this.activeSections.get(sectionKey)
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
// Remove existing section object from scene if it exists
let sectionObject = this.sectionObjects[sectionKey]
if (sectionObject) {
this.cleanupSection(sectionKey)
}
// Get or create mesh from pool
let poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
// Get mesh from pool
poolEntry = this.acquireMesh()
if (!poolEntry) {
console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
@ -75,23 +116,22 @@ export class ChunkMeshManager {
}
const { mesh } = poolEntry
const { geometry } = mesh
// Update geometry attributes efficiently
this.updateGeometryAttribute(geometry, 'position', geometryData.positions, 3)
this.updateGeometryAttribute(geometry, 'normal', geometryData.normals, 3)
this.updateGeometryAttribute(geometry, 'color', geometryData.colors, 3)
this.updateGeometryAttribute(geometry, 'uv', geometryData.uvs, 2)
this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
// Use direct index assignment for better performance (like before)
geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
// Set bounding box and sphere for the 16x16x16 section
geometry.boundingBox = new THREE.Box3(
mesh.geometry.boundingBox = new THREE.Box3(
new THREE.Vector3(-8, -8, -8),
new THREE.Vector3(8, 8, 8)
)
geometry.boundingSphere = new THREE.Sphere(
mesh.geometry.boundingSphere = new THREE.Sphere(
new THREE.Vector3(0, 0, 0),
Math.sqrt(3 * 8 ** 2)
)
@ -100,20 +140,81 @@ export class ChunkMeshManager {
mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
mesh.updateMatrix()
mesh.visible = true
// Store metadata
;(mesh as any).tilesCount = geometryData.positions.length / 3 / 4
;(mesh as any).blocksCount = geometryData.blocksCount
mesh.name = 'mesh'
poolEntry.lastUsedTime = performance.now()
return mesh
// Create or update the section object container
sectionObject = new THREE.Group() as SectionObject
sectionObject.add(mesh)
sectionObject.mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
// Store metadata
sectionObject.tilesCount = geometryData.positions.length / 3 / 4
sectionObject.blocksCount = geometryData.blocksCount
// Add signs container
if (Object.keys(geometryData.signs).length > 0) {
const signsContainer = new THREE.Group()
signsContainer.name = 'signs'
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometryData.signs)) {
const signBlockEntity = this.worldRenderer.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
const sign = this.signHeadsRenderer.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
if (!sign) continue
signsContainer.add(sign)
}
sectionObject.add(signsContainer)
sectionObject.signsContainer = signsContainer
}
// Add heads container
if (Object.keys(geometryData.heads).length > 0) {
const headsContainer = new THREE.Group()
headsContainer.name = 'heads'
for (const [posKey, { isWall, rotation }] of Object.entries(geometryData.heads)) {
const headBlockEntity = this.worldRenderer.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
const head = this.signHeadsRenderer.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
if (!head) continue
headsContainer.add(head)
}
sectionObject.add(headsContainer)
sectionObject.headsContainer = headsContainer
}
// Store and add to scene
this.sectionObjects[sectionKey] = sectionObject
this.scene.add(sectionObject)
sectionObject.matrixAutoUpdate = false
return sectionObject
}
cleanupSection (sectionKey: string) {
// Remove section object from scene
const sectionObject = this.sectionObjects[sectionKey]
if (sectionObject) {
this.scene.remove(sectionObject)
// Dispose signs and heads containers
if (sectionObject.signsContainer) {
this.disposeContainer(sectionObject.signsContainer)
}
if (sectionObject.headsContainer) {
this.disposeContainer(sectionObject.headsContainer)
}
delete this.sectionObjects[sectionKey]
}
}
/**
* Release a section and return its mesh to the pool
*/
releaseSection (sectionKey: string): boolean {
this.cleanupSection(sectionKey)
const poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
return false
@ -130,9 +231,43 @@ export class ChunkMeshManager {
this.activeSections.delete(sectionKey)
// Memory cleanup: if pool exceeds max size and we have free meshes, remove one
this.cleanupExcessMeshes()
return true
}
/**
* Get section object if it exists
*/
getSectionObject (sectionKey: string): SectionObject | undefined {
return this.sectionObjects[sectionKey]
}
/**
* Update box helper for a section
*/
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
const sectionObject = this.sectionObjects[sectionKey]
if (!sectionObject?.mesh) return
if (showChunkBorders) {
if (!sectionObject.boxHelper) {
// mesh with static dimensions: 16x16x16
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
staticChunkMesh.position.copy(sectionObject.mesh.position)
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
boxHelper.name = 'helper'
sectionObject.add(boxHelper)
sectionObject.name = 'chunk'
sectionObject.boxHelper = boxHelper
}
sectionObject.boxHelper.visible = true
} else if (sectionObject.boxHelper) {
sectionObject.boxHelper.visible = false
}
}
/**
* Get mesh for section if it exists
*/
@ -174,6 +309,7 @@ export class ChunkMeshManager {
getStats () {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
const memoryUsage = this.getEstimatedMemoryUsage()
return {
poolSize: this.poolSize,
@ -181,7 +317,87 @@ export class ChunkMeshManager {
freeCount,
hitRate: `${hitRate}%`,
hits: this.hits,
misses: this.misses
misses: this.misses,
memoryUsage
}
}
/**
* Get total tiles rendered
*/
getTotalTiles (): number {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.tilesCount || 0), 0)
}
/**
* Get total blocks rendered
*/
getTotalBlocks (): number {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.blocksCount || 0), 0)
}
/**
* Estimate memory usage in MB
*/
getEstimatedMemoryUsage (): { total: string, breakdown: any } {
let totalBytes = 0
let positionBytes = 0
let normalBytes = 0
let colorBytes = 0
let uvBytes = 0
let indexBytes = 0
for (const poolEntry of this.meshPool) {
if (poolEntry.inUse && poolEntry.mesh.geometry) {
const { geometry } = poolEntry.mesh
const position = geometry.getAttribute('position')
if (position) {
const bytes = position.array.byteLength
positionBytes += bytes
totalBytes += bytes
}
const normal = geometry.getAttribute('normal')
if (normal) {
const bytes = normal.array.byteLength
normalBytes += bytes
totalBytes += bytes
}
const color = geometry.getAttribute('color')
if (color) {
const bytes = color.array.byteLength
colorBytes += bytes
totalBytes += bytes
}
const uv = geometry.getAttribute('uv')
if (uv) {
const bytes = uv.array.byteLength
uvBytes += bytes
totalBytes += bytes
}
if (geometry.index) {
const bytes = geometry.index.array.byteLength
indexBytes += bytes
totalBytes += bytes
}
}
}
const totalMB = (totalBytes / (1024 * 1024)).toFixed(2)
return {
total: `${totalMB} MB`,
breakdown: {
position: `${(positionBytes / (1024 * 1024)).toFixed(2)} MB`,
normal: `${(normalBytes / (1024 * 1024)).toFixed(2)} MB`,
color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
}
}
}
@ -295,4 +511,229 @@ export class ChunkMeshManager {
geometry.index.needsUpdate = true
}
}
private cleanupExcessMeshes () {
// If pool size exceeds max and we have free meshes, remove some
if (this.poolSize > this.maxPoolSize) {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
if (freeCount > 0) {
const excessCount = Math.min(this.poolSize - this.maxPoolSize, freeCount)
for (let i = 0; i < excessCount; i++) {
const freeIndex = this.meshPool.findIndex(entry => !entry.inUse)
if (freeIndex !== -1) {
const poolEntry = this.meshPool[freeIndex]
poolEntry.mesh.geometry.dispose()
this.meshPool.splice(freeIndex, 1)
this.poolSize--
}
}
// console.log(`ChunkMeshManager: Cleaned up ${excessCount} excess meshes. Pool size: ${this.poolSize}/${this.maxPoolSize}`)
}
}
}
private disposeContainer (container: THREE.Group) {
disposeObject(container, true)
}
/**
* Record render time for performance monitoring
*/
recordRenderTime (renderTime: number): void {
this.renderTimes.push(renderTime)
if (this.renderTimes.length > this.maxRenderTimeSamples) {
this.renderTimes.shift()
}
// Check performance periodically
const now = performance.now()
if (now - this.lastPerformanceCheck > this.performanceCheckInterval) {
this.checkPerformance()
this.lastPerformanceCheck = now
}
}
/**
* Get current effective render distance
*/
getEffectiveRenderDistance (): number {
return this.performanceOverrideDistance || this.worldRenderer.viewDistance
}
/**
* Force reset performance override
*/
resetPerformanceOverride (): void {
this.performanceOverrideDistance = undefined
this.renderTimes.length = 0
console.log('ChunkMeshManager: Performance override reset')
}
/**
* Get average render time
*/
getAverageRenderTime (): number {
if (this.renderTimes.length === 0) return 0
return this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length
}
/**
* Check if performance is degraded and adjust render distance
*/
private checkPerformance (): void {
if (this.renderTimes.length < this.maxRenderTimeSamples) return
const avgRenderTime = this.getAverageRenderTime()
const targetRenderTime = 16.67 // 60 FPS target (16.67ms per frame)
const performanceThreshold = targetRenderTime * 1.5 // 25ms threshold
if (avgRenderTime > performanceThreshold) {
// Performance is bad, reduce render distance
const currentViewDistance = this.worldRenderer.viewDistance
const newDistance = Math.max(1, Math.floor(currentViewDistance * 0.8))
if (!this.performanceOverrideDistance || newDistance < this.performanceOverrideDistance) {
this.performanceOverrideDistance = newDistance
console.warn(`ChunkMeshManager: Performance degraded (${avgRenderTime.toFixed(2)}ms avg). Reducing effective render distance to ${newDistance}`)
}
} else if (this.performanceOverrideDistance && avgRenderTime < targetRenderTime * 1.1) {
// Performance is good, gradually restore render distance
const currentViewDistance = this.worldRenderer.viewDistance
const newDistance = Math.min(currentViewDistance, this.performanceOverrideDistance + 1)
if (newDistance !== this.performanceOverrideDistance) {
this.performanceOverrideDistance = newDistance >= currentViewDistance ? undefined : newDistance
console.log(`ChunkMeshManager: Performance improved. Restoring render distance to ${newDistance}`)
}
}
}
/**
* Hide sections beyond performance override distance
*/
updateSectionsVisibility (): void {
const cameraPos = this.worldRenderer.cameraSectionPos
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
if (!this.performanceOverrideDistance) {
sectionObject.visible = true
continue
}
const [x, y, z] = sectionKey.split(',').map(Number)
const sectionPos = { x: x / 16, y: y / 16, z: z / 16 }
// Calculate distance using hypot (same as render distance calculation)
const dx = sectionPos.x - cameraPos.x
const dz = sectionPos.z - cameraPos.z
const distance = Math.floor(Math.hypot(dx, dz))
sectionObject.visible = distance <= this.performanceOverrideDistance
}
}
}
class SignHeadsRenderer {
chunkTextures = new Map<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)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
mesh.renderOrder = 999
const lineHeight = 7 / 16
const scaleFactor = isHanging ? 1.3 : 1
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
const thickness = (isHanging ? 2 : 1.5) / 16
const wallSpacing = 0.25 / 16
if (isWall && !isHanging) {
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
} else {
mesh.position.set(0, 0, thickness / 2 + 0.0001)
}
const group = new THREE.Group()
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.add(mesh)
const height = (isHanging ? 10 : 8) / 16
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
const textPosition = height / 2 + heightOffset
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
return group
}
getSignTexture (position: Vec3, blockEntity, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version)
const canvas = renderSign(blockEntity, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
}

View file

@ -1,11 +1,8 @@
import * as THREE from 'three'
import { Vec3 } from 'vec3'
import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import * as tweenJs from '@tweenjs/tween.js'
import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils'
import { sectionPos } from '../lib/simpleUtils'
import { WorldRendererCommon } from '../lib/worldrendererCommon'
import { WorldDataEmitterWorker } from '../lib/worldDataEmitter'
import { addNewStat } from '../lib/ui/newStats'
@ -15,8 +12,6 @@ import { getMyHand } from '../lib/hand'
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins'
import HoldingBlock from './holdingBlock'
import { getMesh } from './entity/EntityMesh'
import { armorModel } from './entity/armorModels'
import { disposeObject } from './threeJsUtils'
import { CursorBlock } from './world/cursorBlock'
import { getItemUv } from './appShared'
@ -32,7 +27,6 @@ type SectionKey = string
export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const
sectionObjects: Record<string, THREE.Object3D & { foutain?: boolean, boxHelper?: THREE.BoxHelper, mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial> }> = {}
sectionInstancingMode: Record<string, InstancingMode> = {}
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
signsCache = new Map<string, any>()
@ -85,11 +79,11 @@ export class WorldRendererThree extends WorldRendererCommon {
private readonly worldOffset = new THREE.Vector3()
get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
return this.chunkMeshManager.getTotalTiles()
}
get blocksRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
return this.chunkMeshManager.getTotalBlocks()
}
constructor (public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
@ -109,7 +103,7 @@ export class WorldRendererThree extends WorldRendererCommon {
this.cameraShake = new CameraShake(this, this.onRender)
this.media = new ThreeJsMedia(this)
this.instancedRenderer = new InstancedRenderer(this)
this.chunkMeshManager = new ChunkMeshManager(this.material, this.worldSizeParams.worldHeight, this.viewDistance)
this.chunkMeshManager = new ChunkMeshManager(this, this.realScene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
// Enable bypass pooling for debugging if URL param is present
if (new URLSearchParams(location.search).get('bypassMeshPooling') === 'true') {
@ -245,6 +239,12 @@ export class WorldRendererThree extends WorldRendererCommon {
this.onReactiveConfigUpdated('showChunkBorders', (value) => {
this.updateShowChunksBorder()
})
this.onReactiveConfigUpdated('enableDebugOverlay', (value) => {
if (!value) {
// restore visibility
this.chunkMeshManager.updateSectionsVisibility()
}
})
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
@ -384,8 +384,8 @@ export class WorldRendererThree extends WorldRendererCommon {
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
// sum of distances: x + y + z
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
const section = this.sectionObjects[key].mesh!
section.renderOrder = 500 - chunkDistance
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
sectionObject.renderOrder = 500 - chunkDistance
}
override updateViewerPosition (pos: Vec3): void {
@ -395,8 +395,8 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraSectionPositionUpdate () {
// eslint-disable-next-line guard-for-in
for (const key in this.sectionInstancingMode) {
const object = this.sectionObjects[key]
if (object) {
const sectionObject = this.chunkMeshManager.getSectionObject(key)!
if (sectionObject) {
this.updatePosDataChunk(key)
}
@ -420,7 +420,10 @@ export class WorldRendererThree extends WorldRendererCommon {
finishChunk (chunkKey: string) {
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
this.sectionObjects[sectionKey].visible = true
const sectionObject = this.chunkMeshManager.getSectionObject(sectionKey)
if (sectionObject) {
sectionObject.visible = true
}
}
delete this.waitingChunksToDisplay[chunkKey]
}
@ -447,39 +450,19 @@ export class WorldRendererThree extends WorldRendererCommon {
return
}
// remvoe object from scene
let object = this.sectionObjects[data.key]
if (object) {
this.scene.remove(object)
// disposeObject(object)
delete this.sectionObjects[data.key]
}
// Use ChunkMeshManager for optimized mesh handling
const mesh = this.chunkMeshManager.updateSection(data.key, data.geometry)
const sectionObject = this.chunkMeshManager.updateSection(data.key, data.geometry)
if (!mesh) {
console.warn(`Failed to get mesh for section ${data.key}`)
if (!sectionObject) {
return
}
// Create or update the section object container
object = new THREE.Group()
this.sectionObjects[data.key] = object
this.scene.add(object)
// Add the pooled mesh to the container
object.add(mesh)
this.sectionObjects[data.key].mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
// Handle signs and heads (these are added to the container, not the pooled mesh)
this.addSignsAndHeads(object as THREE.Group, data.geometry)
this.updateBoxHelper(data.key)
// Handle chunk-based rendering
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false
sectionObject.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
this.waitingChunksToDisplay[chunkKey] ??= []
this.waitingChunksToDisplay[chunkKey].push(data.key)
@ -489,62 +472,6 @@ export class WorldRendererThree extends WorldRendererCommon {
}
this.updatePosDataChunk(data.key)
object.matrixAutoUpdate = false
}
private addSignsAndHeads (object: THREE.Group, geometry: MesherGeometryOutput) {
// Clear existing signs and heads
// const childrenToRemove = object.children.filter(child => child.name !== 'mesh' && child.name !== 'helper')
// for (const child of childrenToRemove) {
// object.remove(child)
// disposeObject(child)
// }
// Add signs
if (Object.keys(geometry.signs).length) {
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometry.signs)) {
const signBlockEntity = this.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
if (!sign) continue
object.add(sign)
}
}
// Add heads
if (Object.keys(geometry.heads).length) {
for (const [posKey, { isWall, rotation }] of Object.entries(geometry.heads)) {
const headBlockEntity = this.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
const head = this.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
if (!head) continue
object.add(head)
}
}
}
getSignTexture (position: Vec3, blockEntity, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.version)
const canvas = renderSign(blockEntity, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
getCameraPosition () {
@ -629,7 +556,7 @@ export class WorldRendererThree extends WorldRendererCommon {
raycaster.far = distance // Limit raycast distance
// Filter to only nearby chunks for performance
const nearbyChunks = Object.values(this.sectionObjects)
const nearbyChunks = Object.values(this.chunkMeshManager.sectionObjects)
.filter(obj => obj.name === 'chunk' && obj.visible)
.filter(obj => {
// Get the mesh child which has the actual geometry
@ -787,7 +714,7 @@ export class WorldRendererThree extends WorldRendererCommon {
chunksRenderBelowOverride !== undefined ||
chunksRenderDistanceOverride !== undefined
) {
for (const [key, object] of Object.entries(this.sectionObjects)) {
for (const [key, object] of Object.entries(this.chunkMeshManager.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
const isVisible =
// eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
@ -799,10 +726,6 @@ export class WorldRendererThree extends WorldRendererCommon {
object.visible = isVisible
}
} else {
for (const object of Object.values(this.sectionObjects)) {
object.visible = true
}
}
}
@ -842,9 +765,10 @@ export class WorldRendererThree extends WorldRendererCommon {
}
for (const fountain of this.fountains) {
if (this.sectionObjects[fountain.sectionId] && !this.sectionObjects[fountain.sectionId].foutain) {
fountain.createParticles(this.sectionObjects[fountain.sectionId])
this.sectionObjects[fountain.sectionId].foutain = true
const sectionObject = this.chunkMeshManager.getSectionObject(fountain.sectionId)
if (sectionObject && !sectionObject.fountain) {
fountain.createParticles(sectionObject)
sectionObject.fountain = true
}
fountain.render()
}
@ -854,87 +778,18 @@ export class WorldRendererThree extends WorldRendererCommon {
}
const end = performance.now()
const totalTime = end - start
if (this.worldRendererConfig.autoLowerRenderDistance) {
// Record render time for performance monitoring
this.chunkMeshManager.recordRenderTime(totalTime)
}
this.renderTimeAvgCount++
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
this.currentRenderedFrames++
}
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
const textures = blockEntity.SkullOwner?.Properties?.textures[0]
if (!textures) return
try {
const textureData = JSON.parse(Buffer.from(textures.Value, 'base64').toString())
let skinUrl = textureData.textures?.SKIN?.url
const { skinTexturesProxy } = this.worldRendererConfig
if (skinTexturesProxy) {
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
.replace('https://textures.minecraft.net/', skinTexturesProxy)
}
const mesh = getMesh(this, skinUrl, armorModel.head)
const group = new THREE.Group()
if (isWall) {
mesh.position.set(0, 0.3125, 0.3125)
}
// move head model down as armor have a different offset than blocks
mesh.position.y -= 23 / 16
group.add(mesh)
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.scale.set(0.8, 0.8, 0.8)
return group
} catch (err) {
console.error('Error decoding player texture:', err)
}
}
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
const tex = this.getSignTexture(position, blockEntity)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
mesh.renderOrder = 999
const lineHeight = 7 / 16
const scaleFactor = isHanging ? 1.3 : 1
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
const thickness = (isHanging ? 2 : 1.5) / 16
const wallSpacing = 0.25 / 16
if (isWall && !isHanging) {
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
} else {
mesh.position.set(0, 0, thickness / 2 + 0.0001)
}
const group = new THREE.Group()
group.rotation.set(
0,
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
0
)
group.add(mesh)
const height = (isHanging ? 10 : 8) / 16
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
const textPosition = height / 2 + heightOffset
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
return group
}
lightUpdate (chunkX: number, chunkZ: number) {
// set all sections in the chunk dirty
@ -944,45 +799,27 @@ export class WorldRendererThree extends WorldRendererCommon {
}
rerenderAllChunks () { // todo not clear what to do with loading chunks
for (const key of Object.keys(this.sectionObjects)) {
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
const [x, y, z] = key.split(',').map(Number)
this.setSectionDirty(new Vec3(x, y, z))
}
}
updateShowChunksBorder () {
for (const key of Object.keys(this.sectionObjects)) {
for (const key of Object.keys(this.chunkMeshManager.sectionObjects)) {
this.updateBoxHelper(key)
}
}
updateBoxHelper (key: string) {
const { showChunkBorders } = this.worldRendererConfig
const section = this.sectionObjects[key]
if (!section) return
if (showChunkBorders) {
if (!section.boxHelper) {
// mesh with static dimensions: 16x16x16
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), this.chunkBoxMaterial)
staticChunkMesh.position.set(section.mesh!.position.x, section.mesh!.position.y, section.mesh!.position.z)
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
boxHelper.name = 'helper'
// boxHelper.geometry.boundingSphere = section.mesh!.geometry.boundingSphere
section.add(boxHelper)
section.name = 'chunk'
section.boxHelper = boxHelper
}
section.boxHelper.visible = true
} else if (section.boxHelper) {
section.boxHelper.visible = false
}
this.chunkMeshManager.updateBoxHelper(key, showChunkBorders, this.chunkBoxMaterial)
}
resetWorld () {
super.resetWorld()
for (const mesh of Object.values(this.sectionObjects)) {
for (const mesh of Object.values(this.chunkMeshManager.sectionObjects)) {
this.scene.remove(mesh)
}
@ -999,7 +836,7 @@ export class WorldRendererThree extends WorldRendererCommon {
getLoadedChunksRelative (pos: Vec3, includeY = false) {
const [currentX, currentY, currentZ] = sectionPos(pos)
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
return Object.fromEntries(Object.entries(this.chunkMeshManager.sectionObjects).map(([key, o]) => {
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
@ -1016,12 +853,13 @@ export class WorldRendererThree extends WorldRendererCommon {
}
readdChunks () {
for (const key of Object.keys(this.sectionObjects)) {
this.scene.remove(this.sectionObjects[key])
const { sectionObjects } = this.chunkMeshManager
for (const key of Object.keys(sectionObjects)) {
this.scene.remove(sectionObjects[key])
}
setTimeout(() => {
for (const key of Object.keys(this.sectionObjects)) {
this.scene.add(this.sectionObjects[key])
for (const key of Object.keys(sectionObjects)) {
this.scene.add(sectionObjects[key])
}
}, 500)
}
@ -1049,15 +887,8 @@ export class WorldRendererThree extends WorldRendererCommon {
// Remove instanced blocks for this section
this.instancedRenderer?.removeSectionInstances(key)
// Release section from mesh pool
// Release section from mesh pool (this will also remove from scene)
this.chunkMeshManager.releaseSection(key)
const object = this.sectionObjects[key]
if (object) {
this.scene.remove(object)
disposeObject(object)
}
delete this.sectionObjects[key]
}
}
@ -1123,7 +954,7 @@ export class WorldRendererThree extends WorldRendererCommon {
const chunkKey = `${chunkX},${chunkZ}`
const sectionKey = `${chunkX},${sectionY},${chunkZ}`
return !!this.finishedChunks[chunkKey] || !!this.sectionObjects[sectionKey]
return !!this.finishedChunks[chunkKey] || !!this.chunkMeshManager.sectionObjects[sectionKey]
}
updateSectionOffsets () {
@ -1161,7 +992,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
// Apply the offset to the section object
const section = this.sectionObjects[key]
const section = this.chunkMeshManager.sectionObjects[key]
if (section) {
section.position.set(
anim.currentOffsetX,

View file

@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
lockUrl()
}
if (command === 'communication.toggleMicrophone') {
toggleMicrophoneMuted?.()
if (typeof toggleMicrophoneMuted === 'function') {
toggleMicrophoneMuted()
}
}
}

View file

@ -44,6 +44,7 @@ export const defaultOptions = {
useVersionsTextures: 'latest',
// Instanced rendering options
useInstancedRendering: false,
autoLowerRenderDistance: false,
forceInstancedOnly: false,
instancedOnlyDistance: 6,
enableSingleColorMode: false,

View file

@ -116,6 +116,10 @@ export const watchOptionsAfterViewerInit = () => {
// appViewer.inWorldRenderingConfig.neighborChunkUpdates = o.neighborChunkUpdates
})
watchValue(options, o => {
appViewer.inWorldRenderingConfig.autoLowerRenderDistance = o.autoLowerRenderDistance
})
// Instanced rendering options
watchValue(options, o => {
appViewer.inWorldRenderingConfig.useInstancedRendering = o.useInstancedRendering