minecraft-renderer 0.1.36 → 0.1.38

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.
@@ -50,8 +50,10 @@ export class WorldRendererThree extends WorldRendererCommon {
50
50
  cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
51
51
  holdingBlock: IHoldingBlock
52
52
  holdingBlockLeft: IHoldingBlock
53
- realScene = new THREE.Scene()
54
- scene = new THREE.Group()
53
+ scene = new THREE.Scene()
54
+ get realScene() {
55
+ return this.scene
56
+ }
55
57
  ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
56
58
  directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
57
59
  entities = new Entities(this, (globalThis as any).mcData)
@@ -65,7 +67,7 @@ export class WorldRendererThree extends WorldRendererCommon {
65
67
  cameraContainer!: THREE.Object3D
66
68
  media: ThreeJsMedia
67
69
  get waitingChunksToDisplay() {
68
- return {} as { [chunkKey: string]: string[] }
70
+ return this.chunkMeshManager.waitingChunksToDisplay
69
71
  }
70
72
  waypoints: WaypointsRenderer
71
73
  cinimaticScript: CinimaticScriptRunner
@@ -79,6 +81,16 @@ export class WorldRendererThree extends WorldRendererCommon {
79
81
  */
80
82
  camera!: THREE.PerspectiveCamera
81
83
  renderTimeAvg = 0
84
+ private pendingSectionUpdates = new Map<string, { geometry: MesherGeometryOutput, key: string, type: string }>()
85
+ /**
86
+ * Per-section buffering timestamps for `applyPendingSectionUpdates`.
87
+ * Each section gets its own deadline so a continuous stream of updates
88
+ * (e.g. server-side block changes from explosions, pistons, fluid ticks)
89
+ * does not flush freshly added sections together with stale ones via a
90
+ * single global timer.
91
+ */
92
+ private pendingSectionBufferStartTimes = new Map<string, number>()
93
+ private static readonly MAX_SECTION_UPDATE_BUFFER_MS = 500
82
94
  // Memory usage tracking (in bytes)
83
95
  get estimatedMemoryUsage() {
84
96
  return this.chunkMeshManager.getEstimatedMemoryUsage().total
@@ -107,7 +119,7 @@ export class WorldRendererThree extends WorldRendererCommon {
107
119
  DEBUG_RAYCAST = false
108
120
  skyboxRenderer: SkyboxRenderer
109
121
  fireworks: FireworksManager
110
- sceneOrigin = new SceneOrigin(this.realScene)
122
+ sceneOrigin = new SceneOrigin(this.scene)
111
123
  /** Camera world position stored in float64 (JS number) for precision */
112
124
  cameraWorldPos = { x: 0, y: 0, z: 0 }
113
125
 
@@ -450,20 +462,19 @@ export class WorldRendererThree extends WorldRendererCommon {
450
462
  this.cameraWorldPos.y = 0
451
463
  this.cameraWorldPos.z = 0
452
464
 
453
- this.realScene.matrixAutoUpdate = false // for perf
454
- this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
455
- this.realScene.add(this.ambientLight)
465
+ this.scene.matrixAutoUpdate = false // for perf
466
+ this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
467
+ this.scene.add(this.ambientLight)
456
468
  this.directionalLight.position.set(1, 1, 0.5).normalize()
457
469
  this.directionalLight.castShadow = true
458
- this.realScene.add(this.directionalLight)
470
+ this.scene.add(this.directionalLight)
459
471
 
460
472
  const size = this.renderer.getSize(new THREE.Vector2())
461
473
  this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
462
474
  this._wrapCameraPositionWithWarning()
463
475
  this.cameraContainer = new THREE.Object3D()
464
476
  this.cameraContainer.add(this.camera)
465
- this.realScene.add(this.cameraContainer)
466
- this.realScene.add(this.scene)
477
+ this.scene.add(this.cameraContainer)
467
478
  }
468
479
 
469
480
  override watchReactivePlayerState() {
@@ -743,13 +754,108 @@ export class WorldRendererThree extends WorldRendererCommon {
743
754
  }
744
755
 
745
756
  finishChunk(chunkKey: string) {
746
- // ChunkMeshManager applies updates immediately, no buffering needed
757
+ // Reveal all sections of this chunk that were held invisible by the
758
+ // "Batch Chunks Display" (`_renderByChunks`) option. No-op when the
759
+ // option is off — `waitingChunksToDisplay` is empty in that case.
760
+ this.chunkMeshManager.finishChunkDisplay(chunkKey)
761
+ }
762
+
763
+ private applyPendingSectionUpdates() {
764
+ if (this.pendingSectionUpdates.size === 0) return
765
+
766
+ const now = performance.now()
767
+ const sectionHeight = this.getSectionHeight()
768
+ const ready: string[] = []
769
+
770
+ for (const key of this.pendingSectionUpdates.keys()) {
771
+ const startedAt = this.pendingSectionBufferStartTimes.get(key) ?? now
772
+ const sinceFirst = now - startedAt
773
+
774
+ if (sinceFirst < WorldRendererThree.MAX_SECTION_UPDATE_BUFFER_MS) {
775
+ // Still within this section's grace window — wait if any neighbor is
776
+ // currently being re-meshed so we don't briefly expose a hole between
777
+ // the just-updated section and a stale neighbor (sky-flicker bug).
778
+ const [sx, sy, sz] = key.split(',').map(Number)
779
+ const neighborKeys = [
780
+ `${sx - 16},${sy},${sz}`, `${sx + 16},${sy},${sz}`,
781
+ `${sx},${sy - sectionHeight},${sz}`, `${sx},${sy + sectionHeight},${sz}`,
782
+ `${sx},${sy},${sz - 16}`, `${sx},${sy},${sz + 16}`,
783
+ ]
784
+ let neighborBusy = false
785
+ for (const neighborKey of neighborKeys) {
786
+ if (
787
+ this.sectionsWaiting.has(neighborKey) &&
788
+ !this.pendingSectionUpdates.has(neighborKey) &&
789
+ this.sectionObjects[neighborKey]
790
+ ) {
791
+ neighborBusy = true
792
+ break
793
+ }
794
+ }
795
+ if (neighborBusy) continue
796
+ }
797
+
798
+ ready.push(key)
799
+ }
800
+
801
+ if (ready.length === 0) return
802
+
803
+ for (const key of ready) {
804
+ const update = this.pendingSectionUpdates.get(key)!
805
+ this.pendingSectionUpdates.delete(key)
806
+ this.pendingSectionBufferStartTimes.delete(key)
807
+
808
+ const chunkCoords = update.key.split(',')
809
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
810
+
811
+ if (!this.loadedChunks[chunkKey] || !this.active) {
812
+ this.chunkMeshManager.releaseSection(update.key)
813
+ continue
814
+ }
815
+
816
+ if (!update.geometry.positions.length) {
817
+ this.chunkMeshManager.releaseSection(update.key)
818
+ continue
819
+ }
820
+
821
+ this.chunkMeshManager.updateSection(update.key, update.geometry)
822
+ this.updatePosDataChunk(update.key)
823
+ }
824
+ }
825
+
826
+ private clearPendingSectionUpdatesForChunk(x: number, z: number) {
827
+ for (const key of [...this.pendingSectionUpdates.keys()]) {
828
+ if (key.startsWith(`${x},`) && key.endsWith(`,${z}`)) {
829
+ this.pendingSectionUpdates.delete(key)
830
+ this.pendingSectionBufferStartTimes.delete(key)
831
+ }
832
+ }
747
833
  }
748
834
 
749
835
  handleWorkerMessage(data: { geometry: MesherGeometryOutput, key, type }): void {
750
836
  if (data.type === 'geometry') {
751
837
  const chunkCoords = data.key.split(',')
752
- if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
838
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
839
+ if (!this.loadedChunks[chunkKey] || !this.active) {
840
+ this.pendingSectionUpdates.delete(data.key)
841
+ this.pendingSectionBufferStartTimes.delete(data.key)
842
+ return
843
+ }
844
+
845
+ if (this.sectionObjects[data.key]) {
846
+ this.pendingSectionUpdates.set(data.key, data)
847
+ // Per-section deadline: only set if we don't already have one, so
848
+ // repeated updates to the same section don't postpone its flush.
849
+ if (!this.pendingSectionBufferStartTimes.has(data.key)) {
850
+ this.pendingSectionBufferStartTimes.set(data.key, performance.now())
851
+ }
852
+ return
853
+ }
854
+
855
+ if (!data.geometry.positions.length) {
856
+ this.chunkMeshManager.releaseSection(data.key)
857
+ return
858
+ }
753
859
  this.chunkMeshManager.updateSection(data.key, data.geometry)
754
860
  this.updatePosDataChunk(data.key)
755
861
  }
@@ -1052,6 +1158,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1052
1158
  chunksRenderDistanceOverride !== undefined
1053
1159
  ) {
1054
1160
  for (const [key, object] of Object.entries(this.sectionObjects)) {
1161
+ if (object._waitingForChunkDisplay) continue
1055
1162
  const [x, y, z] = key.split(',').map(Number)
1056
1163
  const isVisible =
1057
1164
  // eslint-disable-next-line no-constant-binary-expression, sonarjs/no-redundant-boolean
@@ -1064,9 +1171,11 @@ export class WorldRendererThree extends WorldRendererCommon {
1064
1171
  object.visible = isVisible
1065
1172
  }
1066
1173
  } else {
1067
- for (const object of Object.values(this.sectionObjects)) {
1068
- object.visible = true
1069
- }
1174
+ // No debug visibility override active — defer to the manager so the
1175
+ // performance-based override distance (set by `recordRenderTime` /
1176
+ // `autoLowerRenderDistance`) is honored, instead of force-showing every
1177
+ // section every frame and clobbering it.
1178
+ this.chunkMeshManager.updateSectionsVisibility()
1070
1179
  }
1071
1180
  }
1072
1181
 
@@ -1101,8 +1210,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1101
1210
 
1102
1211
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
1103
1212
  const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
1104
- // ChunkMeshManager applies updates immediately, no pending updates to flush
1105
- this.renderer.render(this.realScene, cam)
1213
+ this.applyPendingSectionUpdates()
1214
+ this.renderer.render(this.scene, cam)
1106
1215
 
1107
1216
  if (
1108
1217
  this.displayOptions.inWorldRenderingConfig.showHand &&
@@ -1237,18 +1346,16 @@ export class WorldRendererThree extends WorldRendererCommon {
1237
1346
  }
1238
1347
 
1239
1348
  updateShowChunksBorder(value: boolean) {
1240
- for (const object of Object.values(this.sectionObjects)) {
1241
- for (const child of object.children) {
1242
- if (child.name === 'helper') {
1243
- child.visible = value
1244
- }
1245
- }
1246
- }
1349
+ // Lazily create helpers on the first toggle (they are not created upfront
1350
+ // for sections streamed in while the option was off).
1351
+ this.chunkMeshManager.updateAllBoxHelpers(value)
1247
1352
  }
1248
1353
 
1249
1354
  resetWorld() {
1250
1355
  super.resetWorld()
1251
1356
 
1357
+ this.pendingSectionUpdates.clear()
1358
+ this.pendingSectionBufferStartTimes.clear()
1252
1359
  this.chunkMeshManager.dispose()
1253
1360
  this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
1254
1361
 
@@ -1279,6 +1386,11 @@ export class WorldRendererThree extends WorldRendererCommon {
1279
1386
  textures[key].dispose()
1280
1387
  delete textures[key]
1281
1388
  }
1389
+ // Sign / head textures moved to ChunkMeshManager.signHeadsRenderer in PR
1390
+ // #16; without invalidating that cache here, sign edits (and any other
1391
+ // block-entity NBT change picked up via setSectionDirty) would re-render
1392
+ // with the stale cached canvas until a full world reset.
1393
+ this.chunkMeshManager.cleanSignChunkTextures(x, z)
1282
1394
  }
1283
1395
 
1284
1396
  readdChunks() {
@@ -1303,6 +1415,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1303
1415
  super.removeColumn(x, z)
1304
1416
 
1305
1417
  this.cleanChunkTextures(x, z)
1418
+ this.clearPendingSectionUpdatesForChunk(x, z)
1306
1419
  const sectionHeight = this.getSectionHeight()
1307
1420
  const worldMinY = this.worldMinYRender
1308
1421
  for (let y = worldMinY; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
@@ -1333,6 +1446,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1333
1446
  }
1334
1447
 
1335
1448
  destroy(): void {
1449
+ this.pendingSectionUpdates.clear()
1450
+ this.pendingSectionBufferStartTimes.clear()
1336
1451
  this.chunkMeshManager.dispose()
1337
1452
  this.disposeModules()
1338
1453
  this.fireworksLegacy.destroy()