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