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.
@@ -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
+ }