finish manager!
This commit is contained in:
parent
9ee28ef62f
commit
2f49cbb35b
7 changed files with 699 additions and 279 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -504,7 +504,9 @@ const alwaysPressedHandledCommand = (command: Command) => {
|
|||
lockUrl()
|
||||
}
|
||||
if (command === 'communication.toggleMicrophone') {
|
||||
toggleMicrophoneMuted?.()
|
||||
if (typeof toggleMicrophoneMuted === 'function') {
|
||||
toggleMicrophoneMuted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const defaultOptions = {
|
|||
useVersionsTextures: 'latest',
|
||||
// Instanced rendering options
|
||||
useInstancedRendering: false,
|
||||
autoLowerRenderDistance: false,
|
||||
forceInstancedOnly: false,
|
||||
instancedOnlyDistance: 6,
|
||||
enableSingleColorMode: false,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue