minecraft-renderer 0.1.48 → 0.1.50

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.
Files changed (34) hide show
  1. package/dist/mesher.js +1 -1
  2. package/dist/mesher.js.map +2 -2
  3. package/dist/mesherWasm.js +3740 -183
  4. package/dist/minecraft-renderer.js +332 -60
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +705 -433
  7. package/package.json +1 -1
  8. package/src/graphicsBackend/config.ts +4 -0
  9. package/src/graphicsBackend/playerState.ts +1 -0
  10. package/src/graphicsBackend/rendererOptionsSync.ts +2 -0
  11. package/src/lib/worldrendererCommon.ts +13 -0
  12. package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
  13. package/src/mesher-shared/shared.ts +8 -0
  14. package/src/three/chunkMeshManager.ts +312 -39
  15. package/src/three/globalBlockBuffer.ts +292 -0
  16. package/src/three/menuBackground/config.ts +1 -1
  17. package/src/three/menuBackground/defaultOptions.ts +52 -19
  18. package/src/three/menuBackground/index.ts +5 -1
  19. package/src/three/modules/sciFiWorldReveal.ts +162 -68
  20. package/src/three/modules/starfield.ts +9 -1
  21. package/src/three/sectionRaycastAabb.ts +167 -0
  22. package/src/three/shaderCubeMesh.ts +93 -0
  23. package/src/three/shaders/cubeBlockShader.ts +354 -0
  24. package/src/three/shaders/textureIndexMapping.ts +122 -0
  25. package/src/three/shaders/tintPalette.ts +198 -0
  26. package/src/three/worldGeometryExport.ts +53 -25
  27. package/src/three/worldRendererThree.ts +56 -23
  28. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  29. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +399 -0
  30. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  31. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
  32. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
  33. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
  34. package/src/wasm-mesher/worker/mesherWasm.ts +17 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,6 +27,10 @@ export const defaultWorldRendererConfig = {
27
27
 
28
28
  // Performance settings
29
29
  wasmMesher: true,
30
+ /** Render full 1×1 cubes through the instanced shader path (requires WebGL2). */
31
+ shaderCubeBlocks: false,
32
+ /** 0=off, 1=holes red, 2=tileIndex, 3=faceId colors, 4=atlas alpha */
33
+ shaderCubeDebugMode: 0,
30
34
  mesherWorkers: 1,
31
35
  addChunksBatchWaitTime: 200,
32
36
  _experimentalSmoothChunkLoading: true,
@@ -39,6 +39,7 @@ export const getInitialPlayerState = (): PlayerStateReactive => proxy({
39
39
  heldItemOff: undefined,
40
40
  perspective: 'first_person',
41
41
  onFire: false,
42
+ fovMultiplier: 1,
42
43
  cameraSpectatingEntity: undefined,
43
44
  team: undefined,
44
45
  })
@@ -7,6 +7,7 @@
7
7
  import { subscribe } from 'valtio/vanilla'
8
8
  import type { AppViewer } from './appViewer'
9
9
  import type { RendererStorageOptions } from '../three/menuBackground/defaultOptions'
10
+ import { rendererShaderCubeDebugModeToValue } from '../three/menuBackground/defaultOptions'
10
11
  import type { MenuBackgroundOptions } from '../three/menuBackground/types'
11
12
  import type { MenuBackgroundRenderer } from '../three/menuBackground/renderer'
12
13
  import { menuBackgroundSpeedToMultiplier } from '../three/menuBackground/config'
@@ -171,6 +172,7 @@ export function applyRendererOptions(
171
172
  cfg.starfield = o.starfieldRendering
172
173
  cfg.defaultSkybox = o.defaultSkybox
173
174
  cfg.fov = o.fov
175
+ cfg.shaderCubeDebugMode = rendererShaderCubeDebugModeToValue(o.rendererShaderCubeDebugMode)
174
176
  }
175
177
 
176
178
  /** World-view + hand/camera options (call when WorldView is ready). */
@@ -149,6 +149,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
149
149
  return false
150
150
  }
151
151
 
152
+ /**
153
+ * Effective instanced cube-shader path (config + runtime caps).
154
+ * WorldRendererThree adds WebGL2; worker uses {@link getMesherConfig}.shaderCubeBlocks.
155
+ */
156
+ protected isShaderCubeBlocksEnabled(): boolean {
157
+ return this.worldRendererConfig.shaderCubeBlocks === true
158
+ }
159
+
160
+ shaderCubeBlocksEnabled(): boolean {
161
+ return this.isShaderCubeBlocksEnabled()
162
+ }
163
+
152
164
  worldRendererConfig: WorldRendererConfig
153
165
  playerStateReactive: PlayerStateReactive
154
166
  playerStateUtils: PlayerStateUtils
@@ -629,6 +641,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
629
641
  worldMaxY: this.worldMinYRender + this.worldSizeParams.worldHeight,
630
642
  disableConversionCache: this.worldRendererConfig.disableMesherConversionCache,
631
643
  computeWireframeEdges: this.worldRendererConfig.futuristicReveal === true,
644
+ shaderCubeBlocks: this.isShaderCubeBlocksEnabled(),
632
645
  }
633
646
  }
634
647
 
@@ -11,7 +11,11 @@ export interface ExportedSection {
11
11
  uvs: number[]
12
12
  indices: number[]
13
13
  }
14
- shaderCubes?: unknown
14
+ shaderCubes?: {
15
+ words: Uint32Array
16
+ count: number
17
+ formatVersion: 2
18
+ }
15
19
  }
16
20
 
17
21
  export interface ExportedWorldGeometry {
@@ -20,6 +20,8 @@ export const defaultMesherConfig = {
20
20
  disableBlockEntityTextures: false,
21
21
  disableConversionCache: false,
22
22
  computeWireframeEdges: false,
23
+ /** Pack eligible full-cube faces as GPU-instanced shader words during WASM post-processing. */
24
+ shaderCubeBlocks: false,
23
25
  }
24
26
 
25
27
  export type CustomBlockModels = {
@@ -63,6 +65,12 @@ export type MesherGeometryOutput = {
63
65
  blocksCount: number
64
66
  wireframePositions?: Float32Array
65
67
  customBlockModels?: CustomBlockModels
68
+ /** GPU-instanced full-cube faces packed by the mesher; consumed by ChunkMeshManager. */
69
+ shaderCubes?: {
70
+ words: Uint32Array
71
+ count: number
72
+ formatVersion: 2
73
+ }
66
74
  }
67
75
 
68
76
  export interface MesherMainEvents {
@@ -4,6 +4,15 @@ import * as THREE from 'three'
4
4
  import * as nbt from 'prismarine-nbt'
5
5
  import { Vec3 } from 'vec3'
6
6
  import { MesherGeometryOutput } from '../mesher-shared/shared'
7
+ import { getShaderCubeResources } from '../wasm-mesher/bridge/shaderCubeBridge'
8
+ import { createCubeBlockMaterial } from './shaders/cubeBlockShader'
9
+ import { createShaderCubeMesh, disposeShaderCubeMesh } from './shaderCubeMesh'
10
+ import { GlobalBlockBuffer } from './globalBlockBuffer'
11
+ import {
12
+ computeShaderSectionRaycastAabb,
13
+ raycastAabb,
14
+ type ShaderSectionRaycastBox,
15
+ } from './sectionRaycastAabb'
7
16
  import { chunkPos } from '../lib/simpleUtils'
8
17
  import { renderSign } from '../sign-renderer'
9
18
  import { getMesh } from './entity/EntityMesh'
@@ -20,7 +29,11 @@ export interface ChunkMeshPool {
20
29
  }
21
30
 
22
31
  export interface SectionObject extends THREE.Group {
23
- mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
32
+ mesh?: THREE.Mesh<THREE.BufferGeometry, THREE.Material>
33
+ /** Per-section instanced shader mesh (sci-fi reveal defer only). */
34
+ shaderMesh?: THREE.Mesh<THREE.InstancedBufferGeometry, THREE.ShaderMaterial>
35
+ /** Shader cube words kept for migration to global buffer after reveal. */
36
+ deferredShaderCubes?: { words: Uint32Array, count: number }
24
37
  tilesCount?: number
25
38
  blocksCount?: number
26
39
 
@@ -82,6 +95,12 @@ export class ChunkMeshManager {
82
95
  * section.
83
96
  */
84
97
  private readonly chunkBoxMaterial = new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
98
+ /** Shared across all sections — atlas/tint uniforms updated via {@link syncCubeShaderUniforms}. */
99
+ private cubeShaderMaterial: THREE.ShaderMaterial | null = null
100
+ /** One instanced mesh for all shader-cube faces (single draw call). */
101
+ globalBlockBuffer: GlobalBlockBuffer | null = null
102
+ /** Tight world AABBs for third-person raycast; no per-section Object3D. */
103
+ private readonly shaderSectionRaycastBoxes = new Map<string, ShaderSectionRaycastBox>()
85
104
 
86
105
  // Performance tracking
87
106
  private hits = 0
@@ -138,66 +157,268 @@ export class ChunkMeshManager {
138
157
  }
139
158
  }
140
159
 
160
+ /** True when section has legacy vertices and/or GPU shader cube instances. */
161
+ sectionHasRenderableContent (geometryData: MesherGeometryOutput): boolean {
162
+ if (geometryData.positions.length > 0) return true
163
+ if (!this.isShaderCubesGpuEnabled()) return false
164
+ return (geometryData.shaderCubes?.count ?? 0) > 0
165
+ }
166
+
167
+ isShaderCubesGpuEnabled (): boolean {
168
+ return this.worldRenderer.shaderCubeBlocksEnabled()
169
+ }
170
+
171
+ syncCubeShaderUniforms (): void {
172
+ if (!this.isShaderCubesGpuEnabled()) return
173
+ const mat = this.cubeShaderMaterial ?? this.getCubeShaderMaterial()
174
+ if (!mat) return
175
+ const atlas = (this.material as THREE.MeshBasicMaterial).map ?? null
176
+ mat.uniforms.u_atlas.value = atlas
177
+ const { tintPalette } = getShaderCubeResources()
178
+ if (!tintPalette.isReady()) {
179
+ tintPalette.createTexture()
180
+ }
181
+ mat.uniforms.u_tintPalette.value = tintPalette.getTexture()
182
+ mat.uniforms.u_debugMode.value = this.worldRenderer.worldRendererConfig.shaderCubeDebugMode ?? 0
183
+ mat.needsUpdate = true
184
+ }
185
+
186
+ private getCubeShaderMaterial (): THREE.ShaderMaterial | null {
187
+ if (!this.isShaderCubesGpuEnabled()) return null
188
+ if (!this.cubeShaderMaterial) {
189
+ this.cubeShaderMaterial = createCubeBlockMaterial()
190
+ this.syncCubeShaderUniforms()
191
+ }
192
+ return this.cubeShaderMaterial
193
+ }
194
+
195
+ private getGlobalBlockBuffer (): GlobalBlockBuffer | null {
196
+ const mat = this.getCubeShaderMaterial()
197
+ if (!mat) return null
198
+ if (!this.globalBlockBuffer) {
199
+ this.globalBlockBuffer = new GlobalBlockBuffer(mat, this.scene)
200
+ }
201
+ return this.globalBlockBuffer
202
+ }
203
+
204
+ /** Sci-fi reveal needs per-section shader meshes (or no global add) until the first wave completes. */
205
+ private shouldDeferShaderToPerSection (sectionKey: string): boolean {
206
+ const sciFi = this.worldRenderer.getModule<{
207
+ shouldUseRevealEffect?: (key: string) => boolean
208
+ isInInitialRevealCampaign?: () => boolean
209
+ }>('futuristicReveal')
210
+ if (!sciFi) return false
211
+ if (sciFi.isInInitialRevealCampaign?.()) return true
212
+ return sciFi.shouldUseRevealEffect?.(sectionKey) === true
213
+ }
214
+
215
+ /**
216
+ * Move deferred per-section shader cubes into the global buffer after reveal completes.
217
+ */
218
+ migrateDeferredShaderToGlobal (sectionKey: string): void {
219
+ const section = this.sectionObjects[sectionKey]
220
+ if (!section?.deferredShaderCubes) return
221
+
222
+ const { words, count } = section.deferredShaderCubes
223
+ const wx = section.worldX
224
+ const wy = section.worldY
225
+ const wz = section.worldZ
226
+
227
+ if (wx !== undefined && wy !== undefined && wz !== undefined) {
228
+ this.registerShaderSectionRaycastBox(sectionKey, words, count, wx, wy, wz)
229
+ }
230
+
231
+ const global = this.getGlobalBlockBuffer()
232
+ global?.addSection(sectionKey, words, count)
233
+
234
+ const hadShaderAsPrimary = section.mesh === (section.shaderMesh as unknown as THREE.Mesh | undefined)
235
+ if (section.shaderMesh) {
236
+ disposeShaderCubeMesh(section.shaderMesh)
237
+ section.remove(section.shaderMesh)
238
+ section.shaderMesh = undefined
239
+ }
240
+ delete section.deferredShaderCubes
241
+
242
+ if (hadShaderAsPrimary) {
243
+ section.mesh = undefined
244
+ }
245
+ }
246
+
247
+ registerShaderSectionRaycastBox (
248
+ sectionKey: string,
249
+ words: Uint32Array,
250
+ faceCount: number,
251
+ sectionCenterX: number,
252
+ sectionCenterY: number,
253
+ sectionCenterZ: number,
254
+ ): void {
255
+ const box = computeShaderSectionRaycastAabb(words, faceCount, sectionCenterX, sectionCenterY, sectionCenterZ)
256
+ if (box) {
257
+ this.shaderSectionRaycastBoxes.set(sectionKey, box)
258
+ } else {
259
+ this.shaderSectionRaycastBoxes.delete(sectionKey)
260
+ }
261
+ }
262
+
263
+ unregisterShaderSectionRaycastBox (sectionKey: string): void {
264
+ this.shaderSectionRaycastBoxes.delete(sectionKey)
265
+ }
266
+
267
+ /** Closest hit against registered shader-cube AABBs (world-space ray). */
268
+ raycastShaderSectionAABBs (
269
+ originWorld: THREE.Vector3,
270
+ direction: THREE.Vector3,
271
+ maxDist: number,
272
+ maxCenterDistance = 80,
273
+ ): number | undefined {
274
+ const ox = originWorld.x
275
+ const oy = originWorld.y
276
+ const oz = originWorld.z
277
+ const dx = direction.x
278
+ const dy = direction.y
279
+ const dz = direction.z
280
+ const maxCenterDistSq = maxCenterDistance * maxCenterDistance
281
+
282
+ let closest = maxDist
283
+ let found = false
284
+
285
+ for (const [key, box] of this.shaderSectionRaycastBoxes) {
286
+ const section = this.sectionObjects[key]
287
+ if (section && !section.visible) continue
288
+
289
+ const dcx = box.cx - ox
290
+ const dcy = box.cy - oy
291
+ const dcz = box.cz - oz
292
+ if (dcx * dcx + dcy * dcy + dcz * dcz > maxCenterDistSq) continue
293
+
294
+ const t = raycastAabb(
295
+ ox, oy, oz, dx, dy, dz,
296
+ box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ,
297
+ closest,
298
+ )
299
+ if (t !== undefined && t < closest) {
300
+ closest = t
301
+ found = true
302
+ }
303
+ }
304
+
305
+ return found ? closest : undefined
306
+ }
307
+
141
308
  /**
142
309
  * Update or create a section with new geometry data
143
310
  */
144
311
  updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
312
+ const hasLegacy = geometryData.positions.length > 0
313
+ const shaderData = geometryData.shaderCubes
314
+ const hasShader = this.isShaderCubesGpuEnabled() && (shaderData?.count ?? 0) > 0
315
+
316
+ if (!hasLegacy && !hasShader) {
317
+ this.releaseSection(sectionKey)
318
+ return null
319
+ }
320
+
145
321
  // Remove existing section object from scene if it exists
146
322
  let sectionObject = this.sectionObjects[sectionKey]
147
323
  if (sectionObject) {
148
324
  this.cleanupSection(sectionKey)
149
325
  }
150
326
 
151
- // Get or create mesh from pool
152
- let poolEntry = this.activeSections.get(sectionKey)
153
- if (!poolEntry) {
154
- poolEntry = this.acquireMesh()
327
+ if (!hasLegacy) {
328
+ this.releasePooledMesh(sectionKey)
329
+ }
330
+
331
+ let legacyMesh: THREE.Mesh<THREE.BufferGeometry, THREE.Material> | undefined
332
+ if (hasLegacy) {
333
+ let poolEntry = this.activeSections.get(sectionKey)
155
334
  if (!poolEntry) {
156
- console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
157
- return null
335
+ poolEntry = this.acquireMesh()
336
+ if (!poolEntry) {
337
+ console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
338
+ return null
339
+ }
340
+
341
+ this.activeSections.set(sectionKey, poolEntry)
342
+ poolEntry.sectionKey = sectionKey
158
343
  }
159
344
 
160
- this.activeSections.set(sectionKey, poolEntry)
161
- poolEntry.sectionKey = sectionKey
162
- }
345
+ const { mesh } = poolEntry
163
346
 
164
- const { mesh } = poolEntry
347
+ this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
348
+ this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
349
+ this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
350
+ this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
165
351
 
166
- // Update geometry attributes efficiently
167
- this.updateGeometryAttribute(mesh.geometry, 'position', geometryData.positions, 3)
168
- this.updateGeometryAttribute(mesh.geometry, 'normal', geometryData.normals, 3)
169
- this.updateGeometryAttribute(mesh.geometry, 'color', geometryData.colors, 3)
170
- this.updateGeometryAttribute(mesh.geometry, 'uv', geometryData.uvs, 2)
352
+ mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
171
353
 
172
- // Use direct index assignment for better performance (like before)
173
- mesh.geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
354
+ mesh.geometry.boundingBox = new THREE.Box3(
355
+ new THREE.Vector3(-8, -8, -8),
356
+ new THREE.Vector3(8, 8, 8)
357
+ )
358
+ mesh.geometry.boundingSphere = new THREE.Sphere(
359
+ new THREE.Vector3(0, 0, 0),
360
+ Math.sqrt(3 * 8 ** 2)
361
+ )
174
362
 
175
- // Set bounding box and sphere for the 16x16x16 section
176
- mesh.geometry.boundingBox = new THREE.Box3(
177
- new THREE.Vector3(-8, -8, -8),
178
- new THREE.Vector3(8, 8, 8)
179
- )
180
- mesh.geometry.boundingSphere = new THREE.Sphere(
181
- new THREE.Vector3(0, 0, 0),
182
- Math.sqrt(3 * 8 ** 2)
183
- )
363
+ this.worldRenderer.sceneOrigin.track(mesh, { updateMatrix: true })
364
+ mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
365
+ mesh.updateMatrix()
366
+ mesh.visible = true
367
+ mesh.name = 'mesh'
184
368
 
185
- // Position the mesh
186
- this.worldRenderer.sceneOrigin.track(mesh, { updateMatrix: true })
187
- mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
188
- mesh.updateMatrix()
189
- mesh.visible = true
190
- mesh.name = 'mesh'
369
+ poolEntry.lastUsedTime = performance.now()
370
+ legacyMesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.Material>
371
+ }
191
372
 
192
- poolEntry.lastUsedTime = performance.now()
373
+ const cubeMaterial = hasShader ? this.getCubeShaderMaterial() : null
374
+ let shaderMesh: THREE.Mesh<THREE.InstancedBufferGeometry, THREE.ShaderMaterial> | undefined
375
+ const deferShader = hasShader && this.shouldDeferShaderToPerSection(sectionKey)
376
+ if (hasShader && shaderData) {
377
+ if (deferShader && cubeMaterial) {
378
+ shaderMesh = createShaderCubeMesh(shaderData, cubeMaterial)
379
+ shaderMesh.visible = true
380
+ } else {
381
+ this.getGlobalBlockBuffer()?.addSection(sectionKey, shaderData.words, shaderData.count)
382
+ }
383
+ }
193
384
 
194
- // Create or update the section object container
195
385
  sectionObject = new THREE.Group() as SectionObject
196
- sectionObject.add(mesh)
197
- sectionObject.mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
386
+ if (legacyMesh) {
387
+ sectionObject.add(legacyMesh)
388
+ sectionObject.mesh = legacyMesh
389
+ }
390
+ if (shaderMesh) {
391
+ sectionObject.add(shaderMesh)
392
+ sectionObject.shaderMesh = shaderMesh
393
+ if (shaderData) {
394
+ sectionObject.deferredShaderCubes = {
395
+ words: shaderData.words,
396
+ count: shaderData.count,
397
+ }
398
+ }
399
+ if (!sectionObject.mesh) {
400
+ sectionObject.mesh = shaderMesh as unknown as THREE.Mesh<THREE.BufferGeometry, THREE.Material>
401
+ }
402
+ }
403
+ if (hasShader && shaderData) {
404
+ this.registerShaderSectionRaycastBox(
405
+ sectionKey,
406
+ shaderData.words,
407
+ shaderData.count,
408
+ geometryData.sx,
409
+ geometryData.sy,
410
+ geometryData.sz,
411
+ )
412
+ }
198
413
 
199
- // Store metadata
200
- sectionObject.tilesCount = geometryData.positions.length / 3 / 4
414
+ let tilesCount = 0
415
+ if (hasLegacy) {
416
+ tilesCount += geometryData.positions.length / 3 / 4
417
+ }
418
+ if (hasShader && shaderData) {
419
+ tilesCount += shaderData.count
420
+ }
421
+ sectionObject.tilesCount = tilesCount
201
422
  sectionObject.blocksCount = geometryData.blocksCount
202
423
  sectionObject.worldX = geometryData.sx
203
424
  sectionObject.worldY = geometryData.sy
@@ -495,6 +716,13 @@ export class ChunkMeshManager {
495
716
  })
496
717
  this.disposeContainer(sectionObject.bannersContainer)
497
718
  }
719
+ this.globalBlockBuffer?.removeSection(sectionKey)
720
+ this.unregisterShaderSectionRaycastBox(sectionKey)
721
+ if (sectionObject.shaderMesh) {
722
+ disposeShaderCubeMesh(sectionObject.shaderMesh)
723
+ sectionObject.shaderMesh = undefined
724
+ }
725
+ delete sectionObject.deferredShaderCubes
498
726
  // Dispose signs and heads containers
499
727
  if (sectionObject.signsContainer) {
500
728
  this.disposeContainer(sectionObject.signsContainer)
@@ -526,6 +754,19 @@ export class ChunkMeshManager {
526
754
  /**
527
755
  * Release a section and return its mesh to the pool
528
756
  */
757
+ private releasePooledMesh (sectionKey: string): void {
758
+ const poolEntry = this.activeSections.get(sectionKey)
759
+ if (!poolEntry) return
760
+
761
+ poolEntry.mesh.visible = false
762
+ poolEntry.inUse = false
763
+ poolEntry.sectionKey = undefined
764
+ poolEntry.lastUsedTime = 0
765
+ this.clearGeometry(poolEntry.mesh.geometry)
766
+ this.activeSections.delete(sectionKey)
767
+ this.cleanupExcessMeshes()
768
+ }
769
+
529
770
  releaseSection (sectionKey: string): boolean {
530
771
  this.cleanupSection(sectionKey)
531
772
 
@@ -691,6 +932,32 @@ export class ChunkMeshManager {
691
932
  let colorBytes = 0
692
933
  let uvBytes = 0
693
934
  let indexBytes = 0
935
+ let shaderInstanceBytes = 0
936
+
937
+ const globalGeom = this.globalBlockBuffer?.mesh.geometry
938
+ if (globalGeom) {
939
+ for (const name of ['a_w0', 'a_w1', 'a_w2', 'a_w3'] as const) {
940
+ const attr = globalGeom.getAttribute(name)
941
+ if (attr) {
942
+ const bytes = attr.array.byteLength
943
+ shaderInstanceBytes += bytes
944
+ totalBytes += bytes
945
+ }
946
+ }
947
+ }
948
+
949
+ for (const sectionObject of Object.values(this.sectionObjects)) {
950
+ const geom = sectionObject.shaderMesh?.geometry
951
+ if (!geom) continue
952
+ for (const name of ['a_w0', 'a_w1', 'a_w2', 'a_w3'] as const) {
953
+ const attr = geom.getAttribute(name)
954
+ if (attr) {
955
+ const bytes = attr.array.byteLength
956
+ shaderInstanceBytes += bytes
957
+ totalBytes += bytes
958
+ }
959
+ }
960
+ }
694
961
 
695
962
  for (const poolEntry of this.meshPool) {
696
963
  if (poolEntry.inUse && poolEntry.mesh.geometry) {
@@ -742,6 +1009,7 @@ export class ChunkMeshManager {
742
1009
  color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
743
1010
  uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
744
1011
  index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
1012
+ shaderInstances: `${(shaderInstanceBytes / (1024 * 1024)).toFixed(2)} MB`,
745
1013
  }
746
1014
  }
747
1015
  }
@@ -767,6 +1035,11 @@ export class ChunkMeshManager {
767
1035
  this.meshPool.length = 0
768
1036
  this.activeSections.clear()
769
1037
  this.chunkBoxMaterial.dispose()
1038
+ this.shaderSectionRaycastBoxes.clear()
1039
+ this.globalBlockBuffer?.dispose()
1040
+ this.globalBlockBuffer = null
1041
+ this.cubeShaderMaterial?.dispose()
1042
+ this.cubeShaderMaterial = null
770
1043
  // Drop any pending near-first reveal state and cancel safety timers.
771
1044
  this.pendingNearReveal.clear()
772
1045
  for (const timer of this.nearRevealTimers.values()) clearTimeout(timer)