minecraft-renderer 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mesher.js +64 -64
- package/dist/mesher.js.map +3 -3
- package/dist/mesherWasm.js +1 -1
- package/dist/minecraft-renderer.js +55 -55
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +363 -363
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +6 -1
- package/src/graphicsBackend/playerState.ts +1 -0
- package/src/lib/worldrendererCommon.ts +8 -0
- package/src/mesher/models.ts +15 -6
- package/src/mesher/shared.ts +3 -1
- package/src/playerState/playerState.ts +2 -0
- package/src/playground/scenes/main.ts +1 -1
- package/src/three/chunkMeshManager.ts +808 -0
- package/src/three/entities.ts +42 -36
- package/src/three/graphicsBackendBase.ts +1 -1
- package/src/three/modules/sciFiWorldReveal.ts +10 -21
- package/src/three/panorama.ts +1 -1
- package/src/three/worldRendererThree.ts +65 -40
- package/src/three/worldBlockGeometry.ts +0 -355
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import PrismarineChatLoader from 'prismarine-chat'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import * as nbt from 'prismarine-nbt'
|
|
5
|
+
import { Vec3 } from 'vec3'
|
|
6
|
+
import { MesherGeometryOutput } from '../mesher/shared'
|
|
7
|
+
import { chunkPos } from '../lib/simpleUtils'
|
|
8
|
+
import { renderSign } from '../sign-renderer'
|
|
9
|
+
import { getMesh } from './entity/EntityMesh'
|
|
10
|
+
import type { WorldRendererThree } from './worldRendererThree'
|
|
11
|
+
import { armorModel } from './entity/armorModels'
|
|
12
|
+
import { disposeObject } from './threeJsUtils'
|
|
13
|
+
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
|
|
14
|
+
|
|
15
|
+
export interface ChunkMeshPool {
|
|
16
|
+
mesh: THREE.Mesh
|
|
17
|
+
inUse: boolean
|
|
18
|
+
lastUsedTime: number
|
|
19
|
+
sectionKey?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SectionObject extends THREE.Group {
|
|
23
|
+
mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
|
|
24
|
+
tilesCount?: number
|
|
25
|
+
blocksCount?: number
|
|
26
|
+
|
|
27
|
+
signsContainer?: THREE.Group
|
|
28
|
+
headsContainer?: THREE.Group
|
|
29
|
+
bannersContainer?: THREE.Group
|
|
30
|
+
boxHelper?: THREE.BoxHelper
|
|
31
|
+
foutain?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ChunkMeshManager {
|
|
35
|
+
private readonly meshPool: ChunkMeshPool[] = []
|
|
36
|
+
private readonly activeSections = new Map<string, ChunkMeshPool>()
|
|
37
|
+
readonly sectionObjects: Record<string, SectionObject> = {}
|
|
38
|
+
private poolSize!: number
|
|
39
|
+
private maxPoolSize!: number
|
|
40
|
+
private minPoolSize!: number
|
|
41
|
+
private readonly signHeadsRenderer: SignHeadsRenderer
|
|
42
|
+
|
|
43
|
+
// Performance tracking
|
|
44
|
+
private hits = 0
|
|
45
|
+
private misses = 0
|
|
46
|
+
|
|
47
|
+
// Debug flag to bypass pooling
|
|
48
|
+
public bypassPooling = false
|
|
49
|
+
|
|
50
|
+
// Performance monitoring
|
|
51
|
+
private readonly renderTimes: number[] = []
|
|
52
|
+
private readonly maxRenderTimeSamples = 30
|
|
53
|
+
private _performanceOverrideDistance?: number
|
|
54
|
+
private lastPerformanceCheck = 0
|
|
55
|
+
private readonly performanceCheckInterval = 2000 // Check every 2 seconds
|
|
56
|
+
|
|
57
|
+
get performanceOverrideDistance () {
|
|
58
|
+
return this._performanceOverrideDistance ?? 0
|
|
59
|
+
}
|
|
60
|
+
set performanceOverrideDistance (value: number | undefined) {
|
|
61
|
+
this._performanceOverrideDistance = value
|
|
62
|
+
this.updateSectionsVisibility()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
constructor (
|
|
66
|
+
public worldRenderer: WorldRendererThree,
|
|
67
|
+
public scene: THREE.Group,
|
|
68
|
+
public material: THREE.Material,
|
|
69
|
+
public worldHeight: number,
|
|
70
|
+
viewDistance = 3,
|
|
71
|
+
) {
|
|
72
|
+
this.updateViewDistance(viewDistance)
|
|
73
|
+
this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer)
|
|
74
|
+
|
|
75
|
+
this.initializePool()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private initializePool () {
|
|
79
|
+
// Create initial pool
|
|
80
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
81
|
+
const geometry = new THREE.BufferGeometry()
|
|
82
|
+
const mesh = new THREE.Mesh(geometry, this.material)
|
|
83
|
+
mesh.visible = false
|
|
84
|
+
mesh.matrixAutoUpdate = false
|
|
85
|
+
mesh.name = 'pooled-section-mesh'
|
|
86
|
+
|
|
87
|
+
const poolEntry: ChunkMeshPool = {
|
|
88
|
+
mesh,
|
|
89
|
+
inUse: false,
|
|
90
|
+
lastUsedTime: 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.meshPool.push(poolEntry)
|
|
94
|
+
// Don't add to scene here - meshes will be added to containers
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Update or create a section with new geometry data
|
|
100
|
+
*/
|
|
101
|
+
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
|
|
102
|
+
// Remove existing section object from scene if it exists
|
|
103
|
+
let sectionObject = this.sectionObjects[sectionKey]
|
|
104
|
+
if (sectionObject) {
|
|
105
|
+
this.cleanupSection(sectionKey)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get or create mesh from pool
|
|
109
|
+
let poolEntry = this.activeSections.get(sectionKey)
|
|
110
|
+
if (!poolEntry) {
|
|
111
|
+
poolEntry = this.acquireMesh()
|
|
112
|
+
if (!poolEntry) {
|
|
113
|
+
console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.activeSections.set(sectionKey, poolEntry)
|
|
118
|
+
poolEntry.sectionKey = sectionKey
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { mesh } = poolEntry
|
|
122
|
+
|
|
123
|
+
// Update geometry attributes efficiently
|
|
124
|
+
this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
|
|
125
|
+
this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
|
|
126
|
+
this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
|
|
127
|
+
this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
|
|
128
|
+
|
|
129
|
+
// Use direct index assignment for better performance (like before)
|
|
130
|
+
mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
|
|
131
|
+
|
|
132
|
+
// Set bounding box and sphere for the 16x16x16 section
|
|
133
|
+
mesh.geometry.boundingBox = new THREE.Box3(
|
|
134
|
+
new THREE.Vector3(-8, -8, -8),
|
|
135
|
+
new THREE.Vector3(8, 8, 8)
|
|
136
|
+
)
|
|
137
|
+
mesh.geometry.boundingSphere = new THREE.Sphere(
|
|
138
|
+
new THREE.Vector3(0, 0, 0),
|
|
139
|
+
Math.sqrt(3 * 8 ** 2)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// Position the mesh
|
|
143
|
+
this.worldRenderer.sceneOrigin.track(mesh, { updateMatrix: true })
|
|
144
|
+
mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
|
|
145
|
+
mesh.updateMatrix()
|
|
146
|
+
mesh.visible = true
|
|
147
|
+
mesh.name = 'mesh'
|
|
148
|
+
|
|
149
|
+
poolEntry.lastUsedTime = performance.now()
|
|
150
|
+
|
|
151
|
+
// Create or update the section object container
|
|
152
|
+
sectionObject = new THREE.Group() as SectionObject
|
|
153
|
+
sectionObject.add(mesh)
|
|
154
|
+
sectionObject.mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
|
|
155
|
+
|
|
156
|
+
// Store metadata
|
|
157
|
+
sectionObject.tilesCount = geometryData.positions.length / 3 / 4
|
|
158
|
+
sectionObject.blocksCount = geometryData.blocksCount
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Add signs container
|
|
162
|
+
if (Object.keys(geometryData.signs).length > 0) {
|
|
163
|
+
const signsContainer = new THREE.Group()
|
|
164
|
+
signsContainer.name = 'signs'
|
|
165
|
+
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometryData.signs)) {
|
|
166
|
+
const signBlockEntity = this.worldRenderer.blockEntities[posKey]
|
|
167
|
+
if (!signBlockEntity) continue
|
|
168
|
+
const [x, y, z] = posKey.split(',')
|
|
169
|
+
const sign = this.signHeadsRenderer.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
|
|
170
|
+
if (!sign) continue
|
|
171
|
+
signsContainer.add(sign)
|
|
172
|
+
}
|
|
173
|
+
sectionObject.add(signsContainer)
|
|
174
|
+
sectionObject.signsContainer = signsContainer
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add heads container
|
|
178
|
+
if (Object.keys(geometryData.heads).length > 0) {
|
|
179
|
+
const headsContainer = new THREE.Group()
|
|
180
|
+
headsContainer.name = 'heads'
|
|
181
|
+
for (const [posKey, { isWall, rotation }] of Object.entries(geometryData.heads)) {
|
|
182
|
+
const headBlockEntity = this.worldRenderer.blockEntities[posKey]
|
|
183
|
+
if (!headBlockEntity) continue
|
|
184
|
+
const [x, y, z] = posKey.split(',')
|
|
185
|
+
const head = this.signHeadsRenderer.renderHead(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(headBlockEntity))
|
|
186
|
+
if (!head) continue
|
|
187
|
+
headsContainer.add(head)
|
|
188
|
+
}
|
|
189
|
+
sectionObject.add(headsContainer)
|
|
190
|
+
sectionObject.headsContainer = headsContainer
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add banners container
|
|
194
|
+
if (Object.keys(geometryData.banners).length > 0) {
|
|
195
|
+
const bannersContainer = new THREE.Group()
|
|
196
|
+
bannersContainer.name = 'banners'
|
|
197
|
+
sectionObject.bannersContainer = bannersContainer
|
|
198
|
+
sectionObject.add(bannersContainer)
|
|
199
|
+
for (const [posKey, { isWall, rotation, blockName }] of Object.entries(geometryData.banners)) {
|
|
200
|
+
const bannerBlockEntity = this.worldRenderer.blockEntities[posKey]
|
|
201
|
+
if (!bannerBlockEntity) continue
|
|
202
|
+
const [x, y, z] = posKey.split(',')
|
|
203
|
+
const bannerTexture = getBannerTexture(this.worldRenderer, blockName, nbt.simplify(bannerBlockEntity))
|
|
204
|
+
if (!bannerTexture) continue
|
|
205
|
+
const banner = createBannerMesh(new Vec3(+x, +y, +z), rotation, isWall, bannerTexture)
|
|
206
|
+
const { x: bwx, y: bwy, z: bwz } = banner.position
|
|
207
|
+
this.worldRenderer.sceneOrigin.track(banner)
|
|
208
|
+
banner.position.set(bwx, bwy, bwz)
|
|
209
|
+
bannersContainer.add(banner)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error('ChunkMeshManager: Error adding signs, heads, or banners to section', err)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Store and add to scene
|
|
217
|
+
this.sectionObjects[sectionKey] = sectionObject
|
|
218
|
+
this.scene.add(sectionObject)
|
|
219
|
+
sectionObject.matrixAutoUpdate = false
|
|
220
|
+
|
|
221
|
+
return sectionObject
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
cleanupSection (sectionKey: string) {
|
|
225
|
+
// Remove section object from scene
|
|
226
|
+
const sectionObject = this.sectionObjects[sectionKey]
|
|
227
|
+
if (sectionObject) {
|
|
228
|
+
// Cleanup banner textures before disposing
|
|
229
|
+
if (sectionObject.bannersContainer) {
|
|
230
|
+
sectionObject.bannersContainer.traverse((child) => {
|
|
231
|
+
if ((child as any).bannerTexture) {
|
|
232
|
+
releaseBannerTexture((child as any).bannerTexture)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
this.disposeContainer(sectionObject.bannersContainer)
|
|
236
|
+
}
|
|
237
|
+
// Dispose signs and heads containers
|
|
238
|
+
if (sectionObject.signsContainer) {
|
|
239
|
+
this.disposeContainer(sectionObject.signsContainer)
|
|
240
|
+
}
|
|
241
|
+
if (sectionObject.headsContainer) {
|
|
242
|
+
this.disposeContainer(sectionObject.headsContainer)
|
|
243
|
+
}
|
|
244
|
+
this.worldRenderer.sceneOrigin.removeAndUntrackAll(sectionObject)
|
|
245
|
+
this.scene.remove(sectionObject)
|
|
246
|
+
delete this.sectionObjects[sectionKey]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Release a section and return its mesh to the pool
|
|
252
|
+
*/
|
|
253
|
+
releaseSection (sectionKey: string): boolean {
|
|
254
|
+
this.cleanupSection(sectionKey)
|
|
255
|
+
|
|
256
|
+
const poolEntry = this.activeSections.get(sectionKey)
|
|
257
|
+
if (!poolEntry) {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Hide mesh and mark as available
|
|
262
|
+
poolEntry.mesh.visible = false
|
|
263
|
+
poolEntry.inUse = false
|
|
264
|
+
poolEntry.sectionKey = undefined
|
|
265
|
+
poolEntry.lastUsedTime = 0
|
|
266
|
+
|
|
267
|
+
// Clear geometry to free memory
|
|
268
|
+
this.clearGeometry(poolEntry.mesh.geometry)
|
|
269
|
+
|
|
270
|
+
this.activeSections.delete(sectionKey)
|
|
271
|
+
|
|
272
|
+
// Memory cleanup: if pool exceeds max size and we have free meshes, remove one
|
|
273
|
+
this.cleanupExcessMeshes()
|
|
274
|
+
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get section object if it exists
|
|
280
|
+
*/
|
|
281
|
+
getSectionObject (sectionKey: string): SectionObject | undefined {
|
|
282
|
+
return this.sectionObjects[sectionKey]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Update box helper for a section
|
|
287
|
+
*/
|
|
288
|
+
updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material) {
|
|
289
|
+
const sectionObject = this.sectionObjects[sectionKey]
|
|
290
|
+
if (!sectionObject?.mesh) return
|
|
291
|
+
|
|
292
|
+
if (showChunkBorders) {
|
|
293
|
+
if (!sectionObject.boxHelper) {
|
|
294
|
+
// mesh with static dimensions: 16x16x16
|
|
295
|
+
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), chunkBoxMaterial)
|
|
296
|
+
staticChunkMesh.position.copy(sectionObject.mesh.position)
|
|
297
|
+
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
|
298
|
+
boxHelper.name = 'helper'
|
|
299
|
+
sectionObject.add(boxHelper)
|
|
300
|
+
sectionObject.name = 'chunk'
|
|
301
|
+
sectionObject.boxHelper = boxHelper
|
|
302
|
+
}
|
|
303
|
+
sectionObject.boxHelper.visible = true
|
|
304
|
+
} else if (sectionObject.boxHelper) {
|
|
305
|
+
sectionObject.boxHelper.visible = false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get mesh for section if it exists
|
|
311
|
+
*/
|
|
312
|
+
getSectionMesh (sectionKey: string): THREE.Mesh | undefined {
|
|
313
|
+
return this.activeSections.get(sectionKey)?.mesh
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check if section is managed by this pool
|
|
318
|
+
*/
|
|
319
|
+
hasSection (sectionKey: string): boolean {
|
|
320
|
+
return this.activeSections.has(sectionKey)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Update pool size based on new view distance
|
|
325
|
+
*/
|
|
326
|
+
updateViewDistance (maxViewDistance: number) {
|
|
327
|
+
// Calculate dynamic pool size based on view distance
|
|
328
|
+
const chunksInView = (maxViewDistance * 2 + 1) ** 2
|
|
329
|
+
const maxSectionsPerChunk = this.worldHeight / 16
|
|
330
|
+
const avgSectionsPerChunk = 5
|
|
331
|
+
this.minPoolSize = Math.floor(chunksInView * avgSectionsPerChunk)
|
|
332
|
+
this.maxPoolSize = Math.floor(chunksInView * maxSectionsPerChunk) + 1
|
|
333
|
+
this.poolSize ??= this.minPoolSize
|
|
334
|
+
|
|
335
|
+
// Expand pool if needed to reach optimal size
|
|
336
|
+
if (this.minPoolSize > this.poolSize) {
|
|
337
|
+
const targetSize = Math.min(this.minPoolSize, this.maxPoolSize)
|
|
338
|
+
this.expandPool(targetSize)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(`ChunkMeshManager: Updated view max distance to ${maxViewDistance}, pool: ${this.poolSize}/${this.maxPoolSize}, optimal: ${this.minPoolSize}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get pool statistics
|
|
346
|
+
*/
|
|
347
|
+
getStats () {
|
|
348
|
+
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
|
349
|
+
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
|
|
350
|
+
const memoryUsage = this.getEstimatedMemoryUsage()
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
poolSize: this.poolSize,
|
|
354
|
+
activeCount: this.activeSections.size,
|
|
355
|
+
freeCount,
|
|
356
|
+
hitRate: `${hitRate}%`,
|
|
357
|
+
hits: this.hits,
|
|
358
|
+
misses: this.misses,
|
|
359
|
+
memoryUsage
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get total tiles rendered
|
|
365
|
+
*/
|
|
366
|
+
getTotalTiles (): number {
|
|
367
|
+
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.tilesCount || 0), 0)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get total blocks rendered
|
|
372
|
+
*/
|
|
373
|
+
getTotalBlocks (): number {
|
|
374
|
+
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.blocksCount || 0), 0)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Estimate memory usage in MB
|
|
379
|
+
*/
|
|
380
|
+
getEstimatedMemoryUsage (): { total: string, breakdown: any } {
|
|
381
|
+
let totalBytes = 0
|
|
382
|
+
let positionBytes = 0
|
|
383
|
+
let normalBytes = 0
|
|
384
|
+
let colorBytes = 0
|
|
385
|
+
let uvBytes = 0
|
|
386
|
+
let indexBytes = 0
|
|
387
|
+
|
|
388
|
+
for (const poolEntry of this.meshPool) {
|
|
389
|
+
if (poolEntry.inUse && poolEntry.mesh.geometry) {
|
|
390
|
+
const { geometry } = poolEntry.mesh
|
|
391
|
+
|
|
392
|
+
const position = geometry.getAttribute('position')
|
|
393
|
+
if (position) {
|
|
394
|
+
const bytes = position.array.byteLength
|
|
395
|
+
positionBytes += bytes
|
|
396
|
+
totalBytes += bytes
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const normal = geometry.getAttribute('normal')
|
|
400
|
+
if (normal) {
|
|
401
|
+
const bytes = normal.array.byteLength
|
|
402
|
+
normalBytes += bytes
|
|
403
|
+
totalBytes += bytes
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const color = geometry.getAttribute('color')
|
|
407
|
+
if (color) {
|
|
408
|
+
const bytes = color.array.byteLength
|
|
409
|
+
colorBytes += bytes
|
|
410
|
+
totalBytes += bytes
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const uv = geometry.getAttribute('uv')
|
|
414
|
+
if (uv) {
|
|
415
|
+
const bytes = uv.array.byteLength
|
|
416
|
+
uvBytes += bytes
|
|
417
|
+
totalBytes += bytes
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (geometry.index) {
|
|
421
|
+
const bytes = geometry.index.array.byteLength
|
|
422
|
+
indexBytes += bytes
|
|
423
|
+
totalBytes += bytes
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const totalMB = (totalBytes / (1024 * 1024)).toFixed(2)
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
total: `${totalMB} MB`,
|
|
432
|
+
breakdown: {
|
|
433
|
+
position: `${(positionBytes / (1024 * 1024)).toFixed(2)} MB`,
|
|
434
|
+
normal: `${(normalBytes / (1024 * 1024)).toFixed(2)} MB`,
|
|
435
|
+
color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
|
|
436
|
+
uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
|
|
437
|
+
index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Cleanup and dispose resources
|
|
444
|
+
*/
|
|
445
|
+
dispose () {
|
|
446
|
+
// Release all active sections (snapshot keys to avoid mutating map during iteration)
|
|
447
|
+
const activeKeys = [...this.activeSections.keys()]
|
|
448
|
+
for (const sectionKey of activeKeys) {
|
|
449
|
+
this.releaseSection(sectionKey)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.signHeadsRenderer.dispose()
|
|
453
|
+
|
|
454
|
+
// Dispose all meshes and geometries
|
|
455
|
+
for (const poolEntry of this.meshPool) {
|
|
456
|
+
// Meshes will be removed from scene when their parent containers are removed
|
|
457
|
+
poolEntry.mesh.geometry.dispose()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.meshPool.length = 0
|
|
461
|
+
this.activeSections.clear()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Private helper methods
|
|
465
|
+
|
|
466
|
+
private acquireMesh (): ChunkMeshPool | undefined {
|
|
467
|
+
if (this.bypassPooling) {
|
|
468
|
+
const entry: ChunkMeshPool = {
|
|
469
|
+
mesh: new THREE.Mesh(new THREE.BufferGeometry(), this.material),
|
|
470
|
+
inUse: true,
|
|
471
|
+
lastUsedTime: performance.now()
|
|
472
|
+
}
|
|
473
|
+
this.meshPool.push(entry)
|
|
474
|
+
this.poolSize++
|
|
475
|
+
return entry
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Find first available mesh
|
|
479
|
+
for (const entry of this.meshPool) {
|
|
480
|
+
if (!entry.inUse) {
|
|
481
|
+
entry.inUse = true
|
|
482
|
+
entry.lastUsedTime = performance.now()
|
|
483
|
+
this.hits++
|
|
484
|
+
return entry
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// No free mesh — expand pool
|
|
489
|
+
this.misses++
|
|
490
|
+
let newPoolSize = Math.min(this.poolSize + 16, this.maxPoolSize)
|
|
491
|
+
if (newPoolSize <= this.meshPool.length) {
|
|
492
|
+
// Already at or above max, do emergency expansion
|
|
493
|
+
newPoolSize = this.meshPool.length + 8
|
|
494
|
+
this.maxPoolSize = newPoolSize
|
|
495
|
+
console.warn(`ChunkMeshManager: Pool exhausted (${this.poolSize}/${this.maxPoolSize}). Emergency expansion to ${newPoolSize}`)
|
|
496
|
+
}
|
|
497
|
+
this.expandPool(newPoolSize)
|
|
498
|
+
|
|
499
|
+
// Try again — find the newly added free entry (no recursion)
|
|
500
|
+
for (let i = this.meshPool.length - 1; i >= 0; i--) {
|
|
501
|
+
const entry = this.meshPool[i]
|
|
502
|
+
if (!entry.inUse) {
|
|
503
|
+
entry.inUse = true
|
|
504
|
+
entry.lastUsedTime = performance.now()
|
|
505
|
+
return entry
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Should never happen — expandPool guarantees new entries
|
|
510
|
+
throw new Error('ChunkMeshManager: Failed to acquire mesh after pool expansion')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private expandPool (newSize: number) {
|
|
514
|
+
const currentLength = this.meshPool.length
|
|
515
|
+
this.poolSize = newSize
|
|
516
|
+
|
|
517
|
+
// Add new meshes to pool
|
|
518
|
+
for (let i = currentLength; i < newSize; i++) {
|
|
519
|
+
const geometry = new THREE.BufferGeometry()
|
|
520
|
+
const mesh = new THREE.Mesh(geometry, this.material)
|
|
521
|
+
mesh.visible = false
|
|
522
|
+
mesh.matrixAutoUpdate = false
|
|
523
|
+
mesh.name = 'pooled-section-mesh'
|
|
524
|
+
|
|
525
|
+
const poolEntry: ChunkMeshPool = {
|
|
526
|
+
mesh,
|
|
527
|
+
inUse: false,
|
|
528
|
+
lastUsedTime: 0
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.meshPool.push(poolEntry)
|
|
532
|
+
// Don't add to scene here - meshes will be added to containers
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private updateGeometryAttribute (
|
|
537
|
+
geometry: THREE.BufferGeometry,
|
|
538
|
+
name: string,
|
|
539
|
+
array: Float32Array,
|
|
540
|
+
itemSize: number
|
|
541
|
+
) {
|
|
542
|
+
const attribute = geometry.getAttribute(name)
|
|
543
|
+
|
|
544
|
+
if (attribute && attribute.count === array.length / itemSize) {
|
|
545
|
+
// Reuse existing attribute
|
|
546
|
+
;(attribute.array as Float32Array).set(array)
|
|
547
|
+
attribute.needsUpdate = true
|
|
548
|
+
} else {
|
|
549
|
+
// Create new attribute (this will dispose the old one automatically)
|
|
550
|
+
geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize))
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private clearGeometry (geometry: THREE.BufferGeometry) {
|
|
555
|
+
const attributes = ['position', 'normal', 'color', 'uv']
|
|
556
|
+
for (const name of attributes) {
|
|
557
|
+
if (geometry.hasAttribute(name)) {
|
|
558
|
+
geometry.deleteAttribute(name)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (geometry.index) {
|
|
562
|
+
geometry.setIndex(null)
|
|
563
|
+
}
|
|
564
|
+
geometry.boundingBox = null
|
|
565
|
+
geometry.boundingSphere = null
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private cleanupExcessMeshes () {
|
|
569
|
+
// If pool size exceeds max and we have free meshes, remove some
|
|
570
|
+
if (this.poolSize > this.maxPoolSize) {
|
|
571
|
+
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
|
572
|
+
if (freeCount > 0) {
|
|
573
|
+
const excessCount = Math.min(this.poolSize - this.maxPoolSize, freeCount)
|
|
574
|
+
for (let i = 0; i < excessCount; i++) {
|
|
575
|
+
const freeIndex = this.meshPool.findIndex(entry => !entry.inUse)
|
|
576
|
+
if (freeIndex !== -1) {
|
|
577
|
+
const poolEntry = this.meshPool[freeIndex]
|
|
578
|
+
poolEntry.mesh.geometry.dispose()
|
|
579
|
+
this.meshPool.splice(freeIndex, 1)
|
|
580
|
+
this.poolSize--
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// console.log(`ChunkMeshManager: Cleaned up ${excessCount} excess meshes. Pool size: ${this.poolSize}/${this.maxPoolSize}`)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private disposeContainer (container: THREE.Group) {
|
|
589
|
+
disposeObject(container, true)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Record render time for performance monitoring
|
|
594
|
+
*/
|
|
595
|
+
recordRenderTime (renderTime: number): void {
|
|
596
|
+
this.renderTimes.push(renderTime)
|
|
597
|
+
if (this.renderTimes.length > this.maxRenderTimeSamples) {
|
|
598
|
+
this.renderTimes.shift()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Check performance periodically
|
|
602
|
+
const now = performance.now()
|
|
603
|
+
if (now - this.lastPerformanceCheck > this.performanceCheckInterval) {
|
|
604
|
+
this.checkPerformance()
|
|
605
|
+
this.lastPerformanceCheck = now
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Get current effective render distance
|
|
611
|
+
*/
|
|
612
|
+
getEffectiveRenderDistance (): number {
|
|
613
|
+
return this.performanceOverrideDistance || this.worldRenderer.viewDistance
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Force reset performance override
|
|
618
|
+
*/
|
|
619
|
+
resetPerformanceOverride (): void {
|
|
620
|
+
this.performanceOverrideDistance = undefined
|
|
621
|
+
this.renderTimes.length = 0
|
|
622
|
+
console.log('ChunkMeshManager: Performance override reset')
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Get average render time
|
|
627
|
+
*/
|
|
628
|
+
getAverageRenderTime (): number {
|
|
629
|
+
if (this.renderTimes.length === 0) return 0
|
|
630
|
+
return this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Check if performance is degraded and adjust render distance
|
|
635
|
+
*/
|
|
636
|
+
private checkPerformance (): void {
|
|
637
|
+
if (this.renderTimes.length < this.maxRenderTimeSamples) return
|
|
638
|
+
|
|
639
|
+
const avgRenderTime = this.getAverageRenderTime()
|
|
640
|
+
const targetRenderTime = 16.67 // 60 FPS target (16.67ms per frame)
|
|
641
|
+
const performanceThreshold = targetRenderTime * 1.5 // 25ms threshold
|
|
642
|
+
|
|
643
|
+
if (avgRenderTime > performanceThreshold) {
|
|
644
|
+
// Performance is bad, reduce render distance
|
|
645
|
+
const currentViewDistance = this.worldRenderer.viewDistance
|
|
646
|
+
const newDistance = Math.max(1, Math.floor(currentViewDistance * 0.8))
|
|
647
|
+
|
|
648
|
+
if (!this.performanceOverrideDistance || newDistance < this.performanceOverrideDistance) {
|
|
649
|
+
this.performanceOverrideDistance = newDistance
|
|
650
|
+
console.warn(`ChunkMeshManager: Performance degraded (${avgRenderTime.toFixed(2)}ms avg). Reducing effective render distance to ${newDistance}`)
|
|
651
|
+
}
|
|
652
|
+
} else if (this.performanceOverrideDistance && avgRenderTime < targetRenderTime * 1.1) {
|
|
653
|
+
// Performance is good, gradually restore render distance
|
|
654
|
+
const currentViewDistance = this.worldRenderer.viewDistance
|
|
655
|
+
const newDistance = Math.min(currentViewDistance, this.performanceOverrideDistance + 1)
|
|
656
|
+
|
|
657
|
+
if (newDistance !== this.performanceOverrideDistance) {
|
|
658
|
+
this.performanceOverrideDistance = newDistance >= currentViewDistance ? undefined : newDistance
|
|
659
|
+
console.log(`ChunkMeshManager: Performance improved. Restoring render distance to ${newDistance}`)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Hide sections beyond performance override distance
|
|
666
|
+
*/
|
|
667
|
+
updateSectionsVisibility (): void {
|
|
668
|
+
const cameraPos = this.worldRenderer.cameraSectionPos
|
|
669
|
+
for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
|
|
670
|
+
if (!this.performanceOverrideDistance) {
|
|
671
|
+
sectionObject.visible = true
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const [x, y, z] = sectionKey.split(',').map(Number)
|
|
676
|
+
const sectionPos = { x: x / 16, y: y / 16, z: z / 16 }
|
|
677
|
+
|
|
678
|
+
// Calculate distance using hypot (same as render distance calculation)
|
|
679
|
+
const dx = sectionPos.x - cameraPos.x
|
|
680
|
+
const dz = sectionPos.z - cameraPos.z
|
|
681
|
+
const distance = Math.floor(Math.hypot(dx, dz))
|
|
682
|
+
|
|
683
|
+
sectionObject.visible = distance <= this.performanceOverrideDistance
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class SignHeadsRenderer {
|
|
690
|
+
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
|
691
|
+
|
|
692
|
+
constructor (public worldRendererThree: WorldRendererThree) {
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
dispose () {
|
|
696
|
+
for (const [, textures] of this.chunkTextures) {
|
|
697
|
+
for (const key of Object.keys(textures)) {
|
|
698
|
+
textures[key].dispose()
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
this.chunkTextures.clear()
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
|
705
|
+
let textureData: string
|
|
706
|
+
if (blockEntity.SkullOwner) {
|
|
707
|
+
textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
|
|
708
|
+
} else {
|
|
709
|
+
textureData = blockEntity.profile?.properties?.find(p => p.name === 'textures')?.value
|
|
710
|
+
}
|
|
711
|
+
if (!textureData) return
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
|
|
715
|
+
let skinUrl = decodedData.textures?.SKIN?.url
|
|
716
|
+
const { skinTexturesProxy } = this.worldRendererThree.worldRendererConfig
|
|
717
|
+
if (skinTexturesProxy) {
|
|
718
|
+
skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
|
719
|
+
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const mesh = getMesh(this.worldRendererThree, skinUrl, armorModel.head as any)
|
|
723
|
+
const group = new THREE.Group()
|
|
724
|
+
if (isWall) {
|
|
725
|
+
mesh.position.set(0, 0.3125, 0.3125)
|
|
726
|
+
}
|
|
727
|
+
// move head model down as armor have a different offset than blocks
|
|
728
|
+
mesh.position.y -= 23 / 16
|
|
729
|
+
group.add(mesh)
|
|
730
|
+
this.worldRendererThree.sceneOrigin.track(group)
|
|
731
|
+
group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
|
|
732
|
+
group.rotation.set(
|
|
733
|
+
0,
|
|
734
|
+
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
735
|
+
0
|
|
736
|
+
)
|
|
737
|
+
group.scale.set(0.8, 0.8, 0.8)
|
|
738
|
+
return group
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.error('Error decoding player texture:', err)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
|
745
|
+
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
|
746
|
+
|
|
747
|
+
if (!tex) return
|
|
748
|
+
|
|
749
|
+
// todo implement
|
|
750
|
+
// const key = JSON.stringify({ position, rotation, isWall })
|
|
751
|
+
// if (this.signsCache.has(key)) {
|
|
752
|
+
// console.log('cached', key)
|
|
753
|
+
// } else {
|
|
754
|
+
// this.signsCache.set(key, tex)
|
|
755
|
+
// }
|
|
756
|
+
|
|
757
|
+
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
|
758
|
+
mesh.renderOrder = 999
|
|
759
|
+
|
|
760
|
+
const lineHeight = 7 / 16
|
|
761
|
+
const scaleFactor = isHanging ? 1.3 : 1
|
|
762
|
+
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
|
|
763
|
+
|
|
764
|
+
const thickness = (isHanging ? 2 : 1.5) / 16
|
|
765
|
+
const wallSpacing = 0.25 / 16
|
|
766
|
+
if (isWall && !isHanging) {
|
|
767
|
+
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
|
|
768
|
+
} else {
|
|
769
|
+
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const group = new THREE.Group()
|
|
773
|
+
group.rotation.set(
|
|
774
|
+
0,
|
|
775
|
+
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
776
|
+
0
|
|
777
|
+
)
|
|
778
|
+
group.add(mesh)
|
|
779
|
+
const height = (isHanging ? 10 : 8) / 16
|
|
780
|
+
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
|
781
|
+
const textPosition = height / 2 + heightOffset
|
|
782
|
+
this.worldRendererThree.sceneOrigin.track(group)
|
|
783
|
+
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
|
784
|
+
return group
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
|
788
|
+
const chunk = chunkPos(position)
|
|
789
|
+
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
|
790
|
+
if (!textures) {
|
|
791
|
+
textures = {}
|
|
792
|
+
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
|
793
|
+
}
|
|
794
|
+
const texturekey = `${position.x},${position.y},${position.z}`
|
|
795
|
+
// todo investigate bug and remove this so don't need to clean in section dirty
|
|
796
|
+
if (textures[texturekey]) return textures[texturekey]
|
|
797
|
+
|
|
798
|
+
const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version)
|
|
799
|
+
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
|
800
|
+
if (!canvas) return
|
|
801
|
+
const tex = new THREE.Texture(canvas)
|
|
802
|
+
tex.magFilter = THREE.NearestFilter
|
|
803
|
+
tex.minFilter = THREE.NearestFilter
|
|
804
|
+
tex.needsUpdate = true
|
|
805
|
+
textures[texturekey] = tex
|
|
806
|
+
return tex
|
|
807
|
+
}
|
|
808
|
+
}
|