minecraft-renderer 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -64,7 +64,7 @@ export class ChunkMeshManager {
64
64
 
65
65
  constructor (
66
66
  public worldRenderer: WorldRendererThree,
67
- public scene: THREE.Group,
67
+ public scene: THREE.Object3D,
68
68
  public material: THREE.Material,
69
69
  public worldHeight: number,
70
70
  viewDistance = 3,
@@ -1,7 +1,6 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
  import * as tweenJs from '@tweenjs/tween.js'
4
- import PrismarineItem from 'prismarine-item'
5
4
  import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
6
5
  import { BlockModel } from 'mc-assets'
7
6
  import { DebugGui } from '../lib/DebugGui'
@@ -17,8 +16,14 @@ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
17
16
  import { IndexedData } from 'minecraft-data'
18
17
  import { WorldRendererConfig } from '../graphicsBackend'
19
18
  import { computeCameraBob, type CameraBobInput } from '../lib/cameraBobbing'
19
+ import { getFirstPersonItemSpecificProps, getHandItemRenderKey } from './holdingBlockItemIdentity'
20
20
 
21
21
  const _tempMat = new THREE.Matrix4()
22
+ const wrapPi = (a: number) => {
23
+ a = (a + Math.PI) % (Math.PI * 2)
24
+ if (a < 0) a += Math.PI * 2
25
+ return a - Math.PI
26
+ }
22
27
 
23
28
  // Vanilla renderPlayerArm transform chain
24
29
  function buildBareHandMatrix(swingProgress: number, equipProgress: number): THREE.Matrix4 {
@@ -94,6 +99,7 @@ export default class HoldingBlock implements IHoldingBlock {
94
99
  equipProgress = 0 // 0 = fully visible, 1 = hidden
95
100
  stopUpdate = false
96
101
  lastHeldItem: HandItemBlock | undefined
102
+ lastHeldItemRenderKey: string | undefined
97
103
  currentDisplayType: 'hand' | 'item' | 'block' = 'hand'
98
104
  isSwinging = false
99
105
  nextIterStopCallbacks: Array<() => void> | undefined
@@ -116,6 +122,7 @@ export default class HoldingBlock implements IHoldingBlock {
116
122
 
117
123
  constructor(public worldRenderer: WorldRendererThree, public offHand = false) {
118
124
  this.initCameraGroup()
125
+ this.swingAnimator = new HandSwingAnimator()
119
126
  this.unsubs.push(
120
127
  this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
121
128
  if (!this.offHand) {
@@ -332,44 +339,32 @@ export default class HoldingBlock implements IHoldingBlock {
332
339
  }
333
340
 
334
341
  isDifferentItem(block: HandItemBlock | undefined) {
335
- const Item = PrismarineItem(this.worldRenderer.version)
336
- if (!this.lastHeldItem) {
337
- return true
338
- }
339
- if (this.lastHeldItem.name !== block?.name) {
340
- return true
341
- }
342
- // eslint-disable-next-line sonarjs/prefer-single-boolean-return
343
- if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
344
- return true
345
- }
346
-
347
- return false
342
+ return this.lastHeldItemRenderKey !== getHandItemRenderKey(this.worldRenderer, block)
348
343
  }
349
344
 
350
345
  updateCameraGroup() {
351
346
  if (this.stopUpdate) return
352
347
  const { camera } = this
353
348
 
354
- // Hand rotation momentum (xBob/yBob) — vanilla Minecraft inertia effect
355
- // Use base rotation from CameraShake (actual player view angles)
356
349
  const now = performance.now()
357
350
  const baseRotation = this.worldRenderer.cameraShake.getBaseRotation()
358
351
  const actualPitch = baseRotation.pitch
359
352
  const actualYaw = baseRotation.yaw
353
+
360
354
  if (this.lastBobUpdateTime === 0) {
361
355
  this.xBob = actualPitch
362
356
  this.yBob = actualYaw
363
- this.lastBobUpdateTime = now
364
357
  } else {
365
358
  const dt = Math.min((now - this.lastBobUpdateTime) / 1000, 0.1)
366
- this.lastBobUpdateTime = now
367
- const factor = 1 - Math.pow(0.5, dt * 20)
368
- this.xBob += (actualPitch - this.xBob) * factor
369
- this.yBob += (actualYaw - this.yBob) * factor
359
+ const pitchFactor = 1 - Math.pow(0.5, dt * 28)
360
+ const yawFactor = 1 - Math.pow(0.5, dt * 36)
361
+ this.xBob += (actualPitch - this.xBob) * pitchFactor
362
+ this.yBob += wrapPi(actualYaw - this.yBob) * yawFactor
370
363
  }
371
- const pitchOffset = (actualPitch - this.xBob) * -0.1
372
- const yawOffset = (actualYaw - this.yBob) * -0.1
364
+
365
+ this.lastBobUpdateTime = now
366
+ const pitchOffset = (actualPitch - this.xBob) * -0.05
367
+ const yawOffset = wrapPi(actualYaw - this.yBob) * -0.035
373
368
 
374
369
  this.cameraGroup.position.copy(camera.position)
375
370
  this.cameraGroup.rotation.copy(camera.rotation)
@@ -431,11 +426,7 @@ export default class HoldingBlock implements IHoldingBlock {
431
426
  const result = this.worldRenderer.entities.getItemMesh({
432
427
  ...handItem.fullItem,
433
428
  itemId: handItem.id,
434
- }, {
435
- 'minecraft:display_context': 'firstperson',
436
- 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
437
- 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
438
- }, false, this.lastItemModelName)
429
+ }, getFirstPersonItemSpecificProps(this.worldRenderer), false, this.lastItemModelName)
439
430
  if (result) {
440
431
  const { mesh: itemMesh, isBlock, modelName } = result
441
432
  if (isBlock) {
@@ -487,8 +478,11 @@ export default class HoldingBlock implements IHoldingBlock {
487
478
  this.holdingBlock?.removeFromParent()
488
479
  this.holdingBlock = undefined
489
480
  this.currentDisplayType = 'hand'
490
- this.swingAnimator?.stopSwing()
491
- this.swingAnimator = undefined
481
+ const swingAnimator = this.swingAnimator
482
+ swingAnimator?.stopSwing()
483
+ if (swingAnimator) {
484
+ swingAnimator.type = 'hand'
485
+ }
492
486
  this.idleAnimator = undefined
493
487
  return
494
488
  }
@@ -516,10 +510,14 @@ export default class HoldingBlock implements IHoldingBlock {
516
510
 
517
511
  switchRequest = 0
518
512
  async setNewItem(handItem?: HandItemBlock) {
519
- if (!this.isDifferentItem(handItem)) return
513
+ const nextRenderKey = getHandItemRenderKey(this.worldRenderer, handItem)
514
+ const itemChanged = this.lastHeldItemRenderKey !== nextRenderKey
515
+ this.lastHeldItem = handItem
516
+ if (!itemChanged) return
517
+
518
+ this.lastHeldItemRenderKey = nextRenderKey
520
519
  this.lastItemModelName = undefined
521
520
  const switchRequest = ++this.switchRequest
522
- this.lastHeldItem = handItem
523
521
 
524
522
  let playAppearAnimation = false
525
523
  if (this.holdingBlock) {
@@ -535,7 +533,11 @@ export default class HoldingBlock implements IHoldingBlock {
535
533
 
536
534
  if (!handItem) {
537
535
  this.currentDisplayType = 'hand'
538
- this.swingAnimator = undefined
536
+ const swingAnimator = this.swingAnimator
537
+ swingAnimator?.stopSwing()
538
+ if (swingAnimator) {
539
+ swingAnimator.type = 'hand'
540
+ }
539
541
  this.idleAnimator = undefined
540
542
  this.blockSwapAnimation = undefined
541
543
  return
@@ -553,8 +555,10 @@ export default class HoldingBlock implements IHoldingBlock {
553
555
  await this.playBlockSwapAnimation('appeared')
554
556
  }
555
557
 
556
- this.swingAnimator = new HandSwingAnimator()
557
- this.swingAnimator.type = result.type
558
+ const swingAnimator = this.swingAnimator
559
+ if (swingAnimator) {
560
+ swingAnimator.type = result.type
561
+ }
558
562
  // Idle animation disabled — walking bob is handled by vanilla bobView applied to cameraGroup
559
563
  this.idleAnimator = undefined
560
564
  }
@@ -0,0 +1,43 @@
1
+ //@ts-nocheck
2
+ import { expect, test, vi } from 'vitest'
3
+ import { getHandItemRenderKey } from './holdingBlockItemIdentity'
4
+
5
+ test('hand item render key ignores live use state but keeps first-person display context', () => {
6
+ const getItemRenderData = vi.fn(() => ({ modelName: 'item/bow' }))
7
+ const worldRenderer = {
8
+ playerStateReactive: {
9
+ itemUsageTicks: 20,
10
+ },
11
+ resourcesManager: {
12
+ currentResources: {},
13
+ },
14
+ getItemRenderData,
15
+ } as any
16
+
17
+ const handItem = {
18
+ type: 'item',
19
+ id: 261,
20
+ name: 'minecraft:bow',
21
+ fullItem: {
22
+ count: 1,
23
+ },
24
+ } as const
25
+
26
+ const activeKey = getHandItemRenderKey(worldRenderer, handItem)
27
+ worldRenderer.playerStateReactive.itemUsageTicks = 0
28
+ const idleKey = getHandItemRenderKey(worldRenderer, handItem)
29
+
30
+ expect(activeKey).toBe(idleKey)
31
+ expect(getItemRenderData).toHaveBeenNthCalledWith(1, {
32
+ ...handItem.fullItem,
33
+ itemId: handItem.id,
34
+ }, {
35
+ 'minecraft:display_context': 'firstperson',
36
+ })
37
+ expect(getItemRenderData).toHaveBeenNthCalledWith(2, {
38
+ ...handItem.fullItem,
39
+ itemId: handItem.id,
40
+ }, {
41
+ 'minecraft:display_context': 'firstperson',
42
+ })
43
+ })
@@ -0,0 +1,30 @@
1
+ //@ts-nocheck
2
+ import type { HandItemBlock, ItemSpecificContextProperties } from '../playerState/types'
3
+ import type { WorldRendererThree } from './worldRendererThree'
4
+
5
+ export const getFirstPersonItemSpecificProps = (worldRenderer: WorldRendererThree): ItemSpecificContextProperties => ({
6
+ 'minecraft:display_context': 'firstperson',
7
+ 'minecraft:use_duration': worldRenderer.playerStateReactive.itemUsageTicks,
8
+ 'minecraft:using_item': !!worldRenderer.playerStateReactive.itemUsageTicks,
9
+ })
10
+
11
+ const getFirstPersonItemIdentityProps = (): ItemSpecificContextProperties => ({
12
+ 'minecraft:display_context': 'firstperson',
13
+ })
14
+
15
+ export const getHandItemRenderKey = (worldRenderer: WorldRendererThree, handItem?: HandItemBlock) => {
16
+ if (!handItem) return 'empty'
17
+ if (handItem.type === 'hand') return 'hand'
18
+
19
+ const itemIdentifier = handItem.name ?? (handItem.id !== undefined ? `#${handItem.id}` : 'unknown')
20
+ if (!worldRenderer.resourcesManager.currentResources) {
21
+ return `${handItem.type}:${itemIdentifier}`
22
+ }
23
+
24
+ const renderData = worldRenderer.getItemRenderData({
25
+ ...handItem.fullItem,
26
+ itemId: handItem.id,
27
+ }, getFirstPersonItemIdentityProps())
28
+
29
+ return `${handItem.type}:${itemIdentifier}:${renderData.modelName}`
30
+ }
@@ -1,7 +1,6 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
  import * as tweenJs from '@tweenjs/tween.js'
4
- import PrismarineItem from 'prismarine-item'
5
4
  import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
6
5
  import { BlockModel } from 'mc-assets'
7
6
  import { DebugGui } from '../lib/DebugGui'
@@ -17,6 +16,7 @@ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
17
16
  import { IndexedData } from 'minecraft-data'
18
17
  import { WorldRendererConfig } from '../graphicsBackend'
19
18
  import { IHoldingBlock } from './holdingBlockTypes'
19
+ import { getFirstPersonItemSpecificProps, getHandItemRenderKey } from './holdingBlockItemIdentity'
20
20
 
21
21
  const rotationPositionData = {
22
22
  itemRight: {
@@ -103,6 +103,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
103
103
  camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100)
104
104
  stopUpdate = false
105
105
  lastHeldItem: HandItemBlock | undefined
106
+ lastHeldItemRenderKey: string | undefined
106
107
  isSwinging = false
107
108
  nextIterStopCallbacks: Array<() => void> | undefined
108
109
  idleAnimator: HandIdleAnimator | undefined
@@ -304,19 +305,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
304
305
  }
305
306
 
306
307
  isDifferentItem(block: HandItemBlock | undefined) {
307
- const Item = PrismarineItem(this.worldRenderer.version)
308
- if (!this.lastHeldItem) {
309
- return true
310
- }
311
- if (this.lastHeldItem.name !== block?.name) {
312
- return true
313
- }
314
- // eslint-disable-next-line sonarjs/prefer-single-boolean-return
315
- if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
316
- return true
317
- }
318
-
319
- return false
308
+ return this.lastHeldItemRenderKey !== getHandItemRenderKey(this.worldRenderer, block)
320
309
  }
321
310
 
322
311
  updateCameraGroup() {
@@ -355,11 +344,7 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
355
344
  const result = this.worldRenderer.entities.getItemMesh({
356
345
  ...handItem.fullItem,
357
346
  itemId: handItem.id,
358
- }, {
359
- 'minecraft:display_context': 'firstperson',
360
- 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
361
- 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
362
- }, false, this.lastItemModelName)
347
+ }, getFirstPersonItemSpecificProps(this.worldRenderer), false, this.lastItemModelName)
363
348
  if (result) {
364
349
  const { mesh: itemMesh, isBlock, modelName } = result
365
350
  if (isBlock) {
@@ -421,10 +406,14 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
421
406
 
422
407
  switchRequest = 0
423
408
  async setNewItem(handItem?: HandItemBlock) {
424
- if (!this.isDifferentItem(handItem)) return
409
+ const nextRenderKey = getHandItemRenderKey(this.worldRenderer, handItem)
410
+ const itemChanged = this.lastHeldItemRenderKey !== nextRenderKey
411
+ this.lastHeldItem = handItem
412
+ if (!itemChanged) return
413
+
414
+ this.lastHeldItemRenderKey = nextRenderKey
425
415
  this.lastItemModelName = undefined
426
416
  const switchRequest = ++this.switchRequest
427
- this.lastHeldItem = handItem
428
417
  let playAppearAnimation = false
429
418
  if (this.holdingBlock) {
430
419
  // play disappear animation
@@ -472,7 +461,8 @@ export default class HoldingBlockLegacy implements IHoldingBlock {
472
461
  await this.playBlockSwapAnimation('appeared')
473
462
  }
474
463
 
475
- this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
464
+ this.swingAnimator ??= new HandSwingAnimator(this.holdingBlockInnerGroup)
465
+ this.swingAnimator.setHandMesh(this.holdingBlockInnerGroup)
476
466
  this.swingAnimator.type = result.type
477
467
  if (this.config.viewBobbing) {
478
468
  this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
@@ -783,9 +773,9 @@ class HandSwingAnimator {
783
773
  private lastTime = 0
784
774
  private isAnimating = false
785
775
  private stopRequested = false
786
- private readonly originalRotation: THREE.Euler
787
- private readonly originalPosition: THREE.Vector3
788
- private readonly originalScale: THREE.Vector3
776
+ private originalRotation: THREE.Euler
777
+ private originalPosition: THREE.Vector3
778
+ private originalScale: THREE.Vector3
789
779
 
790
780
  readonly debugParams = {
791
781
  // Animation timing
@@ -849,6 +839,13 @@ class HandSwingAnimator {
849
839
  // this.debugGui.activate()
850
840
  }
851
841
 
842
+ setHandMesh(handMesh: THREE.Object3D) {
843
+ this.handMesh = handMesh
844
+ this.originalRotation.copy(handMesh.rotation)
845
+ this.originalPosition.copy(handMesh.position)
846
+ this.originalScale.copy(handMesh.scale)
847
+ }
848
+
852
849
  update() {
853
850
  if (!this.isAnimating && !this.debugParams.animationStage) {
854
851
  // If not animating, ensure we're at original position
@@ -58,7 +58,7 @@ export class SciFiWorldRevealModule implements RendererModuleController {
58
58
  // Store original methods for patching
59
59
  private originalFinishChunk: ((chunkKey: string) => void) | null = null
60
60
  private originalDestroy: (() => void) | null = null
61
- private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.Group) | null = null
61
+ private originalSceneAdd: ((...object: THREE.Object3D[]) => THREE.Scene) | null = null
62
62
  private originalHandleWorkerMessage: ((data: { geometry: MesherGeometryOutput; key: string; type: string }) => void) | null = null
63
63
 
64
64
  private configEnabled = true
@@ -163,7 +163,7 @@ export class SciFiWorldRevealModule implements RendererModuleController {
163
163
 
164
164
  // Patch scene.add to intercept mesh additions
165
165
  this.originalSceneAdd = wr.scene.add.bind(wr.scene)
166
- wr.scene.add = (...objects: THREE.Object3D[]): THREE.Group => {
166
+ wr.scene.add = (...objects: THREE.Object3D[]): THREE.Scene => {
167
167
  // Call original add first
168
168
  const result = this.originalSceneAdd!(...objects)
169
169
 
@@ -174,7 +174,7 @@ export class SceneOrigin {
174
174
  /** Untrack an Object3D and remove it from the scene */
175
175
  removeAndUntrack(obj: Object3D): void {
176
176
  this.untrack(obj)
177
- this.scene.remove(obj)
177
+ obj.removeFromParent()
178
178
  }
179
179
 
180
180
  /** Untrack an Object3D and all its descendants, then remove from the scene */
@@ -182,7 +182,7 @@ export class SceneOrigin {
182
182
  obj.traverse((child) => {
183
183
  this.untrack(child)
184
184
  })
185
- this.scene.remove(obj)
185
+ obj.removeFromParent()
186
186
  }
187
187
 
188
188
  /** Get stored world position for a tracked object */
@@ -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)
@@ -79,6 +81,9 @@ 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
+ private pendingSectionBufferStartTime: number | null = null
86
+ private static readonly MAX_SECTION_UPDATE_BUFFER_MS = 500
82
87
  // Memory usage tracking (in bytes)
83
88
  get estimatedMemoryUsage() {
84
89
  return this.chunkMeshManager.getEstimatedMemoryUsage().total
@@ -107,7 +112,7 @@ export class WorldRendererThree extends WorldRendererCommon {
107
112
  DEBUG_RAYCAST = false
108
113
  skyboxRenderer: SkyboxRenderer
109
114
  fireworks: FireworksManager
110
- sceneOrigin = new SceneOrigin(this.realScene)
115
+ sceneOrigin = new SceneOrigin(this.scene)
111
116
  /** Camera world position stored in float64 (JS number) for precision */
112
117
  cameraWorldPos = { x: 0, y: 0, z: 0 }
113
118
 
@@ -450,20 +455,19 @@ export class WorldRendererThree extends WorldRendererCommon {
450
455
  this.cameraWorldPos.y = 0
451
456
  this.cameraWorldPos.z = 0
452
457
 
453
- this.realScene.matrixAutoUpdate = false // for perf
454
- this.realScene.background = new THREE.Color(this.initOptions.config.sceneBackground)
455
- this.realScene.add(this.ambientLight)
458
+ this.scene.matrixAutoUpdate = false // for perf
459
+ this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
460
+ this.scene.add(this.ambientLight)
456
461
  this.directionalLight.position.set(1, 1, 0.5).normalize()
457
462
  this.directionalLight.castShadow = true
458
- this.realScene.add(this.directionalLight)
463
+ this.scene.add(this.directionalLight)
459
464
 
460
465
  const size = this.renderer.getSize(new THREE.Vector2())
461
466
  this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
462
467
  this._wrapCameraPositionWithWarning()
463
468
  this.cameraContainer = new THREE.Object3D()
464
469
  this.cameraContainer.add(this.camera)
465
- this.realScene.add(this.cameraContainer)
466
- this.realScene.add(this.scene)
470
+ this.scene.add(this.cameraContainer)
467
471
  }
468
472
 
469
473
  override watchReactivePlayerState() {
@@ -743,13 +747,94 @@ export class WorldRendererThree extends WorldRendererCommon {
743
747
  }
744
748
 
745
749
  finishChunk(chunkKey: string) {
746
- // ChunkMeshManager applies updates immediately, no buffering needed
750
+ // Existing sections are buffered and flushed from applyPendingSectionUpdates().
751
+ }
752
+
753
+ private applyPendingSectionUpdates() {
754
+ if (this.pendingSectionUpdates.size === 0) return
755
+
756
+ const now = performance.now()
757
+ const sinceFirst = now - (this.pendingSectionBufferStartTime ?? now)
758
+
759
+ if (sinceFirst < WorldRendererThree.MAX_SECTION_UPDATE_BUFFER_MS) {
760
+ const sectionHeight = this.getSectionHeight()
761
+ for (const key of this.pendingSectionUpdates.keys()) {
762
+ const [sx, sy, sz] = key.split(',').map(Number)
763
+ const neighborKeys = [
764
+ `${sx - 16},${sy},${sz}`, `${sx + 16},${sy},${sz}`,
765
+ `${sx},${sy - sectionHeight},${sz}`, `${sx},${sy + sectionHeight},${sz}`,
766
+ `${sx},${sy},${sz - 16}`, `${sx},${sy},${sz + 16}`,
767
+ ]
768
+
769
+ for (const neighborKey of neighborKeys) {
770
+ if (
771
+ this.sectionsWaiting.has(neighborKey) &&
772
+ !this.pendingSectionUpdates.has(neighborKey) &&
773
+ this.sectionObjects[neighborKey]
774
+ ) {
775
+ return
776
+ }
777
+ }
778
+ }
779
+ }
780
+
781
+ const updates = [...this.pendingSectionUpdates.values()]
782
+ this.pendingSectionUpdates.clear()
783
+ this.pendingSectionBufferStartTime = null
784
+
785
+ for (const update of updates) {
786
+ const chunkCoords = update.key.split(',')
787
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
788
+
789
+ if (!this.loadedChunks[chunkKey] || !this.active) {
790
+ this.chunkMeshManager.releaseSection(update.key)
791
+ continue
792
+ }
793
+
794
+ if (!update.geometry.positions.length) {
795
+ this.chunkMeshManager.releaseSection(update.key)
796
+ continue
797
+ }
798
+
799
+ this.chunkMeshManager.updateSection(update.key, update.geometry)
800
+ this.updatePosDataChunk(update.key)
801
+ }
802
+ }
803
+
804
+ private clearPendingSectionUpdatesForChunk(x: number, z: number) {
805
+ for (const key of [...this.pendingSectionUpdates.keys()]) {
806
+ if (key.startsWith(`${x},`) && key.endsWith(`,${z}`)) {
807
+ this.pendingSectionUpdates.delete(key)
808
+ }
809
+ }
810
+
811
+ if (this.pendingSectionUpdates.size === 0) {
812
+ this.pendingSectionBufferStartTime = null
813
+ }
747
814
  }
748
815
 
749
816
  handleWorkerMessage(data: { geometry: MesherGeometryOutput, key, type }): void {
750
817
  if (data.type === 'geometry') {
751
818
  const chunkCoords = data.key.split(',')
752
- if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
819
+ const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
820
+ if (!this.loadedChunks[chunkKey] || !this.active) {
821
+ this.pendingSectionUpdates.delete(data.key)
822
+ if (this.pendingSectionUpdates.size === 0) {
823
+ this.pendingSectionBufferStartTime = null
824
+ }
825
+ return
826
+ }
827
+
828
+ if (this.sectionObjects[data.key]) {
829
+ this.pendingSectionUpdates.set(data.key, data)
830
+ this.pendingSectionBufferStartTime ??= performance.now()
831
+ return
832
+ }
833
+
834
+ if (!data.geometry.positions.length) {
835
+ this.chunkMeshManager.releaseSection(data.key)
836
+ return
837
+ }
753
838
  this.chunkMeshManager.updateSection(data.key, data.geometry)
754
839
  this.updatePosDataChunk(data.key)
755
840
  }
@@ -1101,8 +1186,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1101
1186
 
1102
1187
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
1103
1188
  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)
1189
+ this.applyPendingSectionUpdates()
1190
+ this.renderer.render(this.scene, cam)
1106
1191
 
1107
1192
  if (
1108
1193
  this.displayOptions.inWorldRenderingConfig.showHand &&
@@ -1249,6 +1334,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1249
1334
  resetWorld() {
1250
1335
  super.resetWorld()
1251
1336
 
1337
+ this.pendingSectionUpdates.clear()
1338
+ this.pendingSectionBufferStartTime = null
1252
1339
  this.chunkMeshManager.dispose()
1253
1340
  this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
1254
1341
 
@@ -1303,6 +1390,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1303
1390
  super.removeColumn(x, z)
1304
1391
 
1305
1392
  this.cleanChunkTextures(x, z)
1393
+ this.clearPendingSectionUpdatesForChunk(x, z)
1306
1394
  const sectionHeight = this.getSectionHeight()
1307
1395
  const worldMinY = this.worldMinYRender
1308
1396
  for (let y = worldMinY; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
@@ -1333,6 +1421,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1333
1421
  }
1334
1422
 
1335
1423
  destroy(): void {
1424
+ this.pendingSectionUpdates.clear()
1425
+ this.pendingSectionBufferStartTime = null
1336
1426
  this.chunkMeshManager.dispose()
1337
1427
  this.disposeModules()
1338
1428
  this.fireworksLegacy.destroy()