minecraft-renderer 0.1.33 → 0.1.35
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/minecraft-renderer.js +56 -56
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +422 -422
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +2 -1
- package/src/lib/worldrendererCommon.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 +9 -1
- package/src/three/modules/blockBreakParticles.ts +438 -0
- package/src/three/modules/index.ts +2 -0
- package/src/three/modules/sciFiWorldReveal.ts +10 -21
- package/src/three/panorama.ts +1 -1
- package/src/three/worldRendererThree.ts +60 -39
- package/src/three/worldBlockGeometry.ts +0 -355
|
@@ -13,7 +13,6 @@ import { addNewStat } from '../lib/ui/newStats'
|
|
|
13
13
|
import { MesherGeometryOutput } from '../mesher/shared'
|
|
14
14
|
import { ItemSpecificContextProperties } from '../playerState/types'
|
|
15
15
|
import { setBlockPosition } from '../mesher/standaloneRenderer'
|
|
16
|
-
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
|
|
17
16
|
import { getMyHand } from './hand'
|
|
18
17
|
import { createHoldingBlock } from './holdingBlockFactory'
|
|
19
18
|
import type { IHoldingBlock } from './holdingBlockTypes'
|
|
@@ -34,7 +33,7 @@ import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
|
|
|
34
33
|
import { FireworksManager } from './fireworks'
|
|
35
34
|
import { SceneOrigin } from './sceneOrigin'
|
|
36
35
|
import { downloadWorldGeometry } from './worldGeometryExport'
|
|
37
|
-
import {
|
|
36
|
+
import { ChunkMeshManager } from './chunkMeshManager'
|
|
38
37
|
import type { RendererModuleManifest, RegisteredModule, RendererModuleController } from './rendererModuleSystem'
|
|
39
38
|
import { BUILTIN_MODULES } from './modules/index'
|
|
40
39
|
|
|
@@ -42,16 +41,17 @@ type SectionKey = string
|
|
|
42
41
|
|
|
43
42
|
export class WorldRendererThree extends WorldRendererCommon {
|
|
44
43
|
outputFormat = 'threeJs' as const
|
|
45
|
-
|
|
44
|
+
chunkMeshManager: ChunkMeshManager
|
|
46
45
|
get sectionObjects() {
|
|
47
|
-
return this.
|
|
46
|
+
return this.chunkMeshManager.sectionObjects
|
|
48
47
|
}
|
|
49
48
|
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
|
50
49
|
signsCache = new Map<string, any>()
|
|
51
50
|
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
|
52
51
|
holdingBlock: IHoldingBlock
|
|
53
52
|
holdingBlockLeft: IHoldingBlock
|
|
54
|
-
|
|
53
|
+
realScene = new THREE.Scene()
|
|
54
|
+
scene = new THREE.Group()
|
|
55
55
|
ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
|
56
56
|
directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
|
57
57
|
entities = new Entities(this, (globalThis as any).mcData)
|
|
@@ -65,7 +65,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
65
65
|
cameraContainer!: THREE.Object3D
|
|
66
66
|
media: ThreeJsMedia
|
|
67
67
|
get waitingChunksToDisplay() {
|
|
68
|
-
return
|
|
68
|
+
return {} as { [chunkKey: string]: string[] }
|
|
69
69
|
}
|
|
70
70
|
waypoints: WaypointsRenderer
|
|
71
71
|
cinimaticScript: CinimaticScriptRunner
|
|
@@ -81,7 +81,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
81
81
|
renderTimeAvg = 0
|
|
82
82
|
// Memory usage tracking (in bytes)
|
|
83
83
|
get estimatedMemoryUsage() {
|
|
84
|
-
return this.
|
|
84
|
+
return this.chunkMeshManager.getEstimatedMemoryUsage().total
|
|
85
85
|
}
|
|
86
86
|
// Module system
|
|
87
87
|
private modules = {} as Record<string, RegisteredModule>
|
|
@@ -107,7 +107,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
107
107
|
DEBUG_RAYCAST = false
|
|
108
108
|
skyboxRenderer: SkyboxRenderer
|
|
109
109
|
fireworks: FireworksManager
|
|
110
|
-
sceneOrigin = new SceneOrigin(this.
|
|
110
|
+
sceneOrigin = new SceneOrigin(this.realScene)
|
|
111
111
|
/** Camera world position stored in float64 (JS number) for precision */
|
|
112
112
|
cameraWorldPos = { x: 0, y: 0, z: 0 }
|
|
113
113
|
|
|
@@ -131,11 +131,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
131
131
|
private readonly _tpChunkWorldPos = new THREE.Vector3()
|
|
132
132
|
|
|
133
133
|
get tilesRendered() {
|
|
134
|
-
return
|
|
134
|
+
return this.chunkMeshManager.getTotalTiles()
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
get blocksRendered() {
|
|
138
|
-
return
|
|
138
|
+
return this.chunkMeshManager.getTotalBlocks()
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
constructor(public renderer: THREE.WebGLRenderer, public initOptions: GraphicsInitOptions, public displayOptions: DisplayWorldOptions) {
|
|
@@ -145,8 +145,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
145
145
|
this.renderer = renderer
|
|
146
146
|
displayOptions.rendererState.renderer = WorldRendererThree.getRendererInfo(renderer) ?? '...'
|
|
147
147
|
|
|
148
|
-
// Initialize
|
|
149
|
-
this.
|
|
148
|
+
// Initialize chunk mesh manager
|
|
149
|
+
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
|
|
150
|
+
this.onRenderDistanceChanged = (viewDistance) => {
|
|
151
|
+
this.chunkMeshManager.updateViewDistance(viewDistance)
|
|
152
|
+
}
|
|
150
153
|
|
|
151
154
|
this.cursorBlock = new CursorBlock(this)
|
|
152
155
|
this.holdingBlock = createHoldingBlock(this)
|
|
@@ -158,7 +161,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
// Initialize skybox renderer
|
|
161
|
-
this.skyboxRenderer = new SkyboxRenderer(this.
|
|
164
|
+
this.skyboxRenderer = new SkyboxRenderer(this.realScene, false, null)
|
|
162
165
|
void this.skyboxRenderer.init()
|
|
163
166
|
|
|
164
167
|
this.addDebugOverlay()
|
|
@@ -181,7 +184,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
181
184
|
fov: this.camera.fov
|
|
182
185
|
})
|
|
183
186
|
)
|
|
184
|
-
this.fireworks = new FireworksManager(this.
|
|
187
|
+
this.fireworks = new FireworksManager(this.realScene, this.sceneOrigin)
|
|
185
188
|
|
|
186
189
|
// this.fountain = new Fountain(this.scene, this.scene, {
|
|
187
190
|
// position: new THREE.Vector3(0, 10, 0),
|
|
@@ -447,19 +450,20 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
447
450
|
this.cameraWorldPos.y = 0
|
|
448
451
|
this.cameraWorldPos.z = 0
|
|
449
452
|
|
|
450
|
-
this.
|
|
451
|
-
this.
|
|
452
|
-
this.
|
|
453
|
+
this.realScene.matrixAutoUpdate = false // for perf
|
|
454
|
+
this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
|
|
455
|
+
this.realScene.add(this.ambientLight)
|
|
453
456
|
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
|
454
457
|
this.directionalLight.castShadow = true
|
|
455
|
-
this.
|
|
458
|
+
this.realScene.add(this.directionalLight)
|
|
456
459
|
|
|
457
460
|
const size = this.renderer.getSize(new THREE.Vector2())
|
|
458
461
|
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
|
459
462
|
this._wrapCameraPositionWithWarning()
|
|
460
463
|
this.cameraContainer = new THREE.Object3D()
|
|
461
464
|
this.cameraContainer.add(this.camera)
|
|
462
|
-
this.
|
|
465
|
+
this.realScene.add(this.cameraContainer)
|
|
466
|
+
this.realScene.add(this.scene)
|
|
463
467
|
}
|
|
464
468
|
|
|
465
469
|
override watchReactivePlayerState() {
|
|
@@ -625,7 +629,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
625
629
|
}
|
|
626
630
|
|
|
627
631
|
changeBackgroundColor(color: [number, number, number]): void {
|
|
628
|
-
this.
|
|
632
|
+
this.realScene.background = new THREE.Color(color[0], color[1], color[2])
|
|
629
633
|
}
|
|
630
634
|
|
|
631
635
|
timeUpdated(newTime: number): void {
|
|
@@ -662,7 +666,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
662
666
|
setBlockPosition(mesh, pos)
|
|
663
667
|
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
|
664
668
|
mesh.add(helper)
|
|
665
|
-
this.
|
|
669
|
+
this.realScene.add(mesh)
|
|
666
670
|
}
|
|
667
671
|
|
|
668
672
|
demoItem() {
|
|
@@ -675,7 +679,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
675
679
|
// mesh.scale.set(0.5, 0.5, 0.5)
|
|
676
680
|
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
|
677
681
|
mesh.add(helper)
|
|
678
|
-
this.
|
|
682
|
+
this.realScene.add(mesh)
|
|
679
683
|
}
|
|
680
684
|
|
|
681
685
|
debugOverlayAdded = false
|
|
@@ -695,7 +699,9 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
695
699
|
text += `TE: ${formatBigNumber(this.renderer.info.memory.textures)} `
|
|
696
700
|
text += `F: ${formatBigNumber(this.tilesRendered)} `
|
|
697
701
|
text += `B: ${formatBigNumber(this.blocksRendered)} `
|
|
698
|
-
text += `MEM: ${this.
|
|
702
|
+
text += `MEM: ${this.chunkMeshManager.getEstimatedMemoryUsage().total} `
|
|
703
|
+
const poolStats = this.chunkMeshManager.getStats()
|
|
704
|
+
text += `POOL: ${poolStats.activeCount}/${poolStats.poolSize} HR: ${poolStats.hitRate}`
|
|
699
705
|
pane.updateText(text)
|
|
700
706
|
this.backendInfoReport = text
|
|
701
707
|
}
|
|
@@ -709,7 +715,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
709
715
|
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
|
710
716
|
// sum of distances: x + y + z
|
|
711
717
|
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
|
|
712
|
-
const
|
|
718
|
+
const sectionObj = this.sectionObjects[key]
|
|
719
|
+
const section = (sectionObj as any).mesh ?? sectionObj.children.find(child => child.name === 'mesh')!
|
|
713
720
|
section.renderOrder = 500 - chunkDistance
|
|
714
721
|
}
|
|
715
722
|
|
|
@@ -732,12 +739,15 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
732
739
|
}
|
|
733
740
|
|
|
734
741
|
finishChunk(chunkKey: string) {
|
|
735
|
-
|
|
742
|
+
// ChunkMeshManager applies updates immediately, no buffering needed
|
|
736
743
|
}
|
|
737
744
|
|
|
738
745
|
handleWorkerMessage(data: { geometry: MesherGeometryOutput, key, type }): void {
|
|
739
746
|
if (data.type === 'geometry') {
|
|
740
|
-
|
|
747
|
+
const chunkCoords = data.key.split(',')
|
|
748
|
+
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
|
749
|
+
this.chunkMeshManager.updateSection(data.key, data.geometry)
|
|
750
|
+
this.updatePosDataChunk(data.key)
|
|
741
751
|
}
|
|
742
752
|
}
|
|
743
753
|
|
|
@@ -883,11 +893,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
883
893
|
private debugRaycast(pos: THREE.Vector3, direction: THREE.Vector3, distance: number) {
|
|
884
894
|
// Remove existing debug objects
|
|
885
895
|
if (this.debugRaycastHelper) {
|
|
886
|
-
this.
|
|
896
|
+
this.realScene.remove(this.debugRaycastHelper)
|
|
887
897
|
this.debugRaycastHelper = undefined
|
|
888
898
|
}
|
|
889
899
|
if (this.debugHitPoint) {
|
|
890
|
-
this.
|
|
900
|
+
this.realScene.remove(this.debugHitPoint)
|
|
891
901
|
this.debugHitPoint = undefined
|
|
892
902
|
}
|
|
893
903
|
|
|
@@ -907,14 +917,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
907
917
|
distance * 0.1,
|
|
908
918
|
distance * 0.05
|
|
909
919
|
)
|
|
910
|
-
this.
|
|
920
|
+
this.realScene.add(this.debugRaycastHelper)
|
|
911
921
|
|
|
912
922
|
// Create hit point indicator
|
|
913
923
|
const hitGeometry = new THREE.SphereGeometry(0.2, 8, 8)
|
|
914
924
|
const hitMaterial = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 })
|
|
915
925
|
this.debugHitPoint = new THREE.Mesh(hitGeometry, hitMaterial)
|
|
916
926
|
this.debugHitPoint.position.copy(scenePos).add(direction.clone().multiplyScalar(distance))
|
|
917
|
-
this.
|
|
927
|
+
this.realScene.add(this.debugHitPoint)
|
|
918
928
|
}
|
|
919
929
|
|
|
920
930
|
prevFramePerspective = null as string | null
|
|
@@ -1012,11 +1022,11 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1012
1022
|
|
|
1013
1023
|
// remove any debug raycasting
|
|
1014
1024
|
if (this.debugRaycastHelper) {
|
|
1015
|
-
this.
|
|
1025
|
+
this.realScene.remove(this.debugRaycastHelper)
|
|
1016
1026
|
this.debugRaycastHelper = undefined
|
|
1017
1027
|
}
|
|
1018
1028
|
if (this.debugHitPoint) {
|
|
1019
|
-
this.
|
|
1029
|
+
this.realScene.remove(this.debugHitPoint)
|
|
1020
1030
|
this.debugHitPoint = undefined
|
|
1021
1031
|
}
|
|
1022
1032
|
}
|
|
@@ -1087,9 +1097,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1087
1097
|
|
|
1088
1098
|
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
|
1089
1099
|
const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
|
1090
|
-
//
|
|
1091
|
-
this.
|
|
1092
|
-
this.renderer.render(this.scene, cam)
|
|
1100
|
+
// ChunkMeshManager applies updates immediately, no pending updates to flush
|
|
1101
|
+
this.renderer.render(this.realScene, cam)
|
|
1093
1102
|
|
|
1094
1103
|
if (
|
|
1095
1104
|
this.displayOptions.inWorldRenderingConfig.showHand &&
|
|
@@ -1118,6 +1127,9 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1118
1127
|
}
|
|
1119
1128
|
const end = performance.now()
|
|
1120
1129
|
const totalTime = end - start
|
|
1130
|
+
if (this.worldRendererConfig.autoLowerRenderDistance) {
|
|
1131
|
+
this.chunkMeshManager.recordRenderTime(totalTime)
|
|
1132
|
+
}
|
|
1121
1133
|
this.renderTimeAvgCount++
|
|
1122
1134
|
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
|
|
1123
1135
|
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
|
|
@@ -1233,15 +1245,16 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1233
1245
|
resetWorld() {
|
|
1234
1246
|
super.resetWorld()
|
|
1235
1247
|
|
|
1236
|
-
this.
|
|
1248
|
+
this.chunkMeshManager.dispose()
|
|
1249
|
+
this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
|
|
1237
1250
|
|
|
1238
1251
|
// Clean up debug objects
|
|
1239
1252
|
if (this.debugRaycastHelper) {
|
|
1240
|
-
this.
|
|
1253
|
+
this.realScene.remove(this.debugRaycastHelper)
|
|
1241
1254
|
this.debugRaycastHelper = undefined
|
|
1242
1255
|
}
|
|
1243
1256
|
if (this.debugHitPoint) {
|
|
1244
|
-
this.
|
|
1257
|
+
this.realScene.remove(this.debugHitPoint)
|
|
1245
1258
|
this.debugHitPoint = undefined
|
|
1246
1259
|
}
|
|
1247
1260
|
}
|
|
@@ -1286,7 +1299,14 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1286
1299
|
super.removeColumn(x, z)
|
|
1287
1300
|
|
|
1288
1301
|
this.cleanChunkTextures(x, z)
|
|
1289
|
-
this.
|
|
1302
|
+
const sectionHeight = this.getSectionHeight()
|
|
1303
|
+
const worldMinY = this.worldMinYRender
|
|
1304
|
+
for (let y = worldMinY; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
1305
|
+
const key = `${x},${y},${z}`
|
|
1306
|
+
if (this.chunkMeshManager.sectionObjects[key]) {
|
|
1307
|
+
this.chunkMeshManager.releaseSection(key)
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1290
1310
|
}
|
|
1291
1311
|
|
|
1292
1312
|
setSectionDirty(...args: Parameters<WorldRendererCommon['setSectionDirty']>) {
|
|
@@ -1309,6 +1329,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1309
1329
|
}
|
|
1310
1330
|
|
|
1311
1331
|
destroy(): void {
|
|
1332
|
+
this.chunkMeshManager.dispose()
|
|
1312
1333
|
this.disposeModules()
|
|
1313
1334
|
this.fireworksLegacy.destroy()
|
|
1314
1335
|
super.destroy()
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
//@ts-nocheck
|
|
2
|
-
import * as THREE from 'three'
|
|
3
|
-
import { Vec3 } from 'vec3'
|
|
4
|
-
import nbt from 'prismarine-nbt'
|
|
5
|
-
import { MesherGeometryOutput, IS_FULL_WORLD_SECTION } from '../mesher/shared'
|
|
6
|
-
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
|
|
7
|
-
import type { WorldRendererThree } from './worldRendererThree'
|
|
8
|
-
|
|
9
|
-
export interface SectionObject extends THREE.Object3D {
|
|
10
|
-
foutain?: boolean
|
|
11
|
-
tilesCount?: number
|
|
12
|
-
blocksCount?: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class WorldBlockGeometry {
|
|
16
|
-
sectionObjects: Record<string, SectionObject> = {}
|
|
17
|
-
waitingChunksToDisplay: { [chunkKey: string]: string[] } = {}
|
|
18
|
-
estimatedMemoryUsage = 0
|
|
19
|
-
private pendingUpdates: Map<string, { geometry: MesherGeometryOutput; key: string; type: string }> = new Map()
|
|
20
|
-
private pendingBufferStartTime: number | null = null
|
|
21
|
-
private static readonly MAX_BUFFER_MS = 500
|
|
22
|
-
|
|
23
|
-
constructor(
|
|
24
|
-
private readonly worldRenderer: WorldRendererThree,
|
|
25
|
-
private readonly scene: THREE.Scene,
|
|
26
|
-
private readonly material: THREE.MeshLambertMaterial,
|
|
27
|
-
private readonly displayOptions: any
|
|
28
|
-
) { }
|
|
29
|
-
|
|
30
|
-
handleWorkerGeometryMessage(data: { geometry: MesherGeometryOutput; key: string; type: string }): void {
|
|
31
|
-
const isUpdate = !!this.sectionObjects[data.key]
|
|
32
|
-
|
|
33
|
-
if (isUpdate) {
|
|
34
|
-
// Buffer updates for existing sections — keep old mesh visible
|
|
35
|
-
this.pendingUpdates.set(data.key, data)
|
|
36
|
-
if (this.pendingBufferStartTime === null) {
|
|
37
|
-
this.pendingBufferStartTime = performance.now()
|
|
38
|
-
}
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Initial load — apply immediately
|
|
43
|
-
this._applySectionGeometry(data)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
applyPendingUpdates(): void {
|
|
47
|
-
if (this.pendingUpdates.size === 0) return
|
|
48
|
-
|
|
49
|
-
const now = performance.now()
|
|
50
|
-
const sinceFirst = now - (this.pendingBufferStartTime ?? now)
|
|
51
|
-
|
|
52
|
-
// Wait for neighboring sections still being meshed (unless max timeout reached)
|
|
53
|
-
if (sinceFirst < WorldBlockGeometry.MAX_BUFFER_MS) {
|
|
54
|
-
const sectionHeight = this.worldRenderer.getSectionHeight()
|
|
55
|
-
for (const key of this.pendingUpdates.keys()) {
|
|
56
|
-
const [sx, sy, sz] = key.split(',').map(Number)
|
|
57
|
-
const neighborKeys = [
|
|
58
|
-
`${sx - 16},${sy},${sz}`, `${sx + 16},${sy},${sz}`,
|
|
59
|
-
`${sx},${sy - sectionHeight},${sz}`, `${sx},${sy + sectionHeight},${sz}`,
|
|
60
|
-
`${sx},${sy},${sz - 16}`, `${sx},${sy},${sz + 16}`,
|
|
61
|
-
]
|
|
62
|
-
for (const neighborKey of neighborKeys) {
|
|
63
|
-
// Wait if neighbor is being meshed, hasn't arrived yet, and has a loaded mesh
|
|
64
|
-
if (this.worldRenderer.sectionsWaiting.has(neighborKey) &&
|
|
65
|
-
!this.pendingUpdates.has(neighborKey) &&
|
|
66
|
-
this.sectionObjects[neighborKey]) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Flush all pending updates atomically
|
|
74
|
-
for (const [key, data] of this.pendingUpdates) {
|
|
75
|
-
this._removeSectionSafely(key)
|
|
76
|
-
this._applySectionGeometry(data)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.pendingUpdates.clear()
|
|
80
|
-
this.pendingBufferStartTime = null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private disposeSectionObject(obj: THREE.Object3D): void {
|
|
84
|
-
if (obj instanceof THREE.Mesh) {
|
|
85
|
-
obj.geometry?.dispose?.()
|
|
86
|
-
// Don't dispose material - it's shared across all sections
|
|
87
|
-
}
|
|
88
|
-
if (obj.children) {
|
|
89
|
-
obj.children.forEach(child => this.disposeSectionObject(child))
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private _removeSectionSafely(key: string): void {
|
|
94
|
-
const object = this.sectionObjects[key]
|
|
95
|
-
if (!object) return
|
|
96
|
-
|
|
97
|
-
this.removeSectionMemoryUsage(object)
|
|
98
|
-
object.traverse((child) => {
|
|
99
|
-
if ((child as any).bannerTexture) {
|
|
100
|
-
releaseBannerTexture((child as any).bannerTexture)
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
this.worldRenderer.sceneOrigin.removeAndUntrackAll(object)
|
|
104
|
-
this.disposeSectionObject(object)
|
|
105
|
-
delete this.sectionObjects[key]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private _applySectionGeometry(data: { geometry: MesherGeometryOutput; key: string; type: string }): void {
|
|
109
|
-
const chunkCoords = data.key.split(',')
|
|
110
|
-
if (
|
|
111
|
-
!this.worldRenderer.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] ||
|
|
112
|
-
!data.geometry.positions.length ||
|
|
113
|
-
!this.worldRenderer.active
|
|
114
|
-
)
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
const geometry = new THREE.BufferGeometry()
|
|
118
|
-
const positionAttr = new THREE.BufferAttribute(data.geometry.positions, 3)
|
|
119
|
-
const normalAttr = new THREE.BufferAttribute(data.geometry.normals, 3)
|
|
120
|
-
const colorAttr = new THREE.BufferAttribute(data.geometry.colors, 3)
|
|
121
|
-
const uvAttr = new THREE.BufferAttribute(data.geometry.uvs, 2)
|
|
122
|
-
const indexAttr = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1)
|
|
123
|
-
|
|
124
|
-
geometry.setAttribute('position', positionAttr)
|
|
125
|
-
geometry.setAttribute('normal', normalAttr)
|
|
126
|
-
geometry.setAttribute('color', colorAttr)
|
|
127
|
-
geometry.setAttribute('uv', uvAttr)
|
|
128
|
-
geometry.index = indexAttr
|
|
129
|
-
|
|
130
|
-
// Track memory usage for this section
|
|
131
|
-
this.addSectionMemoryUsage(geometry)
|
|
132
|
-
|
|
133
|
-
const mesh = new THREE.Mesh(geometry, this.material)
|
|
134
|
-
this.worldRenderer.sceneOrigin.track(mesh, { updateMatrix: true })
|
|
135
|
-
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
|
136
|
-
mesh.name = 'mesh'
|
|
137
|
-
const object: THREE.Object3D = new THREE.Group()
|
|
138
|
-
object.add(mesh)
|
|
139
|
-
// mesh with static dimensions: 16x16xsectionHeight
|
|
140
|
-
const sectionHeight = data.geometry.sectionEndY - data.geometry.sectionStartY
|
|
141
|
-
const CHUNK_SIZE = 16
|
|
142
|
-
const staticChunkMesh = new THREE.Mesh(
|
|
143
|
-
new THREE.BoxGeometry(CHUNK_SIZE, sectionHeight, CHUNK_SIZE),
|
|
144
|
-
new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
|
|
145
|
-
)
|
|
146
|
-
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
|
147
|
-
boxHelper.name = 'helper'
|
|
148
|
-
this.worldRenderer.sceneOrigin.track(boxHelper, { updateMatrix: true })
|
|
149
|
-
boxHelper.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
|
150
|
-
object.add(boxHelper)
|
|
151
|
-
object.name = 'chunk'
|
|
152
|
-
; (object as any).tilesCount = data.geometry.positions.length / 3 / 4
|
|
153
|
-
; (object as any).blocksCount = data.geometry.blocksCount
|
|
154
|
-
if (!this.displayOptions.inWorldRenderingConfig.showChunkBorders) {
|
|
155
|
-
boxHelper.visible = false
|
|
156
|
-
}
|
|
157
|
-
// should not compute it once
|
|
158
|
-
if (Object.keys(data.geometry.signs).length) {
|
|
159
|
-
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
|
|
160
|
-
const signBlockEntity = this.worldRenderer.blockEntities[posKey]
|
|
161
|
-
if (!signBlockEntity) continue
|
|
162
|
-
const [x, y, z] = posKey.split(',')
|
|
163
|
-
const sign = this.worldRenderer.renderSign(
|
|
164
|
-
new Vec3(+x, +y, +z),
|
|
165
|
-
rotation,
|
|
166
|
-
isWall,
|
|
167
|
-
isHanging,
|
|
168
|
-
nbt.simplify(signBlockEntity)
|
|
169
|
-
)
|
|
170
|
-
if (!sign) continue
|
|
171
|
-
object.add(sign)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (Object.keys(data.geometry.heads).length) {
|
|
175
|
-
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
|
|
176
|
-
const headBlockEntity = this.worldRenderer.blockEntities[posKey]
|
|
177
|
-
if (!headBlockEntity) continue
|
|
178
|
-
const [x, y, z] = posKey.split(',')
|
|
179
|
-
const head = this.worldRenderer.renderHead(
|
|
180
|
-
new Vec3(+x, +y, +z),
|
|
181
|
-
rotation,
|
|
182
|
-
isWall,
|
|
183
|
-
nbt.simplify(headBlockEntity)
|
|
184
|
-
)
|
|
185
|
-
if (!head) continue
|
|
186
|
-
object.add(head)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (Object.keys(data.geometry.banners).length) {
|
|
190
|
-
for (const [posKey, { isWall, rotation, blockName }] of Object.entries(data.geometry.banners)) {
|
|
191
|
-
const bannerBlockEntity = this.worldRenderer.blockEntities[posKey]
|
|
192
|
-
if (!bannerBlockEntity) continue
|
|
193
|
-
const [x, y, z] = posKey.split(',')
|
|
194
|
-
const bannerTexture = getBannerTexture(this.worldRenderer, blockName, nbt.simplify(bannerBlockEntity))
|
|
195
|
-
if (!bannerTexture) continue
|
|
196
|
-
const banner = createBannerMesh(new Vec3(+x, +y, +z), rotation, isWall, bannerTexture)
|
|
197
|
-
const { x: bwx, y: bwy, z: bwz } = banner.position
|
|
198
|
-
this.worldRenderer.sceneOrigin.track(banner)
|
|
199
|
-
banner.position.set(bwx, bwy, bwz)
|
|
200
|
-
object.add(banner)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
this.sectionObjects[data.key] = object
|
|
204
|
-
// Store section key on object for easier lookup
|
|
205
|
-
;(object as any).sectionKey = data.key
|
|
206
|
-
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
|
|
207
|
-
object.visible = false
|
|
208
|
-
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
|
|
209
|
-
this.waitingChunksToDisplay[chunkKey] ??= []
|
|
210
|
-
this.waitingChunksToDisplay[chunkKey].push(data.key)
|
|
211
|
-
if (this.worldRenderer.finishedChunks[chunkKey]) {
|
|
212
|
-
// todo it might happen even when it was not an update
|
|
213
|
-
this.finishChunk(chunkKey)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this.worldRenderer.updatePosDataChunk(data.key)
|
|
218
|
-
object.matrixAutoUpdate = false
|
|
219
|
-
// Force matrix update after setting camera-relative position (matrixAutoUpdate is false)
|
|
220
|
-
object.updateMatrix()
|
|
221
|
-
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
|
|
222
|
-
// mesh.matrixAutoUpdate = false
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
this.scene.add(object)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
finishChunk(chunkKey: string) {
|
|
229
|
-
for (const sectionKey of this.waitingChunksToDisplay[chunkKey] ?? []) {
|
|
230
|
-
this.sectionObjects[sectionKey].visible = true
|
|
231
|
-
}
|
|
232
|
-
delete this.waitingChunksToDisplay[chunkKey]
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Estimate memory usage of BufferGeometry attributes
|
|
238
|
-
*/
|
|
239
|
-
private estimateGeometryMemoryUsage(geometry: THREE.BufferGeometry): number {
|
|
240
|
-
let memoryBytes = 0
|
|
241
|
-
|
|
242
|
-
// Calculate memory for each attribute
|
|
243
|
-
const { attributes } = geometry
|
|
244
|
-
for (const [name, attribute] of Object.entries(attributes)) {
|
|
245
|
-
if (attribute?.array) {
|
|
246
|
-
// Each number in typed arrays takes different bytes:
|
|
247
|
-
// Float32Array: 4 bytes per number
|
|
248
|
-
// Uint32Array: 4 bytes per number
|
|
249
|
-
// Uint16Array: 2 bytes per number
|
|
250
|
-
const bytesPerElement = attribute.array.BYTES_PER_ELEMENT
|
|
251
|
-
memoryBytes += attribute.array.length * bytesPerElement
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Calculate memory for indices
|
|
256
|
-
if (geometry.index?.array) {
|
|
257
|
-
const bytesPerElement = geometry.index.array.BYTES_PER_ELEMENT
|
|
258
|
-
memoryBytes += geometry.index.array.length * bytesPerElement
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return memoryBytes
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Update memory usage when section is added
|
|
266
|
-
*/
|
|
267
|
-
private addSectionMemoryUsage(geometry: THREE.BufferGeometry): void {
|
|
268
|
-
const memoryUsage = this.estimateGeometryMemoryUsage(geometry)
|
|
269
|
-
this.estimatedMemoryUsage += memoryUsage
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Update memory usage when section is removed
|
|
274
|
-
*/
|
|
275
|
-
private removeSectionMemoryUsage(object: THREE.Object3D): void {
|
|
276
|
-
// Find mesh with geometry in the object
|
|
277
|
-
const mesh = object.children.find((child) => child.name === 'mesh') as THREE.Mesh
|
|
278
|
-
if (mesh?.geometry) {
|
|
279
|
-
const memoryUsage = this.estimateGeometryMemoryUsage(mesh.geometry)
|
|
280
|
-
this.estimatedMemoryUsage -= memoryUsage
|
|
281
|
-
this.estimatedMemoryUsage = Math.max(0, this.estimatedMemoryUsage) // Ensure non-negative
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Get estimated memory usage in a human-readable format
|
|
287
|
-
*/
|
|
288
|
-
getEstimatedMemoryUsage(): { bytes: number; readable: string } {
|
|
289
|
-
const bytes = this.estimatedMemoryUsage
|
|
290
|
-
const mb = bytes / (1024 * 1024)
|
|
291
|
-
const readable = `${mb.toFixed(2)} MB`
|
|
292
|
-
return { bytes, readable }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
resetWorld() {
|
|
296
|
-
for (const mesh of Object.values(this.sectionObjects)) {
|
|
297
|
-
// Track memory usage removal for all sections
|
|
298
|
-
this.removeSectionMemoryUsage(mesh)
|
|
299
|
-
this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
|
|
300
|
-
}
|
|
301
|
-
this.sectionObjects = {}
|
|
302
|
-
this.waitingChunksToDisplay = {}
|
|
303
|
-
|
|
304
|
-
// Reset memory tracking since all sections are cleared
|
|
305
|
-
this.estimatedMemoryUsage = 0
|
|
306
|
-
this.pendingUpdates.clear()
|
|
307
|
-
this.pendingBufferStartTime = null
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
removeColumn(x: number, z: number) {
|
|
311
|
-
const sectionHeight = this.worldRenderer.getSectionHeight()
|
|
312
|
-
const worldMinY = this.worldRenderer.worldMinYRender
|
|
313
|
-
if (IS_FULL_WORLD_SECTION) {
|
|
314
|
-
// Only one section per chunk when full world section
|
|
315
|
-
const y = worldMinY
|
|
316
|
-
this.worldRenderer.setSectionDirty(new Vec3(x, y, z), false)
|
|
317
|
-
const key = `${x},${y},${z}`
|
|
318
|
-
const mesh = this.sectionObjects[key]
|
|
319
|
-
if (mesh) {
|
|
320
|
-
// Track memory usage removal
|
|
321
|
-
this.removeSectionMemoryUsage(mesh)
|
|
322
|
-
// Cleanup banner textures before disposing
|
|
323
|
-
mesh.traverse((child) => {
|
|
324
|
-
if ((child as any).bannerTexture) {
|
|
325
|
-
releaseBannerTexture((child as any).bannerTexture)
|
|
326
|
-
}
|
|
327
|
-
})
|
|
328
|
-
this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
|
|
329
|
-
this.disposeSectionObject(mesh)
|
|
330
|
-
}
|
|
331
|
-
delete this.sectionObjects[key]
|
|
332
|
-
this.pendingUpdates.delete(key)
|
|
333
|
-
} else {
|
|
334
|
-
for (let y = worldMinY; y < this.worldRenderer.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
335
|
-
this.worldRenderer.setSectionDirty(new Vec3(x, y, z), false)
|
|
336
|
-
const key = `${x},${y},${z}`
|
|
337
|
-
const mesh = this.sectionObjects[key]
|
|
338
|
-
if (mesh) {
|
|
339
|
-
// Track memory usage removal
|
|
340
|
-
this.removeSectionMemoryUsage(mesh)
|
|
341
|
-
// Cleanup banner textures before disposing
|
|
342
|
-
mesh.traverse((child) => {
|
|
343
|
-
if ((child as any).bannerTexture) {
|
|
344
|
-
releaseBannerTexture((child as any).bannerTexture)
|
|
345
|
-
}
|
|
346
|
-
})
|
|
347
|
-
this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
|
|
348
|
-
this.disposeSectionObject(mesh)
|
|
349
|
-
}
|
|
350
|
-
delete this.sectionObjects[key]
|
|
351
|
-
this.pendingUpdates.delete(key)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|