minecraft-renderer 0.1.78 → 0.1.80

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 (200) hide show
  1. package/README.md +29 -25
  2. package/dist/mesher.js +36 -36
  3. package/dist/mesher.js.map +4 -4
  4. package/dist/mesherWasm.js +217 -217
  5. package/dist/minecraft-renderer.js +32 -32
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +415 -415
  8. package/package.json +5 -2
  9. package/src/bundler/bundlePrepare.ts +3 -8
  10. package/src/graphicsBackend/appViewer.ts +10 -7
  11. package/src/graphicsBackend/config.ts +1 -5
  12. package/src/graphicsBackend/playerState.ts +35 -34
  13. package/src/graphicsBackend/preloadWorkers.ts +43 -41
  14. package/src/graphicsBackend/rendererDefaultOptions.ts +68 -70
  15. package/src/graphicsBackend/rendererOptionsSync.ts +24 -50
  16. package/src/graphicsBackend/types.ts +2 -2
  17. package/src/index.ts +6 -27
  18. package/src/lib/DebugGui.ts +34 -31
  19. package/src/lib/animationController.ts +9 -9
  20. package/src/lib/bakeLegacyLight.ts +1 -6
  21. package/src/lib/bindAbortableListener.test.ts +32 -12
  22. package/src/lib/bindAbortableListener.ts +14 -6
  23. package/src/lib/blockEntityLightRegistry.ts +11 -20
  24. package/src/lib/blockEntityLighting.test.ts +1 -6
  25. package/src/lib/blockEntityLighting.ts +5 -5
  26. package/src/lib/buildSharedConfig.mjs +4 -4
  27. package/src/lib/buildWorkerMcDataIndexes.test.ts +4 -4
  28. package/src/lib/buildWorkerMcDataIndexes.ts +11 -19
  29. package/src/lib/cameraBobbing.ts +1 -1
  30. package/src/lib/canvas2DOverlay.example.ts +13 -33
  31. package/src/lib/canvas2DOverlay.quickstart.ts +24 -54
  32. package/src/lib/canvas2DOverlay.ts +28 -47
  33. package/src/lib/cleanupDecorator.ts +2 -2
  34. package/src/lib/createPlayerObject.test.ts +2 -7
  35. package/src/lib/createPlayerObject.ts +18 -25
  36. package/src/lib/frameTimingCollector.ts +2 -5
  37. package/src/lib/guiRenderer.ts +57 -42
  38. package/src/lib/items.ts +15 -9
  39. package/src/lib/mesherlogReader.ts +5 -6
  40. package/src/lib/simpleUtils.ts +5 -5
  41. package/src/lib/skyLight.ts +3 -3
  42. package/src/lib/smoothSwitcher.ts +11 -18
  43. package/src/lib/ui/newStats.ts +10 -12
  44. package/src/lib/utils/proxy.ts +7 -1
  45. package/src/lib/utils/skins.ts +4 -4
  46. package/src/lib/utils.ts +1 -1
  47. package/src/lib/workerMessageSanitize.ts +5 -12
  48. package/src/lib/workerProxy.ts +52 -42
  49. package/src/lib/workerSyncOps.test.ts +58 -41
  50. package/src/lib/worldrendererCommon.reconfigure.test.ts +24 -21
  51. package/src/lib/worldrendererCommon.removeColumn.test.ts +11 -13
  52. package/src/lib/worldrendererCommon.ts +338 -211
  53. package/src/mesher-legacy/mesher.ts +9 -4
  54. package/src/mesher-legacy/test/a.ts +1 -1
  55. package/src/mesher-legacy/test/mesherTester.ts +2 -2
  56. package/src/mesher-legacy/test/playground.ts +1 -1
  57. package/src/mesher-legacy/test/run/test-js.ts +1 -1
  58. package/src/mesher-legacy/test/snapshotUtils.ts +3 -8
  59. package/src/mesher-legacy/test/test-perf.ts +1 -1
  60. package/src/mesher-legacy/test/tests.test.ts +2 -2
  61. package/src/mesher-shared/blockEntityMetadata.test.ts +7 -5
  62. package/src/mesher-shared/blockEntityMetadata.ts +28 -19
  63. package/src/mesher-shared/blockPropertiesForMeshing.ts +2 -2
  64. package/src/mesher-shared/computeHeightmap.ts +2 -4
  65. package/src/mesher-shared/exportedGeometryTypes.ts +3 -3
  66. package/src/mesher-shared/faceOcclusion.ts +324 -0
  67. package/src/mesher-shared/models.ts +80 -131
  68. package/src/mesher-shared/modelsGeometryCommon.ts +30 -15
  69. package/src/mesher-shared/shared.ts +46 -46
  70. package/src/mesher-shared/standaloneRenderer.ts +40 -44
  71. package/src/mesher-shared/tests/faceOcclusion.test.ts +179 -0
  72. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +12 -8
  73. package/src/mesher-shared/vertexShading.ts +2 -11
  74. package/src/mesher-shared/world.ts +19 -19
  75. package/src/mesher-shared/worldLighting.test.ts +2 -2
  76. package/src/performanceMonitor/PerformanceMonitor.ts +4 -13
  77. package/src/performanceMonitor/formatPerformanceFactorsDebug.ts +2 -2
  78. package/src/performanceMonitor/index.ts +1 -5
  79. package/src/performanceMonitor/types.ts +1 -1
  80. package/src/playerState/playerState.ts +55 -50
  81. package/src/playerState/types.ts +2 -8
  82. package/src/playground/allEntitiesDebug.ts +19 -16
  83. package/src/playground/baseScene.ts +29 -32
  84. package/src/playground/mobileControls.tsx +133 -129
  85. package/src/playground/playground.html +76 -73
  86. package/src/playground/playground.ts +14 -1
  87. package/src/playground/playgroundUi.tsx +100 -74
  88. package/src/playground/reactUtils.ts +3 -7
  89. package/src/playground/scenes/entities.ts +10 -7
  90. package/src/playground/scenes/frequentUpdates.ts +5 -5
  91. package/src/playground/scenes/geometryExport.ts +6 -20
  92. package/src/playground/scenes/highPosEntityUpdates.ts +2 -10
  93. package/src/playground/scenes/index.ts +1 -1
  94. package/src/playground/scenes/lightingStarfield.ts +18 -12
  95. package/src/playground/scenes/main.ts +15 -11
  96. package/src/playground/scenes/partialBlockCulling.ts +31 -0
  97. package/src/playground/scenes/railsCobweb.ts +1 -1
  98. package/src/playground/scenes/rotationIssue.ts +1 -1
  99. package/src/playground/scenes/slabsOptimization.ts +1 -2
  100. package/src/playground/scenes/transparencyIssue.ts +1 -1
  101. package/src/playground/shared.ts +2 -2
  102. package/src/resourcesManager/resourcesManager.ts +29 -54
  103. package/src/resourcesManager/resourcesManager.worker.test.ts +6 -10
  104. package/src/shims/minecraftData.ts +70 -24
  105. package/src/sign-renderer/index.html +10 -12
  106. package/src/sign-renderer/index.ts +29 -29
  107. package/src/sign-renderer/playground.ts +22 -15
  108. package/src/sign-renderer/tests.test.ts +16 -13
  109. package/src/sign-renderer/vite.config.ts +2 -2
  110. package/src/three/appShared.ts +46 -25
  111. package/src/three/bannerRenderer.ts +33 -61
  112. package/src/three/cameraShake.ts +15 -6
  113. package/src/three/chunkMeshManager.ts +329 -301
  114. package/src/three/cinimaticScript.ts +26 -15
  115. package/src/three/cubeDrawSpans.ts +27 -37
  116. package/src/three/cubeMultiDraw.ts +9 -22
  117. package/src/three/documentRenderer.ts +15 -18
  118. package/src/three/entities.ts +175 -150
  119. package/src/three/entity/EntityMesh.ts +114 -108
  120. package/src/three/entity/animations.js +3 -3
  121. package/src/three/entity/armorModels.ts +15 -15
  122. package/src/three/entity/gltfAnimationUtils.ts +2 -5
  123. package/src/three/fireworks.ts +34 -32
  124. package/src/three/fireworksRenderer.ts +32 -29
  125. package/src/three/globalBlockBuffer.ts +174 -99
  126. package/src/three/globalLegacyBuffer.ts +465 -136
  127. package/src/three/graphicsBackendBase.ts +19 -25
  128. package/src/three/graphicsBackendOffThread.ts +12 -15
  129. package/src/three/hand.ts +21 -21
  130. package/src/three/handLegacy.ts +3 -13
  131. package/src/three/holdingBlock.ts +116 -79
  132. package/src/three/holdingBlockFactory.ts +1 -4
  133. package/src/three/holdingBlockItemIdentity.test.ts +25 -17
  134. package/src/three/holdingBlockItemIdentity.ts +9 -6
  135. package/src/three/holdingBlockLegacy.ts +113 -132
  136. package/src/three/itemMesh.ts +20 -29
  137. package/src/three/legacyMultiDraw.ts +93 -0
  138. package/src/three/legacySectionCull.ts +8 -14
  139. package/src/three/menuBackground/activeView.ts +1 -4
  140. package/src/three/menuBackground/assetUrl.ts +1 -1
  141. package/src/three/menuBackground/classic.ts +4 -12
  142. package/src/three/menuBackground/gpuPreference.ts +1 -3
  143. package/src/three/menuBackground/index.ts +2 -11
  144. package/src/three/menuBackground/renderer.ts +1 -5
  145. package/src/three/menuBackground/types.ts +1 -4
  146. package/src/three/menuBackground/v2.ts +196 -96
  147. package/src/three/menuBackground/v2Meta.ts +3 -3
  148. package/src/three/menuBackground/worldBlocks.ts +37 -35
  149. package/src/three/modules/blockBreakParticles.ts +59 -28
  150. package/src/three/modules/cameraBobbing.ts +3 -6
  151. package/src/three/modules/index.ts +1 -1
  152. package/src/three/modules/rain.ts +10 -23
  153. package/src/three/modules/sciFiWorldReveal.ts +11 -25
  154. package/src/three/modules/starfield.ts +7 -13
  155. package/src/three/renderSlot.ts +28 -16
  156. package/src/three/rendererModuleSystem.ts +1 -3
  157. package/src/three/sceneOrigin.ts +96 -33
  158. package/src/three/sectionRaycastAabb.ts +30 -18
  159. package/src/three/shaderCubeMesh.ts +6 -17
  160. package/src/three/shaders/cubeBlockShader.ts +85 -96
  161. package/src/three/shaders/legacyBlockShader.ts +21 -43
  162. package/src/three/shaders/textureIndexMapping.ts +89 -95
  163. package/src/three/shaders/tintPalette.ts +156 -170
  164. package/src/three/signTextureCache.ts +9 -12
  165. package/src/three/skyboxRenderer.ts +62 -70
  166. package/src/three/tests/chunkMeshManagerLegacy.test.ts +133 -25
  167. package/src/three/tests/cubeDrawSpans.test.ts +55 -25
  168. package/src/three/tests/globalBlockBuffer.test.ts +217 -0
  169. package/src/three/tests/globalLegacyBuffer.test.ts +460 -58
  170. package/src/three/tests/legacyMultiDraw.test.ts +88 -0
  171. package/src/three/tests/pr3PerFrameCpuCuts.test.ts +370 -0
  172. package/src/three/tests/signTextureCache.test.ts +4 -4
  173. package/src/three/threeJsMedia.ts +55 -72
  174. package/src/three/threeJsParticles.ts +32 -41
  175. package/src/three/threeJsSound.ts +3 -3
  176. package/src/three/threeJsUtils.ts +3 -3
  177. package/src/three/threeWorker.ts +2 -2
  178. package/src/three/waypointSprite.ts +55 -82
  179. package/src/three/waypoints.ts +20 -23
  180. package/src/three/world/cursorBlock.ts +25 -12
  181. package/src/three/world/vr.ts +11 -17
  182. package/src/three/worldGeometryExport.ts +15 -34
  183. package/src/three/worldRendererThree.ts +131 -133
  184. package/src/wasm-mesher/bridge/convertChunk.ts +3 -9
  185. package/src/wasm-mesher/bridge/render-from-wasm.ts +204 -229
  186. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +26 -67
  187. package/src/wasm-mesher/tests/cullingRegression.test.ts +307 -0
  188. package/src/wasm-mesher/tests/heightmapParity.test.ts +8 -18
  189. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +2 -2
  190. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +17 -4
  191. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +103 -141
  192. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +14 -22
  193. package/src/wasm-mesher/worker/mesherWasm.ts +150 -141
  194. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +2 -8
  195. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +1 -3
  196. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +1 -5
  197. package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +42 -42
  198. package/src/worldView/worldView.spiral.test.ts +1 -1
  199. package/src/worldView/worldView.ts +34 -39
  200. package/src/worldView/worldViewWorkerBridge.test.ts +4 -4
@@ -4,8 +4,24 @@ import * as nbt from 'prismarine-nbt'
4
4
  import { Vec3 } from 'vec3'
5
5
  import { MesherGeometryOutput } from '../mesher-shared/shared'
6
6
  import { getShaderCubeResources, SHADER_CUBES_WORDS_PER_FACE } from '../wasm-mesher/bridge/shaderCubeBridge'
7
- import { createCubeBlockMaterial, computeSectionOriginRel, setCubeSkyLevel, setCubeShadingTheme, setCubeLightmapParams, type BlockLightmapParams } from './shaders/cubeBlockShader'
8
- import { computeCameraRelativeUniforms, createGlobalLegacyBlendMaterial, createGlobalLegacyBlockMaterial, createLegacyBlockMaterial, setLegacyCameraOrigin, setLegacySkyLevel, setLegacyLightmapParams, type RenderOrigin } from './shaders/legacyBlockShader'
7
+ import {
8
+ createCubeBlockMaterial,
9
+ computeSectionOriginRel,
10
+ setCubeSkyLevel,
11
+ setCubeShadingTheme,
12
+ setCubeLightmapParams,
13
+ type BlockLightmapParams
14
+ } from './shaders/cubeBlockShader'
15
+ import {
16
+ computeCameraRelativeUniforms,
17
+ createGlobalLegacyBlendMaterial,
18
+ createGlobalLegacyBlockMaterial,
19
+ createLegacyBlockMaterial,
20
+ setLegacyCameraOrigin,
21
+ setLegacySkyLevel,
22
+ setLegacyLightmapParams,
23
+ type RenderOrigin
24
+ } from './shaders/legacyBlockShader'
9
25
  import { LEGACY_SECTION_HALF_EXTENT, sectionIntersectsFrustum, setupLegacySectionMatrix, updateLegacySectionCullState } from './legacySectionCull'
10
26
  import { createShaderCubeMesh, disposeShaderCubeMesh } from './shaderCubeMesh'
11
27
  import { GlobalBlockBuffer } from './globalBlockBuffer'
@@ -17,7 +33,7 @@ import {
17
33
  raycastAabb,
18
34
  raycastShaderBlocksAabb,
19
35
  sectionAabbIntersectsRay,
20
- type ShaderSectionRaycastEntry,
36
+ type ShaderSectionRaycastEntry
21
37
  } from './sectionRaycastAabb'
22
38
  import { getMesh } from './entity/EntityMesh'
23
39
  import type { WorldRendererThree } from './worldRendererThree'
@@ -39,7 +55,7 @@ export interface SectionObject extends THREE.Group {
39
55
  /** Per-section instanced shader mesh (sci-fi reveal defer only). */
40
56
  shaderMesh?: THREE.Mesh<THREE.InstancedBufferGeometry, THREE.ShaderMaterial>
41
57
  /** Shader cube words kept for migration to global buffer after reveal. */
42
- deferredShaderCubes?: { words: Uint32Array, count: number }
58
+ deferredShaderCubes?: { words: Uint32Array; count: number }
43
59
  /** Opaque legacy geometry deferred from global buffer during sci-fi reveal. */
44
60
  deferredLegacyOpaque?: LegacySectionGeometry
45
61
  /** Blend legacy geometry deferred from global buffer during sci-fi reveal. */
@@ -139,7 +155,11 @@ export class ChunkMeshManager {
139
155
  private readonly _legacyCullBox = new THREE.Box3()
140
156
  private readonly _legacyCullBoxMin = new THREE.Vector3()
141
157
  private readonly _legacyCullBoxMax = new THREE.Vector3()
142
- private readonly _visibleSectionSpans: Array<{ key: string, distSq: number }> = []
158
+ private readonly _visibleSectionSpans: Array<{ key: string; distSq: number }> = []
159
+ /** Sections with geometry in global legacy opaque and/or blend buffers — cull/raycast scan only these. */
160
+ private readonly legacyCullSections = new Map<string, { worldX: number; worldY: number; worldZ: number }>()
161
+ private _lastCullFingerprint = ''
162
+ private lastBufferStateKey = ''
143
163
  /** Drives per-frame cull + span rebuild; cleared after updateSectionCullAndSort. */
144
164
  cullDirty = true
145
165
  private readonly _lastCullCamPos = new THREE.Vector3()
@@ -170,20 +190,20 @@ export class ChunkMeshManager {
170
190
  private lastPerformanceCheck = 0
171
191
  private readonly performanceCheckInterval = 2000 // Check every 2 seconds
172
192
 
173
- get performanceOverrideDistance () {
193
+ get performanceOverrideDistance() {
174
194
  return this._performanceOverrideDistance ?? 0
175
195
  }
176
- set performanceOverrideDistance (value: number | undefined) {
196
+ set performanceOverrideDistance(value: number | undefined) {
177
197
  this._performanceOverrideDistance = value
178
198
  this.updateSectionsVisibility()
179
199
  }
180
200
 
181
- constructor (
201
+ constructor(
182
202
  public worldRenderer: WorldRendererThree,
183
203
  public scene: THREE.Object3D,
184
204
  public material: THREE.Material,
185
205
  public worldHeight: number,
186
- viewDistance = 3,
206
+ viewDistance = 3
187
207
  ) {
188
208
  this.updateViewDistance(viewDistance)
189
209
  this.signHeadsRenderer = new SignHeadsRenderer(worldRenderer)
@@ -191,7 +211,7 @@ export class ChunkMeshManager {
191
211
  this.initializePool()
192
212
  }
193
213
 
194
- private initializePool () {
214
+ private initializePool() {
195
215
  // Create initial pool
196
216
  for (let i = 0; i < this.poolSize; i++) {
197
217
  const geometry = new THREE.BufferGeometry()
@@ -212,18 +232,18 @@ export class ChunkMeshManager {
212
232
  }
213
233
 
214
234
  /** True when section has legacy vertices and/or GPU shader cube instances. */
215
- sectionHasRenderableContent (geometryData: MesherGeometryOutput): boolean {
235
+ sectionHasRenderableContent(geometryData: MesherGeometryOutput): boolean {
216
236
  if (geometryData.positions.length > 0) return true
217
237
  if ((geometryData.blend?.positions.length ?? 0) > 0) return true
218
238
  if (!this.isShaderCubesGpuEnabled()) return false
219
239
  return (geometryData.shaderCubes?.count ?? 0) > 0
220
240
  }
221
241
 
222
- isShaderCubesGpuEnabled (): boolean {
242
+ isShaderCubesGpuEnabled(): boolean {
223
243
  return this.worldRenderer.shaderCubeBlocksEnabled()
224
244
  }
225
245
 
226
- syncCubeShaderUniforms (): void {
246
+ syncCubeShaderUniforms(): void {
227
247
  if (!this.isShaderCubesGpuEnabled()) return
228
248
  const mat = this.cubeShaderMaterial ?? this.getCubeShaderMaterial()
229
249
  if (!mat) return
@@ -240,7 +260,7 @@ export class ChunkMeshManager {
240
260
  mat.needsUpdate = true
241
261
  }
242
262
 
243
- syncLegacyShaderUniforms (): void {
263
+ syncLegacyShaderUniforms(): void {
244
264
  const atlas = (this.material as THREE.MeshBasicMaterial).map ?? null
245
265
  if (this.legacyShaderMaterial) {
246
266
  this.legacyShaderMaterial.uniforms.u_atlas.value = atlas
@@ -257,7 +277,7 @@ export class ChunkMeshManager {
257
277
  }
258
278
 
259
279
  /** Render-time sky light cap (0–1, from time-of-day / 15). */
260
- setSkyLevel (value: number): void {
280
+ setSkyLevel(value: number): void {
261
281
  const cube = this.cubeShaderMaterial ?? (this.isShaderCubesGpuEnabled() ? this.getCubeShaderMaterial() : null)
262
282
  if (cube) setCubeSkyLevel(cube, value)
263
283
  if (this.legacyShaderMaterial) setLegacySkyLevel(this.legacyShaderMaterial, value)
@@ -266,13 +286,13 @@ export class ChunkMeshManager {
266
286
  this.blockEntityLightRegistry.setSkyLevel(value)
267
287
  }
268
288
 
269
- setShadingTheme (theme: 'vanilla' | 'high-contrast', cardinalLight: string): void {
289
+ setShadingTheme(theme: 'vanilla' | 'high-contrast', cardinalLight: string): void {
270
290
  const cube = this.cubeShaderMaterial ?? (this.isShaderCubesGpuEnabled() ? this.getCubeShaderMaterial() : null)
271
291
  if (cube) setCubeShadingTheme(cube, theme, cardinalLight)
272
292
  }
273
293
 
274
294
  /** Vanilla-like lightmap curve params (live tuning via window.setBlockLightmap). */
275
- setBlockLightmapParams (params: BlockLightmapParams): void {
295
+ setBlockLightmapParams(params: BlockLightmapParams): void {
276
296
  const cube = this.cubeShaderMaterial ?? (this.isShaderCubesGpuEnabled() ? this.getCubeShaderMaterial() : null)
277
297
  if (cube) setCubeLightmapParams(cube, params)
278
298
  if (this.legacyShaderMaterial) setLegacyLightmapParams(this.legacyShaderMaterial, params)
@@ -281,7 +301,7 @@ export class ChunkMeshManager {
281
301
  this.blockEntityLightRegistry.setLightmapParams(params)
282
302
  }
283
303
 
284
- private getLegacyShaderMaterial (): THREE.ShaderMaterial {
304
+ private getLegacyShaderMaterial(): THREE.ShaderMaterial {
285
305
  if (!this.legacyShaderMaterial) {
286
306
  this.legacyShaderMaterial = createLegacyBlockMaterial()
287
307
  this.syncLegacyShaderUniforms()
@@ -289,7 +309,7 @@ export class ChunkMeshManager {
289
309
  return this.legacyShaderMaterial
290
310
  }
291
311
 
292
- private getGlobalLegacyShaderMaterial (): THREE.ShaderMaterial {
312
+ private getGlobalLegacyShaderMaterial(): THREE.ShaderMaterial {
293
313
  if (!this.globalLegacyShaderMaterial) {
294
314
  this.globalLegacyShaderMaterial = createGlobalLegacyBlockMaterial()
295
315
  this.syncLegacyShaderUniforms()
@@ -297,18 +317,15 @@ export class ChunkMeshManager {
297
317
  return this.globalLegacyShaderMaterial
298
318
  }
299
319
 
300
- private getGlobalLegacyBuffer (): GlobalLegacyBuffer {
320
+ private getGlobalLegacyBuffer(): GlobalLegacyBuffer {
301
321
  if (!this.globalLegacyBuffer) {
302
- this.globalLegacyBuffer = new GlobalLegacyBuffer(
303
- this.getGlobalLegacyShaderMaterial(),
304
- this.scene,
305
- )
322
+ this.globalLegacyBuffer = new GlobalLegacyBuffer(this.getGlobalLegacyShaderMaterial(), this.scene)
306
323
  this.globalLegacyBuffer.setRenderOrigin(this.renderOrigin)
307
324
  }
308
325
  return this.globalLegacyBuffer
309
326
  }
310
327
 
311
- private getGlobalLegacyBlendShaderMaterial (): THREE.ShaderMaterial {
328
+ private getGlobalLegacyBlendShaderMaterial(): THREE.ShaderMaterial {
312
329
  if (!this.globalLegacyBlendShaderMaterial) {
313
330
  this.globalLegacyBlendShaderMaterial = createGlobalLegacyBlendMaterial()
314
331
  this.syncLegacyShaderUniforms()
@@ -316,32 +333,28 @@ export class ChunkMeshManager {
316
333
  return this.globalLegacyBlendShaderMaterial
317
334
  }
318
335
 
319
- private getGlobalLegacyBlendBuffer (): GlobalLegacyBuffer {
336
+ private getGlobalLegacyBlendBuffer(): GlobalLegacyBuffer {
320
337
  if (!this.globalLegacyBlendBuffer) {
321
- this.globalLegacyBlendBuffer = new GlobalLegacyBuffer(
322
- this.getGlobalLegacyBlendShaderMaterial(),
323
- this.scene,
324
- {
325
- name: 'globalLegacyBlend',
326
- initialCapacityQuads: 32_000,
327
- growthIncrementQuads: 32_000,
328
- },
329
- )
338
+ this.globalLegacyBlendBuffer = new GlobalLegacyBuffer(this.getGlobalLegacyBlendShaderMaterial(), this.scene, {
339
+ name: 'globalLegacyBlend',
340
+ initialCapacityQuads: 32_000,
341
+ growthIncrementQuads: 32_000
342
+ })
330
343
  this.globalLegacyBlendBuffer.setRenderOrigin(this.renderOrigin)
331
344
  }
332
345
  return this.globalLegacyBlendBuffer
333
346
  }
334
347
 
335
- getRenderOrigin (): Readonly<RenderOrigin> {
348
+ getRenderOrigin(): Readonly<RenderOrigin> {
336
349
  return this.renderOrigin
337
350
  }
338
351
 
339
- maybeRebase (camera: RenderOrigin): void {
352
+ maybeRebase(camera: RenderOrigin): void {
340
353
  const R = this.renderOrigin
341
354
  if (
342
- Math.abs(camera.x - R.x) <= ChunkMeshManager.REBASE_THRESHOLD
343
- && Math.abs(camera.y - R.y) <= ChunkMeshManager.REBASE_THRESHOLD
344
- && Math.abs(camera.z - R.z) <= ChunkMeshManager.REBASE_THRESHOLD
355
+ Math.abs(camera.x - R.x) <= ChunkMeshManager.REBASE_THRESHOLD &&
356
+ Math.abs(camera.y - R.y) <= ChunkMeshManager.REBASE_THRESHOLD &&
357
+ Math.abs(camera.z - R.z) <= ChunkMeshManager.REBASE_THRESHOLD
345
358
  ) {
346
359
  return
347
360
  }
@@ -349,12 +362,12 @@ export class ChunkMeshManager {
349
362
  const newOrigin: RenderOrigin = {
350
363
  x: Math.round(camera.x / 16) * 16,
351
364
  y: Math.round(camera.y / 16) * 16,
352
- z: Math.round(camera.z / 16) * 16,
365
+ z: Math.round(camera.z / 16) * 16
353
366
  }
354
367
  const delta: RenderOrigin = {
355
368
  x: newOrigin.x - R.x,
356
369
  y: newOrigin.y - R.y,
357
- z: newOrigin.z - R.z,
370
+ z: newOrigin.z - R.z
358
371
  }
359
372
 
360
373
  this.globalLegacyBuffer?.rebase(delta)
@@ -365,13 +378,7 @@ export class ChunkMeshManager {
365
378
  if (!sectionKey) continue
366
379
  const sectionObject = this.sectionObjects[sectionKey]
367
380
  if (!sectionObject) continue
368
- setupLegacySectionMatrix(
369
- poolEntry.mesh,
370
- sectionObject.worldX ?? 0,
371
- sectionObject.worldY ?? 0,
372
- sectionObject.worldZ ?? 0,
373
- newOrigin,
374
- )
381
+ setupLegacySectionMatrix(poolEntry.mesh, sectionObject.worldX ?? 0, sectionObject.worldY ?? 0, sectionObject.worldZ ?? 0, newOrigin)
375
382
  }
376
383
 
377
384
  this.renderOrigin = newOrigin
@@ -380,22 +387,58 @@ export class ChunkMeshManager {
380
387
  }
381
388
 
382
389
  /** Whether a section still holds a pooled legacy mesh (defer / invariant fallback). */
383
- sectionUsesPooledLegacyMesh (sectionKey: string): boolean {
390
+ sectionUsesPooledLegacyMesh(sectionKey: string): boolean {
384
391
  return this.activeSections.has(sectionKey)
385
392
  }
386
393
 
394
+ private registerLegacyCullSection(sectionKey: string, worldX: number, worldY: number, worldZ: number): void {
395
+ this.legacyCullSections.set(sectionKey, { worldX, worldY, worldZ })
396
+ }
397
+
398
+ private maybeUnregisterLegacyCullSection(sectionKey: string): void {
399
+ const inOpaque = this.globalLegacyBuffer?.hasSection(sectionKey) ?? false
400
+ const inBlend = this.globalLegacyBlendBuffer?.hasSection(sectionKey) ?? false
401
+ if (!inOpaque && !inBlend) {
402
+ this.legacyCullSections.delete(sectionKey)
403
+ }
404
+ }
405
+
406
+ private updatePooledLegacyCullState(cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number): void {
407
+ for (const poolEntry of this.activeSections.values()) {
408
+ const sectionKey = poolEntry.sectionKey
409
+ if (!sectionKey) continue
410
+ const sectionObject = this.sectionObjects[sectionKey]
411
+ if (!sectionObject) continue
412
+
413
+ updateLegacySectionCullState(
414
+ poolEntry.mesh,
415
+ sectionObject.worldX ?? 0,
416
+ sectionObject.worldY ?? 0,
417
+ sectionObject.worldZ ?? 0,
418
+ cameraWorldX,
419
+ cameraWorldY,
420
+ cameraWorldZ,
421
+ this._legacyCullFrustum,
422
+ this._legacyCullBox,
423
+ this._legacyCullBoxMin,
424
+ this._legacyCullBoxMax
425
+ )
426
+ }
427
+ }
428
+
387
429
  /**
388
430
  * Shared section visibility + span groups for global legacy and cube buffers.
389
431
  */
390
- updateSectionCullAndSort (camera: THREE.Camera, cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number): void {
432
+ updateSectionCullAndSort(camera: THREE.Camera, cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number): void {
391
433
  this._legacyCullProjScreen.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
392
434
  this._legacyCullFrustum.setFromProjectionMatrix(this._legacyCullProjScreen)
393
435
 
394
436
  const visible = this._visibleSectionSpans
395
437
  visible.length = 0
396
438
 
397
- for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
398
- if (sectionObject.worldX === undefined || !sectionObject.visible) continue
439
+ for (const [sectionKey] of this.legacyCullSections) {
440
+ const sectionObject = this.sectionObjects[sectionKey]
441
+ if (!sectionObject?.visible || sectionObject.worldX === undefined) continue
399
442
  const { visible: inFrustum, distSq } = sectionIntersectsFrustum(
400
443
  sectionObject.worldX,
401
444
  sectionObject.worldY ?? 0,
@@ -406,20 +449,34 @@ export class ChunkMeshManager {
406
449
  this._legacyCullFrustum,
407
450
  this._legacyCullBox,
408
451
  this._legacyCullBoxMin,
409
- this._legacyCullBoxMax,
452
+ this._legacyCullBoxMax
410
453
  )
411
454
  if (inFrustum) {
412
455
  visible.push({ key: sectionKey, distSq })
413
456
  }
414
457
  }
415
458
 
416
- this.globalLegacyBuffer?.updateDrawSpans(visible, 'opaque')
417
- this.globalLegacyBlendBuffer?.updateDrawSpans(visible, 'sortedBlend')
418
-
459
+ const opaqueBuf = this.globalLegacyBuffer
460
+ const blendBuf = this.globalLegacyBlendBuffer
419
461
  const gb = this.globalBlockBuffer
462
+
463
+ const opaqueKeys: string[] = []
464
+ for (const entry of visible) {
465
+ if (opaqueBuf?.hasSection(entry.key)) opaqueKeys.push(entry.key)
466
+ }
467
+ opaqueKeys.sort()
468
+
469
+ const blendVisible = visible.filter(v => blendBuf?.hasSection(v.key))
470
+ blendVisible.sort((a, b) => b.distSq - a.distSq)
471
+ const blendKeys = blendVisible.map(v => v.key)
472
+
473
+ const cubeVisibleKeys: string[] = []
474
+ const visibleSlots: Array<{ start: number; count: number }> = []
420
475
  if (gb) {
421
- const visibleSlots: Array<{ start: number, count: number }> = []
422
476
  gb.forEachSectionSlot((key, slot) => {
477
+ const sectionObject = this.sectionObjects[key]
478
+ // Keep in sync with legacy gate (line 440).
479
+ if (!sectionObject?.visible) return
423
480
  const entry = this.shaderSectionRaycastBoxes.get(key)
424
481
  if (!entry) {
425
482
  return
@@ -434,48 +491,90 @@ export class ChunkMeshManager {
434
491
  this._legacyCullFrustum,
435
492
  this._legacyCullBox,
436
493
  this._legacyCullBoxMin,
437
- this._legacyCullBoxMax,
494
+ this._legacyCullBoxMax
438
495
  )
439
496
  if (!inFrustum) {
440
497
  return
441
498
  }
499
+ cubeVisibleKeys.push(key)
442
500
  const drawStart = gb.getSectionDrawStart(key)
443
- if (drawStart !== undefined) {
444
- visibleSlots.push({ start: drawStart, count: slot.count })
501
+ const drawCount = gb.getSectionDrawCount(key)
502
+ if (drawStart !== undefined && drawCount !== undefined) {
503
+ visibleSlots.push({ start: drawStart, count: drawCount })
445
504
  }
446
505
  })
447
- const spans = buildVisibleCubeSpans(visibleSlots, gb.getHighWatermark())
448
- gb.setVisibleSpans(spans)
449
506
  }
507
+ cubeVisibleKeys.sort()
508
+
509
+ const fingerprint = [
510
+ opaqueKeys.join(','),
511
+ blendKeys.join(','),
512
+ cubeVisibleKeys.join(','),
513
+ opaqueBuf?.getLayoutVersion() ?? 0,
514
+ blendBuf?.getLayoutVersion() ?? 0,
515
+ gb?.getLayoutVersion() ?? 0,
516
+ opaqueBuf?.getUploadEpoch() ?? 0,
517
+ blendBuf?.getUploadEpoch() ?? 0,
518
+ gb?.getUploadEpoch() ?? 0
519
+ ].join('|')
520
+
521
+ if (fingerprint === this._lastCullFingerprint && !this.hasPendingBufferWork()) {
522
+ this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
523
+ return
524
+ }
525
+ this._lastCullFingerprint = fingerprint
450
526
 
451
- for (const poolEntry of this.activeSections.values()) {
452
- const sectionKey = poolEntry.sectionKey
453
- if (!sectionKey) continue
454
- const sectionObject = this.sectionObjects[sectionKey]
455
- if (!sectionObject) continue
527
+ opaqueBuf?.updateDrawSpans(visible, 'opaque')
528
+ blendBuf?.updateDrawSpans(visible, 'sortedBlend')
456
529
 
457
- updateLegacySectionCullState(
458
- poolEntry.mesh,
459
- sectionObject.worldX ?? 0,
460
- sectionObject.worldY ?? 0,
461
- sectionObject.worldZ ?? 0,
462
- cameraWorldX,
463
- cameraWorldY,
464
- cameraWorldZ,
465
- this._legacyCullFrustum,
466
- this._legacyCullBox,
467
- this._legacyCullBoxMin,
468
- this._legacyCullBoxMax,
530
+ if (gb) {
531
+ const spans = buildVisibleCubeSpans(
532
+ visibleSlots,
533
+ gb.getHighWatermark(),
534
+ gb.canUseFullDrawShortcut(),
535
+ (start, end) => gb.isRangeFullyUploaded(start, end),
536
+ gb.getPendingDirtyRanges()
469
537
  )
538
+ gb.setVisibleSpans(spans)
539
+ }
540
+
541
+ this.lastBufferStateKey = this.bufferStateKey()
542
+ this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
543
+ }
544
+
545
+ private bufferStateKey(): string {
546
+ const b = this.globalBlockBuffer
547
+ const o = this.globalLegacyBuffer
548
+ const bl = this.globalLegacyBlendBuffer
549
+ return [
550
+ o?.getLayoutVersion() ?? 0,
551
+ o?.getUploadEpoch() ?? 0,
552
+ bl?.getLayoutVersion() ?? 0,
553
+ bl?.getUploadEpoch() ?? 0,
554
+ b?.getLayoutVersion() ?? 0,
555
+ b?.getUploadEpoch() ?? 0
556
+ ].join('|')
557
+ }
558
+
559
+ /** Mark cull dirty when any buffer's layout or upload state changed since the last cull. */
560
+ markCullDirtyIfBufferStateChanged(): void {
561
+ const key = this.bufferStateKey()
562
+ if (key !== this.lastBufferStateKey) {
563
+ this.markCullDirty()
470
564
  }
471
565
  }
472
566
 
473
- markCullDirty (): void {
567
+ markCullDirty(): void {
474
568
  this.cullDirty = true
475
569
  }
476
570
 
571
+ hasPendingBufferWork(): boolean {
572
+ const buffers = [this.globalLegacyBuffer, this.globalLegacyBlendBuffer, this.globalBlockBuffer]
573
+ return buffers.some(b => b != null && (b.hasPendingUploads() || b.hasPendingReplace() || b.getPendingMove() != null))
574
+ }
575
+
477
576
  /** Compare camera pose; mark cull dirty when position or rotation changed. */
478
- updateCullDirtyFromCamera (camera: THREE.Camera, cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number): void {
577
+ updateCullDirtyFromCamera(camera: THREE.Camera, cameraWorldX: number, cameraWorldY: number, cameraWorldZ: number): void {
479
578
  camera.getWorldQuaternion(this._cullViewQuat)
480
579
  if (!this._cullCamInitialized) {
481
580
  this._lastCullCamPos.set(cameraWorldX, cameraWorldY, cameraWorldZ)
@@ -484,10 +583,7 @@ export class ChunkMeshManager {
484
583
  this.cullDirty = true
485
584
  return
486
585
  }
487
- const posChanged =
488
- this._lastCullCamPos.x !== cameraWorldX ||
489
- this._lastCullCamPos.y !== cameraWorldY ||
490
- this._lastCullCamPos.z !== cameraWorldZ
586
+ const posChanged = this._lastCullCamPos.x !== cameraWorldX || this._lastCullCamPos.y !== cameraWorldY || this._lastCullCamPos.z !== cameraWorldZ
491
587
  const quatChanged =
492
588
  this._lastCullCamQuat.x !== this._cullViewQuat.x ||
493
589
  this._lastCullCamQuat.y !== this._cullViewQuat.y ||
@@ -500,11 +596,11 @@ export class ChunkMeshManager {
500
596
  }
501
597
  }
502
598
 
503
- clearCullDirty (): void {
599
+ clearCullDirty(): void {
504
600
  this.cullDirty = false
505
601
  }
506
602
 
507
- setLegacyCameraOrigin (x: number, y: number, z: number): void {
603
+ setLegacyCameraOrigin(x: number, y: number, z: number): void {
508
604
  const R = this.renderOrigin
509
605
  setLegacyCameraOrigin(this.getLegacyShaderMaterial(), R, x, y, z)
510
606
  setLegacyCameraOrigin(this.getGlobalLegacyShaderMaterial(), R, x, y, z)
@@ -531,7 +627,7 @@ export class ChunkMeshManager {
531
627
  }
532
628
  }
533
629
 
534
- private getCubeShaderMaterial (): THREE.ShaderMaterial | null {
630
+ private getCubeShaderMaterial(): THREE.ShaderMaterial | null {
535
631
  if (!this.isShaderCubesGpuEnabled()) return null
536
632
  if (!this.cubeShaderMaterial) {
537
633
  this.cubeShaderMaterial = createCubeBlockMaterial()
@@ -542,7 +638,7 @@ export class ChunkMeshManager {
542
638
  return this.cubeShaderMaterial
543
639
  }
544
640
 
545
- private getGlobalBlockBuffer (): GlobalBlockBuffer | null {
641
+ private getGlobalBlockBuffer(): GlobalBlockBuffer | null {
546
642
  const mat = this.getCubeShaderMaterial()
547
643
  if (!mat) return null
548
644
  if (!this.globalBlockBuffer) {
@@ -551,12 +647,12 @@ export class ChunkMeshManager {
551
647
  return this.globalBlockBuffer
552
648
  }
553
649
 
554
- private shouldDeferLegacyOpaqueToPerSection (sectionKey: string): boolean {
650
+ private shouldDeferLegacyOpaqueToPerSection(sectionKey: string): boolean {
555
651
  return this.shouldDeferShaderToPerSection(sectionKey)
556
652
  }
557
653
 
558
654
  /** Sci-fi reveal keeps geometry off global buffers until the section finishes reveal. */
559
- private shouldDeferShaderToPerSection (sectionKey: string): boolean {
655
+ private shouldDeferShaderToPerSection(sectionKey: string): boolean {
560
656
  const sciFi = this.worldRenderer.getModule<{
561
657
  shouldDeferSectionGeometry?: (key: string) => boolean
562
658
  }>('futuristicReveal')
@@ -566,7 +662,7 @@ export class ChunkMeshManager {
566
662
  /**
567
663
  * Move deferred per-section shader cubes into the global buffer after reveal completes.
568
664
  */
569
- migrateDeferredShaderToGlobal (sectionKey: string): void {
665
+ migrateDeferredShaderToGlobal(sectionKey: string): void {
570
666
  const section = this.sectionObjects[sectionKey]
571
667
  if (!section?.deferredShaderCubes) return
572
668
 
@@ -599,7 +695,7 @@ export class ChunkMeshManager {
599
695
  /**
600
696
  * Move deferred per-section opaque legacy into the global buffer after reveal completes.
601
697
  */
602
- migrateDeferredLegacyToGlobal (sectionKey: string): void {
698
+ migrateDeferredLegacyToGlobal(sectionKey: string): void {
603
699
  const section = this.sectionObjects[sectionKey]
604
700
  if (!section) return
605
701
 
@@ -609,13 +705,10 @@ export class ChunkMeshManager {
609
705
  const wy = section.worldY
610
706
  const wz = section.worldZ
611
707
  if (wx !== undefined && wy !== undefined && wz !== undefined) {
612
- this.getGlobalLegacyBuffer().addSection(
613
- sectionKey,
614
- { positions, colors, skyLights, blockLights, uvs, indices },
615
- wx,
616
- wy,
617
- wz,
618
- )
708
+ const added = this.getGlobalLegacyBuffer().addSection(sectionKey, { positions, colors, skyLights, blockLights, uvs, indices }, wx, wy, wz)
709
+ if (added) {
710
+ this.registerLegacyCullSection(sectionKey, wx, wy, wz)
711
+ }
619
712
  }
620
713
  delete section.deferredLegacyOpaque
621
714
  }
@@ -626,13 +719,10 @@ export class ChunkMeshManager {
626
719
  const wy = section.worldY
627
720
  const wz = section.worldZ
628
721
  if (wx !== undefined && wy !== undefined && wz !== undefined) {
629
- this.getGlobalLegacyBlendBuffer().addSection(
630
- sectionKey,
631
- { positions, colors, skyLights, blockLights, uvs, indices },
632
- wx,
633
- wy,
634
- wz,
635
- )
722
+ const added = this.getGlobalLegacyBlendBuffer().addSection(sectionKey, { positions, colors, skyLights, blockLights, uvs, indices }, wx, wy, wz)
723
+ if (added) {
724
+ this.registerLegacyCullSection(sectionKey, wx, wy, wz)
725
+ }
636
726
  }
637
727
  delete section.deferredLegacyBlend
638
728
  section.hasBlendMesh = false
@@ -642,17 +732,13 @@ export class ChunkMeshManager {
642
732
  const hadLegacyAsPrimary = section.mesh === this.activeSections.get(sectionKey)?.mesh
643
733
  this.releasePooledMesh(sectionKey)
644
734
  if (hadLegacyAsPrimary) {
645
- section.mesh = section.shaderMesh as unknown as THREE.Mesh<THREE.BufferGeometry, THREE.Material> | undefined ?? undefined
735
+ section.mesh = (section.shaderMesh as unknown as THREE.Mesh<THREE.BufferGeometry, THREE.Material> | undefined) ?? undefined
646
736
  }
647
737
  }
648
738
  this.markCullDirty()
649
739
  }
650
740
 
651
- raycastGlobalLegacySections (
652
- raycaster: THREE.Raycaster,
653
- origin: THREE.Vector3,
654
- maxCenterDistance: number,
655
- ): number | undefined {
741
+ raycastGlobalLegacySections(raycaster: THREE.Raycaster, origin: THREE.Vector3, maxCenterDistance: number): number | undefined {
656
742
  const maxDistSq = maxCenterDistance * maxCenterDistance
657
743
  const dirX = raycaster.ray.direction.x
658
744
  const dirY = raycaster.ray.direction.y
@@ -660,28 +746,15 @@ export class ChunkMeshManager {
660
746
  const far = raycaster.far
661
747
  const halfExtent = LEGACY_SECTION_HALF_EXTENT + 0.01
662
748
  const candidates: string[] = []
663
- for (const [key, section] of Object.entries(this.sectionObjects)) {
664
- if (section.worldX === undefined) continue
749
+ for (const [key] of this.legacyCullSections) {
750
+ const section = this.sectionObjects[key]
751
+ if (!section || section.worldX === undefined) continue
665
752
  const dx = section.worldX - origin.x
666
753
  const dy = (section.worldY ?? 0) - origin.y
667
754
  const dz = (section.worldZ ?? 0) - origin.z
668
755
  if (dx * dx + dy * dy + dz * dz > maxDistSq) continue
669
- const inOpaque = this.globalLegacyBuffer?.hasSection(key) ?? false
670
- const inBlend = this.globalLegacyBlendBuffer?.hasSection(key) ?? false
671
- if (!inOpaque && !inBlend) continue
672
- if (!sectionAabbIntersectsRay(
673
- section.worldX,
674
- section.worldY ?? 0,
675
- section.worldZ ?? 0,
676
- origin.x,
677
- origin.y,
678
- origin.z,
679
- dirX,
680
- dirY,
681
- dirZ,
682
- far,
683
- halfExtent,
684
- )) continue
756
+ if (!sectionAabbIntersectsRay(section.worldX, section.worldY ?? 0, section.worldZ ?? 0, origin.x, origin.y, origin.z, dirX, dirY, dirZ, far, halfExtent))
757
+ continue
685
758
  candidates.push(key)
686
759
  }
687
760
 
@@ -694,13 +767,13 @@ export class ChunkMeshManager {
694
767
  return hits[0]?.distance
695
768
  }
696
769
 
697
- registerShaderSectionRaycastBox (
770
+ registerShaderSectionRaycastBox(
698
771
  sectionKey: string,
699
772
  words: Uint32Array,
700
773
  faceCount: number,
701
774
  sectionCenterX: number,
702
775
  sectionCenterY: number,
703
- sectionCenterZ: number,
776
+ sectionCenterZ: number
704
777
  ): void {
705
778
  const box = computeShaderSectionRaycastAabb(words, faceCount, sectionCenterX, sectionCenterY, sectionCenterZ)
706
779
  if (box) {
@@ -708,24 +781,19 @@ export class ChunkMeshManager {
708
781
  box,
709
782
  sectionCenterX,
710
783
  sectionCenterY,
711
- sectionCenterZ,
784
+ sectionCenterZ
712
785
  })
713
786
  } else {
714
787
  this.shaderSectionRaycastBoxes.delete(sectionKey)
715
788
  }
716
789
  }
717
790
 
718
- unregisterShaderSectionRaycastBox (sectionKey: string): void {
791
+ unregisterShaderSectionRaycastBox(sectionKey: string): void {
719
792
  this.shaderSectionRaycastBoxes.delete(sectionKey)
720
793
  }
721
794
 
722
795
  /** Closest hit against registered shader-cube AABBs (world-space ray). */
723
- raycastShaderSectionAABBs (
724
- originWorld: THREE.Vector3,
725
- direction: THREE.Vector3,
726
- maxDist: number,
727
- maxCenterDistance = 80,
728
- ): number | undefined {
796
+ raycastShaderSectionAABBs(originWorld: THREE.Vector3, direction: THREE.Vector3, maxDist: number, maxCenterDistance = 80): number | undefined {
729
797
  const ox = originWorld.x
730
798
  const oy = originWorld.y
731
799
  const oz = originWorld.z
@@ -753,11 +821,7 @@ export class ChunkMeshManager {
753
821
  const dcz = box.cz - oz
754
822
  if (dcx * dcx + dcy * dcy + dcz * dcz > maxCenterDistSq) continue
755
823
 
756
- let t = raycastAabb(
757
- ox, oy, oz, dx, dy, dz,
758
- box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ,
759
- closest,
760
- )
824
+ let t = raycastAabb(ox, oy, oz, dx, dy, dz, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, closest)
761
825
  if (t === undefined && isPointInsideAabb(ox, oy, oz, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
762
826
  const gb = this.globalBlockBuffer
763
827
  const slot = gb?.getSectionSlot(key)
@@ -770,10 +834,15 @@ export class ChunkMeshManager {
770
834
  entry.sectionCenterX,
771
835
  entry.sectionCenterY,
772
836
  entry.sectionCenterZ,
773
- ox, oy, oz, dx, dy, dz,
837
+ ox,
838
+ oy,
839
+ oz,
840
+ dx,
841
+ dy,
842
+ dz,
774
843
  closest,
775
844
  this.blockRaycastVisitGen,
776
- this.blockRaycastVisitStamp,
845
+ this.blockRaycastVisitStamp
777
846
  )
778
847
  } else {
779
848
  const def = this.sectionObjects[key]?.deferredShaderCubes
@@ -786,10 +855,15 @@ export class ChunkMeshManager {
786
855
  entry.sectionCenterX,
787
856
  entry.sectionCenterY,
788
857
  entry.sectionCenterZ,
789
- ox, oy, oz, dx, dy, dz,
858
+ ox,
859
+ oy,
860
+ oz,
861
+ dx,
862
+ dy,
863
+ dz,
790
864
  closest,
791
865
  this.blockRaycastVisitGen,
792
- this.blockRaycastVisitStamp,
866
+ this.blockRaycastVisitStamp
793
867
  )
794
868
  }
795
869
  }
@@ -806,12 +880,12 @@ export class ChunkMeshManager {
806
880
  /**
807
881
  * Update or create a section with new geometry data
808
882
  */
809
- private uploadLegacyPooledMesh (
883
+ private uploadLegacyPooledMesh(
810
884
  poolEntry: ChunkMeshPool,
811
885
  geometryData: MesherGeometryOutput | MesherGeometryOutput['blend'],
812
886
  sx: number,
813
887
  sy: number,
814
- sz: number,
888
+ sz: number
815
889
  ): THREE.Mesh<THREE.BufferGeometry, THREE.Material> {
816
890
  const { mesh } = poolEntry
817
891
  const geo = geometryData!
@@ -822,14 +896,8 @@ export class ChunkMeshManager {
822
896
  this.updateGeometryAttribute(mesh.geometry, 'a_blockLight', geo.blockLights, 1)
823
897
  this.updateGeometryAttribute(mesh.geometry, 'uv', geo.uvs, 2)
824
898
  mesh.geometry.index = new THREE.BufferAttribute(geo.indices as Uint32Array | Uint16Array, 1)
825
- mesh.geometry.boundingBox = new THREE.Box3(
826
- new THREE.Vector3(-8, -8, -8),
827
- new THREE.Vector3(8, 8, 8),
828
- )
829
- mesh.geometry.boundingSphere = new THREE.Sphere(
830
- new THREE.Vector3(0, 0, 0),
831
- Math.sqrt(3 * 8 ** 2),
832
- )
899
+ mesh.geometry.boundingBox = new THREE.Box3(new THREE.Vector3(-8, -8, -8), new THREE.Vector3(8, 8, 8))
900
+ mesh.geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.sqrt(3 * 8 ** 2))
833
901
  setupLegacySectionMatrix(mesh, sx, sy, sz, this.renderOrigin)
834
902
  mesh.visible = false
835
903
  mesh.name = 'mesh'
@@ -837,7 +905,7 @@ export class ChunkMeshManager {
837
905
  return mesh as THREE.Mesh<THREE.BufferGeometry, THREE.Material>
838
906
  }
839
907
 
840
- private acquirePooledSectionMesh (sectionKey: string): ChunkMeshPool | null {
908
+ private acquirePooledSectionMesh(sectionKey: string): ChunkMeshPool | null {
841
909
  let poolEntry = this.activeSections.get(sectionKey)
842
910
  if (!poolEntry) {
843
911
  poolEntry = this.acquireMesh()
@@ -851,12 +919,18 @@ export class ChunkMeshManager {
851
919
  return poolEntry
852
920
  }
853
921
 
854
- updateSection (sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
922
+ updateSection(sectionKey: string, geometryData: MesherGeometryOutput): SectionObject | null {
855
923
  const hasOpaque = geometryData.positions.length > 0
856
924
  const hasBlend = (geometryData.blend?.positions.length ?? 0) > 0
857
925
  const hasLegacy = hasOpaque || hasBlend
858
926
  const shaderData = geometryData.shaderCubes
859
927
  const hasShader = this.isShaderCubesGpuEnabled() && (shaderData?.count ?? 0) > 0
928
+ const deferOpaque = hasOpaque && this.shouldDeferLegacyOpaqueToPerSection(sectionKey)
929
+ const deferBlend = hasBlend && this.shouldDeferLegacyOpaqueToPerSection(sectionKey)
930
+ const deferShader = hasShader && this.shouldDeferShaderToPerSection(sectionKey)
931
+ const willAddOpaqueGlobal = hasOpaque && !deferOpaque
932
+ const willAddBlendGlobal = hasBlend && !deferBlend
933
+ const willAddCubeGlobal = hasShader && !!shaderData && !deferShader
860
934
 
861
935
  if (!hasLegacy && !hasShader) {
862
936
  this.releaseSection(sectionKey)
@@ -865,10 +939,19 @@ export class ChunkMeshManager {
865
939
 
866
940
  // Remove existing section object from scene if it exists
867
941
  let sectionObject = this.sectionObjects[sectionKey]
942
+ const wasRemesh = sectionObject != null
868
943
  if (sectionObject) {
869
944
  this.cleanupSection(sectionKey, { forRemesh: true })
870
945
  }
871
946
 
947
+ if (wasRemesh) {
948
+ if (!willAddCubeGlobal) this.globalBlockBuffer?.removeSection(sectionKey)
949
+ if (!willAddOpaqueGlobal) this.globalLegacyBuffer?.removeSection(sectionKey)
950
+ if (!willAddBlendGlobal) this.globalLegacyBlendBuffer?.removeSection(sectionKey)
951
+ if (!(hasShader && shaderData)) this.unregisterShaderSectionRaycastBox(sectionKey)
952
+ if (!willAddOpaqueGlobal && !willAddBlendGlobal) this.maybeUnregisterLegacyCullSection(sectionKey)
953
+ }
954
+
872
955
  if (!hasBlend) {
873
956
  this.releasePooledMesh(sectionKey)
874
957
  }
@@ -885,9 +968,8 @@ export class ChunkMeshManager {
885
968
  skyLights: geometryData.skyLights as Float32Array,
886
969
  blockLights: geometryData.blockLights as Float32Array,
887
970
  uvs: geometryData.uvs as Float32Array,
888
- indices: geometryData.indices as Uint32Array | Uint16Array,
971
+ indices: geometryData.indices as Uint32Array | Uint16Array
889
972
  }
890
- const deferOpaque = this.shouldDeferLegacyOpaqueToPerSection(sectionKey)
891
973
  if (deferOpaque) {
892
974
  deferredLegacyOpaque = {
893
975
  positions: new Float32Array(opaqueGeo.positions),
@@ -895,9 +977,7 @@ export class ChunkMeshManager {
895
977
  skyLights: new Float32Array(opaqueGeo.skyLights),
896
978
  blockLights: new Float32Array(opaqueGeo.blockLights),
897
979
  uvs: new Float32Array(opaqueGeo.uvs),
898
- indices: opaqueGeo.indices instanceof Uint32Array
899
- ? new Uint32Array(opaqueGeo.indices)
900
- : new Uint16Array(opaqueGeo.indices),
980
+ indices: opaqueGeo.indices instanceof Uint32Array ? new Uint32Array(opaqueGeo.indices) : new Uint16Array(opaqueGeo.indices)
901
981
  }
902
982
  if (!hasBlend) {
903
983
  const poolEntry = this.acquirePooledSectionMesh(sectionKey)
@@ -905,14 +985,10 @@ export class ChunkMeshManager {
905
985
  legacyMesh = this.uploadLegacyPooledMesh(poolEntry, geometryData, geometryData.sx, geometryData.sy, geometryData.sz)
906
986
  }
907
987
  } else {
908
- const added = this.getGlobalLegacyBuffer().addSection(
909
- sectionKey,
910
- opaqueGeo,
911
- geometryData.sx,
912
- geometryData.sy,
913
- geometryData.sz,
914
- )
915
- if (!added) {
988
+ const added = this.getGlobalLegacyBuffer().addSection(sectionKey, opaqueGeo, geometryData.sx, geometryData.sy, geometryData.sz)
989
+ if (added) {
990
+ this.registerLegacyCullSection(sectionKey, geometryData.sx, geometryData.sy, geometryData.sz)
991
+ } else {
916
992
  const poolEntry = this.acquirePooledSectionMesh(sectionKey)
917
993
  if (!poolEntry) return null
918
994
  legacyMesh = this.uploadLegacyPooledMesh(poolEntry, geometryData, geometryData.sx, geometryData.sy, geometryData.sz)
@@ -927,9 +1003,8 @@ export class ChunkMeshManager {
927
1003
  skyLights: geometryData.blend.skyLights as Float32Array,
928
1004
  blockLights: geometryData.blend.blockLights as Float32Array,
929
1005
  uvs: geometryData.blend.uvs as Float32Array,
930
- indices: geometryData.blend.indices as Uint32Array | Uint16Array,
1006
+ indices: geometryData.blend.indices as Uint32Array | Uint16Array
931
1007
  }
932
- const deferBlend = this.shouldDeferLegacyOpaqueToPerSection(sectionKey)
933
1008
  if (deferBlend) {
934
1009
  deferredLegacyBlend = {
935
1010
  positions: new Float32Array(blendGeo.positions),
@@ -937,40 +1012,22 @@ export class ChunkMeshManager {
937
1012
  skyLights: new Float32Array(blendGeo.skyLights),
938
1013
  blockLights: new Float32Array(blendGeo.blockLights),
939
1014
  uvs: new Float32Array(blendGeo.uvs),
940
- indices: blendGeo.indices instanceof Uint32Array
941
- ? new Uint32Array(blendGeo.indices)
942
- : new Uint16Array(blendGeo.indices),
1015
+ indices: blendGeo.indices instanceof Uint32Array ? new Uint32Array(blendGeo.indices) : new Uint16Array(blendGeo.indices)
943
1016
  }
944
1017
  const poolEntry = this.acquirePooledSectionMesh(sectionKey)
945
1018
  if (!poolEntry) return null
946
- const blendMesh = this.uploadLegacyPooledMesh(
947
- poolEntry,
948
- geometryData.blend,
949
- geometryData.sx,
950
- geometryData.sy,
951
- geometryData.sz,
952
- )
1019
+ const blendMesh = this.uploadLegacyPooledMesh(poolEntry, geometryData.blend, geometryData.sx, geometryData.sy, geometryData.sz)
953
1020
  legacyMesh = legacyMesh ?? blendMesh
954
1021
  hasBlendMesh = true
955
1022
  } else {
956
- const added = this.getGlobalLegacyBlendBuffer().addSection(
957
- sectionKey,
958
- blendGeo,
959
- geometryData.sx,
960
- geometryData.sy,
961
- geometryData.sz,
962
- )
963
- if (!added) {
1023
+ const added = this.getGlobalLegacyBlendBuffer().addSection(sectionKey, blendGeo, geometryData.sx, geometryData.sy, geometryData.sz)
1024
+ if (added) {
1025
+ this.registerLegacyCullSection(sectionKey, geometryData.sx, geometryData.sy, geometryData.sz)
1026
+ } else {
964
1027
  console.warn(`ChunkMeshManager: blend invariant violation for section ${sectionKey}, using pooled mesh fallback`)
965
1028
  const poolEntry = this.acquirePooledSectionMesh(sectionKey)
966
1029
  if (!poolEntry) return null
967
- const blendMesh = this.uploadLegacyPooledMesh(
968
- poolEntry,
969
- geometryData.blend,
970
- geometryData.sx,
971
- geometryData.sy,
972
- geometryData.sz,
973
- )
1030
+ const blendMesh = this.uploadLegacyPooledMesh(poolEntry, geometryData.blend, geometryData.sx, geometryData.sy, geometryData.sz)
974
1031
  legacyMesh = legacyMesh ?? blendMesh
975
1032
  hasBlendMesh = true
976
1033
  }
@@ -979,7 +1036,6 @@ export class ChunkMeshManager {
979
1036
 
980
1037
  const cubeMaterial = hasShader ? this.getCubeShaderMaterial() : null
981
1038
  let shaderMesh: THREE.Mesh<THREE.InstancedBufferGeometry, THREE.ShaderMaterial> | undefined
982
- const deferShader = hasShader && this.shouldDeferShaderToPerSection(sectionKey)
983
1039
  if (hasShader && shaderData) {
984
1040
  if (deferShader && cubeMaterial) {
985
1041
  shaderMesh = createShaderCubeMesh(shaderData, cubeMaterial)
@@ -1000,7 +1056,7 @@ export class ChunkMeshManager {
1000
1056
  if (shaderData) {
1001
1057
  sectionObject.deferredShaderCubes = {
1002
1058
  words: shaderData.words,
1003
- count: shaderData.count,
1059
+ count: shaderData.count
1004
1060
  }
1005
1061
  }
1006
1062
  if (!sectionObject.mesh) {
@@ -1014,14 +1070,7 @@ export class ChunkMeshManager {
1014
1070
  sectionObject.deferredLegacyBlend = deferredLegacyBlend
1015
1071
  }
1016
1072
  if (hasShader && shaderData) {
1017
- this.registerShaderSectionRaycastBox(
1018
- sectionKey,
1019
- shaderData.words,
1020
- shaderData.count,
1021
- geometryData.sx,
1022
- geometryData.sy,
1023
- geometryData.sz,
1024
- )
1073
+ this.registerShaderSectionRaycastBox(sectionKey, shaderData.words, shaderData.count, geometryData.sx, geometryData.sy, geometryData.sz)
1025
1074
  }
1026
1075
 
1027
1076
  let tilesCount = 0
@@ -1096,20 +1145,12 @@ export class ChunkMeshManager {
1096
1145
  const bannerTexture = getBannerTexture(this.worldRenderer, blockName, nbt.simplify(bannerBlockEntity))
1097
1146
  if (!bannerTexture) continue
1098
1147
  const skyLevel = this.blockEntityLightRegistry.getSkyLevel()
1099
- const banner = createBannerMesh(
1100
- new Vec3(+x, +y, +z),
1101
- rotation,
1102
- isWall,
1103
- bannerTexture,
1104
- blockLightNorm,
1105
- skyLightNorm,
1106
- skyLevel,
1107
- )
1148
+ const banner = createBannerMesh(new Vec3(+x, +y, +z), rotation, isWall, bannerTexture, blockLightNorm, skyLightNorm, skyLevel)
1108
1149
  if (banner.bannerMaterial) {
1109
1150
  this.blockEntityLightRegistry.register({
1110
1151
  material: banner.bannerMaterial,
1111
1152
  blockLightNorm,
1112
- skyLightNorm,
1153
+ skyLightNorm
1113
1154
  })
1114
1155
  }
1115
1156
  const { x: bwx, y: bwy, z: bwz } = banner.position
@@ -1143,8 +1184,7 @@ export class ChunkMeshManager {
1143
1184
  // setting so the near-first reveal gate has sections to hold.
1144
1185
  const chunkCoords = sectionKey.split(',')
1145
1186
  const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
1146
- const renderByChunks = !!this.worldRenderer.displayOptions
1147
- ?.inWorldRenderingConfig?._renderByChunks
1187
+ const renderByChunks = !!this.worldRenderer.displayOptions?.inWorldRenderingConfig?._renderByChunks
1148
1188
  const forceBatchForWasm = !!this.worldRenderer.worldRendererConfig?.wasmMesher
1149
1189
  if ((renderByChunks || forceBatchForWasm) && !this.worldRenderer.finishedChunks[chunkKey]) {
1150
1190
  sectionObject.visible = false
@@ -1165,7 +1205,7 @@ export class ChunkMeshManager {
1165
1205
  * reveal is deferred (parked in `pendingNearReveal`) and re-checked on
1166
1206
  * the next chunkFinished / player-move / grace-expiry.
1167
1207
  */
1168
- finishChunkDisplay (chunkKey: string): void {
1208
+ finishChunkDisplay(chunkKey: string): void {
1169
1209
  const sectionKeys = this.waitingChunksToDisplay[chunkKey]
1170
1210
  if (!sectionKeys) {
1171
1211
  // No held sections (empty column / non-batched path) — but the
@@ -1184,7 +1224,7 @@ export class ChunkMeshManager {
1184
1224
  this.tryRevealPending()
1185
1225
  }
1186
1226
 
1187
- private flushChunkDisplay (chunkKey: string): void {
1227
+ private flushChunkDisplay(chunkKey: string): void {
1188
1228
  const sectionKeys = this.waitingChunksToDisplay[chunkKey]
1189
1229
  this.pendingNearReveal.delete(chunkKey)
1190
1230
  this.clearNearRevealTimer(chunkKey)
@@ -1197,13 +1237,14 @@ export class ChunkMeshManager {
1197
1237
  sectionObject.visible = true
1198
1238
  }
1199
1239
  delete this.waitingChunksToDisplay[chunkKey]
1240
+ this.markCullDirty()
1200
1241
  }
1201
1242
 
1202
1243
  // Re-check every parked entry; each has its own grace window via `ageMs`.
1203
1244
  // Single pass is enough — pending entries are already finished, so flushing
1204
1245
  // one cannot un-block another via this code path (cascading happens via
1205
1246
  // chunkFinished events and per-pending grace timers).
1206
- tryRevealPending (): void {
1247
+ tryRevealPending(): void {
1207
1248
  if (this.pendingNearReveal.size === 0) return
1208
1249
  const now = Date.now()
1209
1250
  for (const [chunkKey, enqueuedAt] of [...this.pendingNearReveal]) {
@@ -1215,7 +1256,7 @@ export class ChunkMeshManager {
1215
1256
 
1216
1257
  // Drop gate state for an unloaded column and re-evaluate any farther
1217
1258
  // chunks that may have been blocked by it.
1218
- onChunkRemovedFromGate (chunkKey: string): void {
1259
+ onChunkRemovedFromGate(chunkKey: string): void {
1219
1260
  this.pendingNearReveal.delete(chunkKey)
1220
1261
  this.clearNearRevealTimer(chunkKey)
1221
1262
  this.clearExpectedGraceTimer(chunkKey)
@@ -1223,7 +1264,7 @@ export class ChunkMeshManager {
1223
1264
  this.tryRevealPending()
1224
1265
  }
1225
1266
 
1226
- private isWasmGateActive (): boolean {
1267
+ private isWasmGateActive(): boolean {
1227
1268
  return !!this.worldRenderer.worldRendererConfig?.wasmMesher
1228
1269
  }
1229
1270
 
@@ -1237,7 +1278,7 @@ export class ChunkMeshManager {
1237
1278
  * - After grace: only actually-loaded-but-not-finished columns block,
1238
1279
  * so a never-arriving column does not freeze the view.
1239
1280
  */
1240
- private isBlockedByNearer (chunkKey: string, ageMs: number): boolean {
1281
+ private isBlockedByNearer(chunkKey: string, ageMs: number): boolean {
1241
1282
  const viewer = this.worldRenderer.viewerChunkPosition
1242
1283
  if (!viewer) return false
1243
1284
  const ownParts = chunkKey.split(',')
@@ -1284,7 +1325,7 @@ export class ChunkMeshManager {
1284
1325
  return false
1285
1326
  }
1286
1327
 
1287
- private armNearRevealTimer (chunkKey: string): void {
1328
+ private armNearRevealTimer(chunkKey: string): void {
1288
1329
  if (this.nearRevealTimers.has(chunkKey)) return
1289
1330
  const timer = setTimeout(() => {
1290
1331
  this.nearRevealTimers.delete(chunkKey)
@@ -1296,7 +1337,7 @@ export class ChunkMeshManager {
1296
1337
  this.nearRevealTimers.set(chunkKey, timer)
1297
1338
  }
1298
1339
 
1299
- private clearNearRevealTimer (chunkKey: string): void {
1340
+ private clearNearRevealTimer(chunkKey: string): void {
1300
1341
  const timer = this.nearRevealTimers.get(chunkKey)
1301
1342
  if (timer) {
1302
1343
  clearTimeout(timer)
@@ -1309,7 +1350,7 @@ export class ChunkMeshManager {
1309
1350
  * "expected but never arrived" positions stop blocking promptly,
1310
1351
  * without waiting for the next chunkFinished / player-move event.
1311
1352
  */
1312
- private armExpectedGraceTimer (chunkKey: string): void {
1353
+ private armExpectedGraceTimer(chunkKey: string): void {
1313
1354
  if (this.nearRevealGraceTimers.has(chunkKey)) return
1314
1355
  const timer = setTimeout(() => {
1315
1356
  this.nearRevealGraceTimers.delete(chunkKey)
@@ -1319,7 +1360,7 @@ export class ChunkMeshManager {
1319
1360
  this.nearRevealGraceTimers.set(chunkKey, timer)
1320
1361
  }
1321
1362
 
1322
- private clearExpectedGraceTimer (chunkKey: string): void {
1363
+ private clearExpectedGraceTimer(chunkKey: string): void {
1323
1364
  const timer = this.nearRevealGraceTimers.get(chunkKey)
1324
1365
  if (timer) {
1325
1366
  clearTimeout(timer)
@@ -1327,7 +1368,7 @@ export class ChunkMeshManager {
1327
1368
  }
1328
1369
  }
1329
1370
 
1330
- cleanupSection (sectionKey: string, opts?: { forRemesh?: boolean }) {
1371
+ cleanupSection(sectionKey: string, opts?: { forRemesh?: boolean }) {
1331
1372
  // Remove section object from scene
1332
1373
  const sectionObject = this.sectionObjects[sectionKey]
1333
1374
  if (sectionObject) {
@@ -1346,7 +1387,7 @@ export class ChunkMeshManager {
1346
1387
  // Cleanup banner textures before disposing
1347
1388
  if (sectionObject.bannersContainer) {
1348
1389
  for (const child of sectionObject.bannersContainer.children) {
1349
- const banner = child as THREE.Group & { bannerMaterial?: THREE.MeshBasicMaterial, bannerTexture?: THREE.Texture }
1390
+ const banner = child as THREE.Group & { bannerMaterial?: THREE.MeshBasicMaterial; bannerTexture?: THREE.Texture }
1350
1391
  if (banner.bannerMaterial) {
1351
1392
  this.blockEntityLightRegistry.unregister(banner.bannerMaterial)
1352
1393
  }
@@ -1356,10 +1397,13 @@ export class ChunkMeshManager {
1356
1397
  }
1357
1398
  this.disposeContainer(sectionObject.bannersContainer)
1358
1399
  }
1359
- this.globalBlockBuffer?.removeSection(sectionKey)
1360
- this.globalLegacyBuffer?.removeSection(sectionKey)
1361
- this.globalLegacyBlendBuffer?.removeSection(sectionKey)
1362
- this.unregisterShaderSectionRaycastBox(sectionKey)
1400
+ if (!opts?.forRemesh) {
1401
+ this.globalBlockBuffer?.removeSection(sectionKey)
1402
+ this.globalLegacyBuffer?.removeSection(sectionKey)
1403
+ this.globalLegacyBlendBuffer?.removeSection(sectionKey)
1404
+ this.maybeUnregisterLegacyCullSection(sectionKey)
1405
+ this.unregisterShaderSectionRaycastBox(sectionKey)
1406
+ }
1363
1407
  this.markCullDirty()
1364
1408
  delete sectionObject.deferredLegacyOpaque
1365
1409
  delete sectionObject.deferredLegacyBlend
@@ -1398,8 +1442,7 @@ export class ChunkMeshManager {
1398
1442
  }
1399
1443
  delete this.sectionObjects[sectionKey]
1400
1444
  if (!opts?.forRemesh) {
1401
- this.worldRenderer.getModule<{ onSectionRemoved?: (key: string) => void }>('futuristicReveal')
1402
- ?.onSectionRemoved?.(sectionKey)
1445
+ this.worldRenderer.getModule<{ onSectionRemoved?: (key: string) => void }>('futuristicReveal')?.onSectionRemoved?.(sectionKey)
1403
1446
  }
1404
1447
  }
1405
1448
  }
@@ -1407,7 +1450,7 @@ export class ChunkMeshManager {
1407
1450
  /**
1408
1451
  * Release a section and return its mesh to the pool
1409
1452
  */
1410
- private releasePooledMesh (sectionKey: string): void {
1453
+ private releasePooledMesh(sectionKey: string): void {
1411
1454
  const poolEntry = this.activeSections.get(sectionKey)
1412
1455
  if (!poolEntry) return
1413
1456
 
@@ -1420,7 +1463,7 @@ export class ChunkMeshManager {
1420
1463
  this.cleanupExcessMeshes()
1421
1464
  }
1422
1465
 
1423
- releaseSection (sectionKey: string): boolean {
1466
+ releaseSection(sectionKey: string): boolean {
1424
1467
  this.cleanupSection(sectionKey)
1425
1468
 
1426
1469
  const poolEntry = this.activeSections.get(sectionKey)
@@ -1448,14 +1491,14 @@ export class ChunkMeshManager {
1448
1491
  /**
1449
1492
  * Get section object if it exists
1450
1493
  */
1451
- getSectionObject (sectionKey: string): SectionObject | undefined {
1494
+ getSectionObject(sectionKey: string): SectionObject | undefined {
1452
1495
  return this.sectionObjects[sectionKey]
1453
1496
  }
1454
1497
 
1455
1498
  /**
1456
1499
  * Update box helper for a section
1457
1500
  */
1458
- updateBoxHelper (sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material = this.chunkBoxMaterial) {
1501
+ updateBoxHelper(sectionKey: string, showChunkBorders: boolean, chunkBoxMaterial: THREE.Material = this.chunkBoxMaterial) {
1459
1502
  const sectionObject = this.sectionObjects[sectionKey]
1460
1503
  if (!sectionObject) return
1461
1504
 
@@ -1491,7 +1534,7 @@ export class ChunkMeshManager {
1491
1534
  * after the move from `WorldBlockGeometry` (which created the helpers
1492
1535
  * eagerly per section) to the pooled `ChunkMeshManager`.
1493
1536
  */
1494
- updateAllBoxHelpers (showChunkBorders: boolean) {
1537
+ updateAllBoxHelpers(showChunkBorders: boolean) {
1495
1538
  for (const sectionKey of Object.keys(this.sectionObjects)) {
1496
1539
  this.updateBoxHelper(sectionKey, showChunkBorders)
1497
1540
  }
@@ -1500,21 +1543,21 @@ export class ChunkMeshManager {
1500
1543
  /**
1501
1544
  * Get mesh for section if it exists
1502
1545
  */
1503
- getSectionMesh (sectionKey: string): THREE.Mesh | undefined {
1546
+ getSectionMesh(sectionKey: string): THREE.Mesh | undefined {
1504
1547
  return this.activeSections.get(sectionKey)?.mesh
1505
1548
  }
1506
1549
 
1507
1550
  /**
1508
1551
  * Check if section is managed by this pool
1509
1552
  */
1510
- hasSection (sectionKey: string): boolean {
1553
+ hasSection(sectionKey: string): boolean {
1511
1554
  return this.activeSections.has(sectionKey)
1512
1555
  }
1513
1556
 
1514
1557
  /**
1515
1558
  * Update pool size based on new view distance
1516
1559
  */
1517
- updateViewDistance (maxViewDistance: number) {
1560
+ updateViewDistance(maxViewDistance: number) {
1518
1561
  // Calculate dynamic pool size based on view distance
1519
1562
  const chunksInView = (maxViewDistance * 2 + 1) ** 2
1520
1563
  const maxSectionsPerChunk = this.worldHeight / 16
@@ -1535,7 +1578,7 @@ export class ChunkMeshManager {
1535
1578
  /**
1536
1579
  * Get pool statistics
1537
1580
  */
1538
- getGlobalBufferStats (): GlobalBufferStats {
1581
+ getGlobalBufferStats(): GlobalBufferStats {
1539
1582
  const snapshotLegacy = (buffer: GlobalLegacyBuffer | null): GlobalBufferSlotStats | null => {
1540
1583
  if (!buffer) return null
1541
1584
  return {
@@ -1543,7 +1586,7 @@ export class ChunkMeshManager {
1543
1586
  capacity: buffer.getCapacityQuads(),
1544
1587
  sections: buffer.getSectionCount(),
1545
1588
  usedBytes: buffer.getUsedMemoryBytes(),
1546
- capacityBytes: buffer.getMemoryBytes(),
1589
+ capacityBytes: buffer.getMemoryBytes()
1547
1590
  }
1548
1591
  }
1549
1592
 
@@ -1555,17 +1598,17 @@ export class ChunkMeshManager {
1555
1598
  capacity: cubes.getCapacityFaces(),
1556
1599
  sections: cubes.getSectionCount(),
1557
1600
  usedBytes: cubes.getUsedMemoryBytes(),
1558
- capacityBytes: cubes.getMemoryBytes(),
1601
+ capacityBytes: cubes.getMemoryBytes()
1559
1602
  }
1560
1603
  : null,
1561
1604
  legacyOpaque: snapshotLegacy(this.globalLegacyBuffer),
1562
- legacyBlend: snapshotLegacy(this.globalLegacyBlendBuffer),
1605
+ legacyBlend: snapshotLegacy(this.globalLegacyBlendBuffer)
1563
1606
  }
1564
1607
  }
1565
1608
 
1566
- getStats () {
1609
+ getStats() {
1567
1610
  const freeCount = this.meshPool.filter(entry => !entry.inUse).length
1568
- const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
1611
+ const hitRate = this.hits + this.misses > 0 ? ((this.hits / (this.hits + this.misses)) * 100).toFixed(1) : '0'
1569
1612
  const memoryUsage = this.getEstimatedMemoryUsage()
1570
1613
 
1571
1614
  return {
@@ -1582,21 +1625,21 @@ export class ChunkMeshManager {
1582
1625
  /**
1583
1626
  * Get total tiles rendered
1584
1627
  */
1585
- getTotalTiles (): number {
1628
+ getTotalTiles(): number {
1586
1629
  return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.tilesCount || 0), 0)
1587
1630
  }
1588
1631
 
1589
1632
  /**
1590
1633
  * Get total blocks rendered
1591
1634
  */
1592
- getTotalBlocks (): number {
1635
+ getTotalBlocks(): number {
1593
1636
  return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj.blocksCount || 0), 0)
1594
1637
  }
1595
1638
 
1596
1639
  /**
1597
1640
  * Estimate memory usage in MB
1598
1641
  */
1599
- getEstimatedMemoryUsage (): { total: string, breakdown: any } {
1642
+ getEstimatedMemoryUsage(): { total: string; breakdown: any } {
1600
1643
  let totalBytes = 0
1601
1644
  let positionBytes = 0
1602
1645
  let normalBytes = 0
@@ -1685,7 +1728,7 @@ export class ChunkMeshManager {
1685
1728
  color: `${(colorBytes / (1024 * 1024)).toFixed(2)} MB`,
1686
1729
  uv: `${(uvBytes / (1024 * 1024)).toFixed(2)} MB`,
1687
1730
  index: `${(indexBytes / (1024 * 1024)).toFixed(2)} MB`,
1688
- shaderInstances: `${(shaderInstanceBytes / (1024 * 1024)).toFixed(2)} MB`,
1731
+ shaderInstances: `${(shaderInstanceBytes / (1024 * 1024)).toFixed(2)} MB`
1689
1732
  }
1690
1733
  }
1691
1734
  }
@@ -1693,7 +1736,7 @@ export class ChunkMeshManager {
1693
1736
  /**
1694
1737
  * Cleanup and dispose resources
1695
1738
  */
1696
- dispose () {
1739
+ dispose() {
1697
1740
  // Release all active sections (snapshot keys to avoid mutating map during iteration)
1698
1741
  const activeKeys = [...this.activeSections.keys()]
1699
1742
  for (const sectionKey of activeKeys) {
@@ -1712,6 +1755,7 @@ export class ChunkMeshManager {
1712
1755
  this.activeSections.clear()
1713
1756
  this.chunkBoxMaterial.dispose()
1714
1757
  this.shaderSectionRaycastBoxes.clear()
1758
+ this.lastBufferStateKey = ''
1715
1759
  this.globalBlockBuffer?.dispose()
1716
1760
  this.globalBlockBuffer = null
1717
1761
  this.globalLegacyBuffer?.dispose()
@@ -1736,7 +1780,7 @@ export class ChunkMeshManager {
1736
1780
 
1737
1781
  // Private helper methods
1738
1782
 
1739
- private acquireMesh (): ChunkMeshPool | undefined {
1783
+ private acquireMesh(): ChunkMeshPool | undefined {
1740
1784
  if (this.bypassPooling) {
1741
1785
  const entry: ChunkMeshPool = {
1742
1786
  mesh: new THREE.Mesh(new THREE.BufferGeometry(), this.getLegacyShaderMaterial()),
@@ -1783,7 +1827,7 @@ export class ChunkMeshManager {
1783
1827
  throw new Error('ChunkMeshManager: Failed to acquire mesh after pool expansion')
1784
1828
  }
1785
1829
 
1786
- private expandPool (newSize: number) {
1830
+ private expandPool(newSize: number) {
1787
1831
  const currentLength = this.meshPool.length
1788
1832
  this.poolSize = newSize
1789
1833
 
@@ -1806,12 +1850,7 @@ export class ChunkMeshManager {
1806
1850
  }
1807
1851
  }
1808
1852
 
1809
- private updateGeometryAttribute (
1810
- geometry: THREE.BufferGeometry,
1811
- name: string,
1812
- array: Float32Array,
1813
- itemSize: number
1814
- ) {
1853
+ private updateGeometryAttribute(geometry: THREE.BufferGeometry, name: string, array: Float32Array, itemSize: number) {
1815
1854
  const attribute = geometry.getAttribute(name)
1816
1855
 
1817
1856
  if (attribute && attribute.count === array.length / itemSize) {
@@ -1824,7 +1863,7 @@ export class ChunkMeshManager {
1824
1863
  }
1825
1864
  }
1826
1865
 
1827
- private clearGeometry (geometry: THREE.BufferGeometry) {
1866
+ private clearGeometry(geometry: THREE.BufferGeometry) {
1828
1867
  const attributes = ['position', 'normal', 'color', 'a_skyLight', 'a_blockLight', 'uv']
1829
1868
  for (const name of attributes) {
1830
1869
  if (geometry.hasAttribute(name)) {
@@ -1838,7 +1877,7 @@ export class ChunkMeshManager {
1838
1877
  geometry.boundingSphere = null
1839
1878
  }
1840
1879
 
1841
- private cleanupExcessMeshes () {
1880
+ private cleanupExcessMeshes() {
1842
1881
  // If pool size exceeds max and we have free meshes, remove some
1843
1882
  if (this.poolSize > this.maxPoolSize) {
1844
1883
  const freeCount = this.meshPool.filter(entry => !entry.inUse).length
@@ -1858,14 +1897,14 @@ export class ChunkMeshManager {
1858
1897
  }
1859
1898
  }
1860
1899
 
1861
- private disposeContainer (container: THREE.Group, cleanTextures = true) {
1900
+ private disposeContainer(container: THREE.Group, cleanTextures = true) {
1862
1901
  disposeObject(container, cleanTextures)
1863
1902
  }
1864
1903
 
1865
1904
  /**
1866
1905
  * Record render time for performance monitoring
1867
1906
  */
1868
- recordRenderTime (renderTime: number): void {
1907
+ recordRenderTime(renderTime: number): void {
1869
1908
  this.renderTimes.push(renderTime)
1870
1909
  if (this.renderTimes.length > this.maxRenderTimeSamples) {
1871
1910
  this.renderTimes.shift()
@@ -1882,31 +1921,31 @@ export class ChunkMeshManager {
1882
1921
  /**
1883
1922
  * Get current effective render distance
1884
1923
  */
1885
- getEffectiveRenderDistance (): number {
1924
+ getEffectiveRenderDistance(): number {
1886
1925
  return this.performanceOverrideDistance || this.worldRenderer.viewDistance
1887
1926
  }
1888
1927
 
1889
1928
  /**
1890
1929
  * Force reset performance override
1891
1930
  */
1892
- resetPerformanceOverride (): void {
1931
+ resetPerformanceOverride(): void {
1893
1932
  this.performanceOverrideDistance = undefined
1894
1933
  this.renderTimes.length = 0
1895
1934
  console.log('ChunkMeshManager: Performance override reset')
1896
1935
  }
1897
1936
 
1898
1937
  /**
1899
- * Get average render time
1900
- */
1901
- getAverageRenderTime (): number {
1938
+ * Get average render time
1939
+ */
1940
+ getAverageRenderTime(): number {
1902
1941
  if (this.renderTimes.length === 0) return 0
1903
1942
  return this.renderTimes.reduce((sum, time) => sum + time, 0) / this.renderTimes.length
1904
1943
  }
1905
1944
 
1906
1945
  /**
1907
- * Check if performance is degraded and adjust render distance
1908
- */
1909
- private checkPerformance (): void {
1946
+ * Check if performance is degraded and adjust render distance
1947
+ */
1948
+ private checkPerformance(): void {
1910
1949
  if (this.renderTimes.length < this.maxRenderTimeSamples) return
1911
1950
 
1912
1951
  const avgRenderTime = this.getAverageRenderTime()
@@ -1937,7 +1976,7 @@ export class ChunkMeshManager {
1937
1976
  /**
1938
1977
  * Hide sections beyond performance override distance
1939
1978
  */
1940
- updateSectionsVisibility (): void {
1979
+ updateSectionsVisibility(): void {
1941
1980
  const cameraPos = this.worldRenderer.cameraSectionPos
1942
1981
  for (const [sectionKey, sectionObject] of Object.entries(this.sectionObjects)) {
1943
1982
  // Don't override "Batch Chunks Display" hiding — those sections must
@@ -1964,16 +2003,14 @@ export class ChunkMeshManager {
1964
2003
  }
1965
2004
  }
1966
2005
 
1967
-
1968
2006
  class SignHeadsRenderer {
1969
- constructor (public worldRendererThree: WorldRendererThree) {
1970
- }
2007
+ constructor(public worldRendererThree: WorldRendererThree) {}
1971
2008
 
1972
- dispose () {
2009
+ dispose() {
1973
2010
  disposeAllSignTextures()
1974
2011
  }
1975
2012
 
1976
- renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
2013
+ renderHead(position: Vec3, rotation: number, isWall: boolean, blockEntity) {
1977
2014
  let textureData: string
1978
2015
  if (blockEntity.SkullOwner) {
1979
2016
  textureData = blockEntity.SkullOwner.Properties?.textures?.[0]?.Value
@@ -1987,8 +2024,7 @@ class SignHeadsRenderer {
1987
2024
  let skinUrl = decodedData.textures?.SKIN?.url
1988
2025
  const { skinTexturesProxy } = this.worldRendererThree.worldRendererConfig
1989
2026
  if (skinTexturesProxy) {
1990
- skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy)
1991
- .replace('https://textures.minecraft.net/', skinTexturesProxy)
2027
+ skinUrl = skinUrl?.replace('http://textures.minecraft.net/', skinTexturesProxy).replace('https://textures.minecraft.net/', skinTexturesProxy)
1992
2028
  }
1993
2029
 
1994
2030
  const mesh = getMesh(this.worldRendererThree, skinUrl, armorModel.head as any)
@@ -2001,11 +2037,7 @@ class SignHeadsRenderer {
2001
2037
  group.add(mesh)
2002
2038
  this.worldRendererThree.sceneOrigin.track(group)
2003
2039
  group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
2004
- group.rotation.set(
2005
- 0,
2006
- -THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
2007
- 0
2008
- )
2040
+ group.rotation.set(0, -THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)), 0)
2009
2041
  group.scale.set(0.8, 0.8, 0.8)
2010
2042
  return group
2011
2043
  } catch (err) {
@@ -2013,7 +2045,7 @@ class SignHeadsRenderer {
2013
2045
  }
2014
2046
  }
2015
2047
 
2016
- renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
2048
+ renderSign(position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
2017
2049
  const tex = getSignTexture(this.worldRendererThree, blockEntity, isHanging)
2018
2050
 
2019
2051
  if (!tex) return
@@ -2034,11 +2066,7 @@ class SignHeadsRenderer {
2034
2066
  }
2035
2067
 
2036
2068
  const group = new THREE.Group() as THREE.Group & { signTexture?: THREE.Texture }
2037
- group.rotation.set(
2038
- 0,
2039
- -THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
2040
- 0
2041
- )
2069
+ group.rotation.set(0, -THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)), 0)
2042
2070
  group.add(mesh)
2043
2071
  group.signTexture = tex
2044
2072
  const height = (isHanging ? 10 : 8) / 16