minecraft-renderer 0.1.107 → 0.1.109

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 (95) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +84 -84
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +517 -517
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +5 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +13 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +3 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  19. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  20. package/src/lib/worldrendererCommon.ts +329 -67
  21. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  22. package/src/lighting/cache/rendererLightCache.ts +248 -0
  23. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  25. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  26. package/src/lighting/debug/clientLightProbe.ts +20 -0
  27. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  28. package/src/lighting/debug/clientLightTrace.ts +335 -0
  29. package/src/lighting/entity/entityLightController.test.ts +216 -0
  30. package/src/lighting/entity/entityLightController.ts +322 -0
  31. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  32. package/src/lighting/owner/clientLightOwner.ts +523 -0
  33. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  34. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  35. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  36. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  37. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  38. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  39. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  40. package/src/lighting/tables/lightTables1171.ts +59 -0
  41. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  42. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  43. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  44. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  45. package/src/mesher-shared/lightNibblePack.ts +26 -0
  46. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  47. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  48. package/src/mesher-shared/shared.ts +48 -1
  49. package/src/three/chunkMeshManager.ts +26 -0
  50. package/src/three/cloudCover.ts +22 -0
  51. package/src/three/cloudGeometry.ts +71 -0
  52. package/src/three/entities.ts +77 -11
  53. package/src/three/entity/EntityMesh.ts +1 -0
  54. package/src/three/entity/legWalkAnimation.test.ts +11 -2
  55. package/src/three/entity/legWalkAnimation.ts +1 -9
  56. package/src/three/globalBlockBuffer.ts +94 -2
  57. package/src/three/globalLegacyBuffer.ts +94 -2
  58. package/src/three/graphicsBackendBase.ts +2 -9
  59. package/src/three/modules/clouds.ts +98 -75
  60. package/src/three/tests/cloudGeometry.test.ts +87 -0
  61. package/src/three/tests/clouds.test.ts +65 -14
  62. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  63. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  64. package/src/three/worldRendererThree.ts +75 -34
  65. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  66. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  67. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  68. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  69. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  70. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  71. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  72. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  73. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  74. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  75. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  76. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  77. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  78. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  79. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  80. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  81. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  82. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  83. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  84. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  85. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  86. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  87. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  88. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  89. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  90. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  91. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  92. package/src/worldView/types.ts +1 -0
  93. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  94. package/src/worldView/worldView.spiral.test.ts +1 -1
  95. package/src/worldView/worldView.ts +46 -1
@@ -12,6 +12,7 @@ import {
12
12
  import { VERTICES_PER_FACE, computeSectionOriginRel } from './shaders/cubeBlockShader'
13
13
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
14
14
  import { packWord2Empty } from '../wasm-mesher/bridge/shaderCubeBridge'
15
+ import { isClientLightTraceEnabled, recordClientLightTrace, recordClientLightTraceGpuSample } from '../lighting/debug/clientLightTrace'
15
16
 
16
17
  type WebGLRendererInternals = THREE.WebGLRenderer & {
17
18
  properties: {
@@ -162,9 +163,9 @@ export class GlobalBlockBuffer {
162
163
  getSectionDrawStart(sectionKey: string): number | undefined {
163
164
  const slot = this.sectionSlots.get(sectionKey)
164
165
  if (!slot) return undefined
165
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
166
166
  const replace = this.pendingReplace.get(sectionKey)
167
167
  if (replace) return replace.oldStart
168
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
168
169
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
169
170
  return slot.start
170
171
  }
@@ -172,13 +173,37 @@ export class GlobalBlockBuffer {
172
173
  getSectionDrawCount(sectionKey: string): number | undefined {
173
174
  const slot = this.sectionSlots.get(sectionKey)
174
175
  if (!slot) return undefined
175
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
176
176
  const replace = this.pendingReplace.get(sectionKey)
177
177
  if (replace) return replace.oldCount
178
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
178
179
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
179
180
  return slot.count
180
181
  }
181
182
 
183
+ getGpuSlotTrace(sectionKey: string):
184
+ | {
185
+ displayedStart?: number
186
+ displayedCount?: number
187
+ candidateStart: number
188
+ candidateCount: number
189
+ pendingReplace: boolean
190
+ pendingMove: boolean
191
+ unuploadedRanges: number
192
+ }
193
+ | undefined {
194
+ const slot = this.sectionSlots.get(sectionKey)
195
+ if (!slot) return undefined
196
+ return {
197
+ displayedStart: this.getSectionDrawStart(sectionKey),
198
+ displayedCount: this.getSectionDrawCount(sectionKey),
199
+ candidateStart: slot.start,
200
+ candidateCount: slot.count,
201
+ pendingReplace: this.pendingReplace.has(sectionKey),
202
+ pendingMove: this.pendingMove?.key === sectionKey,
203
+ unuploadedRanges: this.pendingRanges.length
204
+ }
205
+ }
206
+
182
207
  getUploadEpoch(): number {
183
208
  return this.uploadEpoch
184
209
  }
@@ -282,6 +307,20 @@ export class GlobalBlockBuffer {
282
307
  this.markDirty(slot.start, slot.start + faceCount - 1)
283
308
  this.mesh.geometry.instanceCount = this.highWatermark
284
309
  this.layoutVersion++
310
+ if (isClientLightTraceEnabled()) {
311
+ const gpu = this.getGpuSlotTrace(sectionKey)
312
+ recordClientLightTrace({
313
+ phase: 'gpuStaged',
314
+ sectionKey,
315
+ displayedStart: gpu?.displayedStart,
316
+ displayedCount: gpu?.displayedCount,
317
+ candidateStart: gpu?.candidateStart,
318
+ candidateCount: gpu?.candidateCount,
319
+ pendingReplace: gpu?.pendingReplace,
320
+ pendingMove: gpu?.pendingMove,
321
+ unuploadedRanges: gpu?.unuploadedRanges
322
+ })
323
+ }
285
324
  }
286
325
 
287
326
  getLayoutVersion(): number {
@@ -388,6 +427,20 @@ export class GlobalBlockBuffer {
388
427
  this.markDirty(newStart, newStart + section.count - 1)
389
428
  this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
390
429
  this.layoutVersion++
430
+ if (isClientLightTraceEnabled()) {
431
+ const gpu = this.getGpuSlotTrace(section.key)
432
+ recordClientLightTrace({
433
+ phase: 'gpuStaged',
434
+ sectionKey: section.key,
435
+ displayedStart: gpu?.displayedStart,
436
+ displayedCount: gpu?.displayedCount,
437
+ candidateStart: gpu?.candidateStart,
438
+ candidateCount: gpu?.candidateCount,
439
+ pendingReplace: gpu?.pendingReplace,
440
+ pendingMove: true,
441
+ unuploadedRanges: gpu?.unuploadedRanges
442
+ })
443
+ }
391
444
  }
392
445
 
393
446
  uploadDirtyRange(): void {
@@ -411,6 +464,13 @@ export class GlobalBlockBuffer {
411
464
  r.start = offset + count
412
465
  }
413
466
  this.uploadEpoch++
467
+ if (isClientLightTraceEnabled()) {
468
+ recordClientLightTraceGpuSample('gpuUploaded', this.highWatermark, () => ({
469
+ phase: 'gpuUploaded',
470
+ unuploadedRanges: this.pendingRanges.length,
471
+ drawableFaces: this.highWatermark
472
+ }))
473
+ }
414
474
  }
415
475
 
416
476
  setCameraOrigin(renderOrigin: RenderOrigin, x: number, y: number, z: number): void {
@@ -608,6 +668,9 @@ export class GlobalBlockBuffer {
608
668
  private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
609
669
  const sections: Array<{ key: string; start: number; count: number }> = []
610
670
  for (const [key, slot] of this.sectionSlots) {
671
+ if (this.pendingReplace.has(key)) continue
672
+ if (this.pendingMove?.key === key) continue
673
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) continue
611
674
  sections.push({ key, start: slot.start, count: slot.count })
612
675
  }
613
676
  if (sections.length === 0) return undefined
@@ -684,11 +747,26 @@ export class GlobalBlockBuffer {
684
747
  this.mesh.geometry.instanceCount = this.highWatermark
685
748
  this.layoutVersion++
686
749
  this.uploadEpoch++
750
+ if (isClientLightTraceEnabled()) {
751
+ const gpu = this.getGpuSlotTrace(key)
752
+ recordClientLightTrace({
753
+ phase: 'gpuCommitted',
754
+ sectionKey: key,
755
+ displayedStart: gpu?.displayedStart,
756
+ displayedCount: gpu?.displayedCount,
757
+ candidateStart: gpu?.candidateStart,
758
+ candidateCount: gpu?.candidateCount,
759
+ pendingReplace: false,
760
+ pendingMove: gpu?.pendingMove,
761
+ unuploadedRanges: gpu?.unuploadedRanges
762
+ })
763
+ }
687
764
  }
688
765
 
689
766
  private finalizePendingMove(): void {
690
767
  const move = this.pendingMove
691
768
  if (!move) return
769
+ const movedKey = move.key
692
770
 
693
771
  const { oldStart, count } = move
694
772
  this.zeroAndFreeSlot(oldStart, count)
@@ -697,6 +775,20 @@ export class GlobalBlockBuffer {
697
775
  this.pendingMove = null
698
776
  this.layoutVersion++
699
777
  this.uploadEpoch++
778
+ if (isClientLightTraceEnabled()) {
779
+ const gpu = this.getGpuSlotTrace(movedKey)
780
+ recordClientLightTrace({
781
+ phase: 'gpuCommitted',
782
+ sectionKey: movedKey,
783
+ displayedStart: gpu?.displayedStart,
784
+ displayedCount: gpu?.displayedCount,
785
+ candidateStart: gpu?.candidateStart,
786
+ candidateCount: gpu?.candidateCount,
787
+ pendingReplace: gpu?.pendingReplace,
788
+ pendingMove: false,
789
+ unuploadedRanges: gpu?.unuploadedRanges
790
+ })
791
+ }
700
792
  }
701
793
 
702
794
  private shrinkHighWatermark(): void {
@@ -10,6 +10,7 @@ import {
10
10
  type LegacyMultiDrawScratch
11
11
  } from './legacyMultiDraw'
12
12
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
13
+ import { isClientLightTraceEnabled, recordClientLightTrace, recordClientLightTraceGpuSample } from '../lighting/debug/clientLightTrace'
13
14
 
14
15
  const VERTS_PER_QUAD = 4
15
16
  const INDICES_PER_QUAD = 6
@@ -337,6 +338,20 @@ export class GlobalLegacyBuffer {
337
338
  this.markDirty(slot.start, slot.start + quadCount - 1)
338
339
  this.syncDefaultDrawGroups()
339
340
  this.layoutVersion++
341
+ if (isClientLightTraceEnabled()) {
342
+ const gpu = this.getGpuSlotTrace(sectionKey)
343
+ recordClientLightTrace({
344
+ phase: 'gpuStaged',
345
+ sectionKey,
346
+ displayedStart: gpu?.displayedStart,
347
+ displayedCount: gpu?.displayedCount,
348
+ candidateStart: gpu?.candidateStart,
349
+ candidateCount: gpu?.candidateCount,
350
+ pendingReplace: gpu?.pendingReplace,
351
+ pendingMove: gpu?.pendingMove,
352
+ unuploadedRanges: gpu?.unuploadedRanges
353
+ })
354
+ }
340
355
  return true
341
356
  }
342
357
 
@@ -367,9 +382,9 @@ export class GlobalLegacyBuffer {
367
382
  getSectionDrawStart(sectionKey: string): number | undefined {
368
383
  const slot = this.sectionSlots.get(sectionKey)
369
384
  if (!slot) return undefined
370
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
371
385
  const replace = this.pendingReplace.get(sectionKey)
372
386
  if (replace) return replace.oldStart
387
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
373
388
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
374
389
  return slot.start
375
390
  }
@@ -377,13 +392,37 @@ export class GlobalLegacyBuffer {
377
392
  getSectionDrawCount(sectionKey: string): number | undefined {
378
393
  const slot = this.sectionSlots.get(sectionKey)
379
394
  if (!slot) return undefined
380
- if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
381
395
  const replace = this.pendingReplace.get(sectionKey)
382
396
  if (replace) return replace.oldCount
397
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.count
383
398
  if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return undefined
384
399
  return slot.count
385
400
  }
386
401
 
402
+ getGpuSlotTrace(sectionKey: string):
403
+ | {
404
+ displayedStart?: number
405
+ displayedCount?: number
406
+ candidateStart: number
407
+ candidateCount: number
408
+ pendingReplace: boolean
409
+ pendingMove: boolean
410
+ unuploadedRanges: number
411
+ }
412
+ | undefined {
413
+ const slot = this.sectionSlots.get(sectionKey)
414
+ if (!slot) return undefined
415
+ return {
416
+ displayedStart: this.getSectionDrawStart(sectionKey),
417
+ displayedCount: this.getSectionDrawCount(sectionKey),
418
+ candidateStart: slot.start,
419
+ candidateCount: slot.count,
420
+ pendingReplace: this.pendingReplace.has(sectionKey),
421
+ pendingMove: this.pendingMove?.key === sectionKey,
422
+ unuploadedRanges: this.pendingRanges.length
423
+ }
424
+ }
425
+
387
426
  getPendingMove(): PendingMove | null {
388
427
  return this.pendingMove
389
428
  }
@@ -425,6 +464,20 @@ export class GlobalLegacyBuffer {
425
464
  this.markDirty(newStart, newStart + section.count - 1)
426
465
  this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
427
466
  this.layoutVersion++
467
+ if (isClientLightTraceEnabled()) {
468
+ const gpu = this.getGpuSlotTrace(section.key)
469
+ recordClientLightTrace({
470
+ phase: 'gpuStaged',
471
+ sectionKey: section.key,
472
+ displayedStart: gpu?.displayedStart,
473
+ displayedCount: gpu?.displayedCount,
474
+ candidateStart: gpu?.candidateStart,
475
+ candidateCount: gpu?.candidateCount,
476
+ pendingReplace: gpu?.pendingReplace,
477
+ pendingMove: true,
478
+ unuploadedRanges: gpu?.unuploadedRanges
479
+ })
480
+ }
428
481
  }
429
482
 
430
483
  updateDrawSpans(visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
@@ -696,6 +749,13 @@ export class GlobalLegacyBuffer {
696
749
  r.start = quadOffset + quadCount
697
750
  }
698
751
  this.uploadEpoch++
752
+ if (isClientLightTraceEnabled()) {
753
+ recordClientLightTraceGpuSample('gpuUploaded', this.highWatermark, () => ({
754
+ phase: 'gpuUploaded',
755
+ unuploadedRanges: this.pendingRanges.length,
756
+ drawableFaces: this.highWatermark
757
+ }))
758
+ }
699
759
  }
700
760
 
701
761
  uploadDirtyIndexRange(): void {
@@ -898,6 +958,9 @@ export class GlobalLegacyBuffer {
898
958
  private findMovableSection(maxCount: number): { key: string; start: number; count: number } | undefined {
899
959
  const sections: Array<{ key: string; start: number; count: number }> = []
900
960
  for (const [key, slot] of this.sectionSlots) {
961
+ if (this.pendingReplace.has(key)) continue
962
+ if (this.pendingMove?.key === key) continue
963
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) continue
901
964
  sections.push({ key, start: slot.start, count: slot.count })
902
965
  }
903
966
  if (sections.length === 0) return undefined
@@ -1006,11 +1069,26 @@ export class GlobalLegacyBuffer {
1006
1069
  this.syncDefaultDrawGroups()
1007
1070
  this.layoutVersion++
1008
1071
  this.uploadEpoch++
1072
+ if (isClientLightTraceEnabled()) {
1073
+ const gpu = this.getGpuSlotTrace(key)
1074
+ recordClientLightTrace({
1075
+ phase: 'gpuCommitted',
1076
+ sectionKey: key,
1077
+ displayedStart: gpu?.displayedStart,
1078
+ displayedCount: gpu?.displayedCount,
1079
+ candidateStart: gpu?.candidateStart,
1080
+ candidateCount: gpu?.candidateCount,
1081
+ pendingReplace: false,
1082
+ pendingMove: gpu?.pendingMove,
1083
+ unuploadedRanges: gpu?.unuploadedRanges
1084
+ })
1085
+ }
1009
1086
  }
1010
1087
 
1011
1088
  private finalizePendingMove(): void {
1012
1089
  const move = this.pendingMove
1013
1090
  if (!move) return
1091
+ const movedKey = move.key
1014
1092
 
1015
1093
  const { oldStart, count } = move
1016
1094
  this.zeroAndFreeSlot(oldStart, count)
@@ -1019,6 +1097,20 @@ export class GlobalLegacyBuffer {
1019
1097
  this.pendingMove = null
1020
1098
  this.layoutVersion++
1021
1099
  this.uploadEpoch++
1100
+ if (isClientLightTraceEnabled()) {
1101
+ const gpu = this.getGpuSlotTrace(movedKey)
1102
+ recordClientLightTrace({
1103
+ phase: 'gpuCommitted',
1104
+ sectionKey: movedKey,
1105
+ displayedStart: gpu?.displayedStart,
1106
+ displayedCount: gpu?.displayedCount,
1107
+ candidateStart: gpu?.candidateStart,
1108
+ candidateCount: gpu?.candidateCount,
1109
+ pendingReplace: gpu?.pendingReplace,
1110
+ pendingMove: false,
1111
+ unuploadedRanges: gpu?.unuploadedRanges
1112
+ })
1113
+ }
1022
1114
  }
1023
1115
 
1024
1116
  private growCapacity(minQuads: number): void {
@@ -87,15 +87,8 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
87
87
  },
88
88
  feedChunkPacket(payload: FeedChunkPacketPayload) {
89
89
  // Forward parsed/raw map_chunk + update_light packets from the
90
- // web-client to all WASM mesher workers. The fan-out below uses
91
- // structured clone (one Uint8Array can only be transferred to a
92
- // single recipient); useWorkerProxy still gives zero-copy transfer
93
- // from main into the off-thread renderer worker for free.
94
- const { kind, ...rest } = payload
95
- const message = { type: kind, ...rest }
96
- for (const worker of worldRenderer.workers) {
97
- worker.postMessage(message)
98
- }
90
+ // web-client to WASM mesher workers (and the light owner when flagged).
91
+ worldRenderer.feedChunkPacket(payload)
99
92
  },
100
93
  getChunksDebugState() {
101
94
  const loadedSectionsChunks: Record<string, true> = {}
@@ -4,30 +4,28 @@ import cloudsPng from 'mc-assets/dist/other-textures/latest/environment/clouds.p
4
4
  import type { WorldRendererThree } from '../worldRendererThree'
5
5
  import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
6
6
  import { vanillaCelestialAngle, vanillaDayBrightness } from '../../lib/skyLight'
7
+ import { loadCloudCover } from '../cloudCover'
8
+ import { buildCloudGeometry, CLOUD_CELL_SIZE, type CloudCover } from '../cloudGeometry'
7
9
 
8
- // Vanilla: clouds.png is 256×256, each texel a 12×12 block cell, slab 4 blocks thick.
9
- const CELL_SIZE = 12
10
- const TEXTURE_CELLS = 256
11
- const PATTERN_PERIOD = CELL_SIZE * TEXTURE_CELLS
12
- const THICKNESS = 4
13
10
  /** Vanilla drifts the pattern 0.03 blocks per tick (20 TPS) toward -X. */
14
11
  const DRIFT_BLOCKS_PER_SECOND = 0.03 * 20
15
12
  /** Vanilla offsets the slab bottom by +0.33 from the dimension's cloud height. */
16
13
  const HEIGHT_BIAS = 0.33
17
14
  const MIN_RADIUS = 256
18
- const MAX_RADIUS = 768
15
+ const MAX_RADIUS = 576
19
16
  const OPACITY = 0.8
20
- /** Vanilla shades the underside darker than the top. */
21
- const BOTTOM_SHADE = 0.7
22
17
 
23
18
  const threeVersion = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10)
24
19
 
25
20
  const vertexShader = /* glsl */ `
26
21
  #include <common>
27
22
  #include <logdepthbuf_pars_vertex>
23
+ attribute float shade;
28
24
  varying vec2 vLocal;
25
+ varying float vShade;
29
26
  void main() {
30
27
  vLocal = (modelMatrix * vec4(position, 1.0)).xz;
28
+ vShade = shade;
31
29
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
32
30
  #include <logdepthbuf_vertex>
33
31
  }
@@ -36,32 +34,39 @@ const vertexShader = /* glsl */ `
36
34
  const fragmentShader = /* glsl */ `
37
35
  #include <common>
38
36
  #include <logdepthbuf_pars_fragment>
39
- uniform sampler2D map;
40
- uniform vec2 patternOffset;
41
37
  uniform vec3 color;
42
38
  uniform float opacity;
43
39
  uniform float fadeStart;
44
40
  uniform float fadeEnd;
45
41
  varying vec2 vLocal;
42
+ varying float vShade;
46
43
  void main() {
47
44
  #include <logdepthbuf_fragment>
48
- float cover = texture2D(map, (vLocal + patternOffset) / ${PATTERN_PERIOD.toFixed(1)}).a;
49
- if (cover < 0.5) discard;
50
45
  float alpha = opacity * (1.0 - smoothstep(fadeStart, fadeEnd, length(vLocal)));
51
46
  if (alpha <= 0.0) discard;
52
- gl_FragColor = vec4(color, alpha);
47
+ gl_FragColor = vec4(color * vShade, alpha);
53
48
  #include <tonemapping_fragment>
54
49
  #include <${threeVersion >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
55
50
  }
56
51
  `
57
52
 
58
53
  /**
59
- * Flat "fast graphics" clouds: one camera-following quad, the cloud mask is sampled from
60
- * vanilla clouds.png in the fragment shader, so there is no per-cell geometry to rebuild
61
- * as the player moves or the pattern drifts.
54
+ * Vanilla "fancy" clouds: a 4-block-thick slab of 12×12 cells taken from clouds.png, with real top,
55
+ * bottom and side faces. The cells around the camera are rebuilt only when the camera or the drifting
56
+ * pattern crosses a cell boundary; everything finer than that is a mesh position offset.
57
+ * A colour-less depth pre-pass over the same geometry keeps overlapping faces from blending twice.
62
58
  */
63
59
  export class CloudsModule implements RendererModuleController {
64
- private mesh?: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>
60
+ private readonly group = new THREE.Group()
61
+ private readonly uniforms = {
62
+ color: { value: new THREE.Color(1, 1, 1) },
63
+ opacity: { value: OPACITY },
64
+ fadeStart: { value: MIN_RADIUS * 0.55 },
65
+ fadeEnd: { value: MIN_RADIUS }
66
+ }
67
+ private meshes?: THREE.Mesh<THREE.BufferGeometry, THREE.ShaderMaterial>[]
68
+ private cover?: CloudCover
69
+ private builtKey = ''
65
70
  private enabled = false
66
71
  private drift = 0
67
72
  private timeOfDay: number
@@ -69,19 +74,20 @@ export class CloudsModule implements RendererModuleController {
69
74
 
70
75
  constructor(private readonly worldRenderer: WorldRendererThree) {
71
76
  this.timeOfDay = worldRenderer.timeOfTheDay
77
+ this.group.name = 'clouds'
72
78
  }
73
79
 
74
80
  enable(): void {
75
81
  if (this.enabled) return
76
82
  this.enabled = true
77
- if (!this.mesh) this.create()
78
- this.mesh!.visible = true
83
+ if (!this.meshes) this.create()
84
+ this.group.visible = true
79
85
  }
80
86
 
81
87
  disable(): void {
82
88
  if (!this.enabled) return
83
89
  this.enabled = false
84
- if (this.mesh) this.mesh.visible = false
90
+ this.group.visible = false
85
91
  }
86
92
 
87
93
  autoEnableCheck(): boolean {
@@ -94,78 +100,96 @@ export class CloudsModule implements RendererModuleController {
94
100
  }
95
101
 
96
102
  render?: (deltaTime: number) => void = deltaTime => {
97
- this.drift = (this.drift + deltaTime * DRIFT_BLOCKS_PER_SECOND) % PATTERN_PERIOD
103
+ // Not wrapped: cell indices wrap in the lookup, and wrapping here would force a rebuild.
104
+ this.drift += deltaTime * DRIFT_BLOCKS_PER_SECOND
98
105
  }
99
106
 
100
107
  dispose(): void {
101
- if (!this.mesh) return
102
- this.worldRenderer.scene.remove(this.mesh)
103
- this.mesh.geometry.dispose()
104
- this.mesh.material.uniforms.map.value?.dispose()
105
- this.mesh.material.dispose()
106
- this.mesh = undefined
108
+ if (!this.meshes) return
109
+ this.worldRenderer.scene.remove(this.group)
110
+ this.meshes[0].geometry.dispose()
111
+ for (const mesh of this.meshes) mesh.material.dispose()
112
+ this.meshes = undefined
113
+ this.cover = undefined
114
+ this.builtKey = ''
107
115
  }
108
116
 
109
117
  private create(): void {
110
- const map = new THREE.TextureLoader().load(cloudsPng)
111
- map.wrapS = THREE.RepeatWrapping
112
- map.wrapT = THREE.RepeatWrapping
113
- map.magFilter = THREE.NearestFilter
114
- map.minFilter = THREE.NearestFilter
115
- map.generateMipmaps = false
116
- // Keep png row 0 at -Z like vanilla's texture-to-world mapping.
117
- map.flipY = false
118
-
119
- const material = new THREE.ShaderMaterial({
120
- uniforms: {
121
- map: { value: map },
122
- patternOffset: { value: new THREE.Vector2() },
123
- color: { value: new THREE.Color(1, 1, 1) },
124
- opacity: { value: OPACITY },
125
- fadeStart: { value: MIN_RADIUS * 0.55 },
126
- fadeEnd: { value: MIN_RADIUS }
127
- },
118
+ const shared = {
119
+ uniforms: this.uniforms,
128
120
  vertexShader,
129
121
  fragmentShader,
130
- transparent: true,
131
- depthWrite: false,
132
- depthTest: true,
133
122
  side: THREE.DoubleSide,
123
+ transparent: true,
134
124
  forceSinglePass: true
135
- })
125
+ }
126
+ const main = new THREE.ShaderMaterial({ ...shared, depthWrite: false })
127
+ // Same program, so its depth matches the main pass exactly.
128
+ const depthOnly = new THREE.ShaderMaterial({ ...shared, depthWrite: true, colorWrite: false })
136
129
 
137
- const geometry = new THREE.PlaneGeometry(2, 2)
138
- geometry.rotateX(-Math.PI / 2)
130
+ const geometry = new THREE.BufferGeometry()
131
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(0), 3))
139
132
 
140
- const mesh = new THREE.Mesh(geometry, material)
141
- mesh.name = 'clouds'
142
- mesh.frustumCulled = false
143
133
  // After sun/moon (-2) and stars (-1), before other transparents so water/glass composite on top.
144
- mesh.renderOrder = -0.5
145
- mesh.onBeforeRender = () => this.syncToCamera(mesh)
146
- this.mesh = mesh
147
- this.worldRenderer.scene.add(mesh)
134
+ const passes: [THREE.ShaderMaterial, number][] = [
135
+ [depthOnly, -0.6],
136
+ [main, -0.5]
137
+ ]
138
+ this.meshes = passes.map(([material, renderOrder]) => {
139
+ const mesh = new THREE.Mesh(geometry, material)
140
+ mesh.frustumCulled = false
141
+ mesh.renderOrder = renderOrder
142
+ this.group.add(mesh)
143
+ return mesh
144
+ })
145
+ // The depth pre-pass is drawn first, so syncing there positions both passes.
146
+ this.meshes[0].onBeforeRender = () => this.syncToCamera()
147
+ this.worldRenderer.scene.add(this.group)
148
+
149
+ loadCloudCover(cloudsPng).then(
150
+ cover => {
151
+ this.cover = cover
152
+ },
153
+ error => console.warn('Failed to load clouds.png, clouds will not render', error)
154
+ )
155
+ }
156
+
157
+ private rebuild(centerX: number, centerZ: number, radiusCells: number): void {
158
+ const data = buildCloudGeometry(this.cover!, centerX, centerZ, radiusCells)
159
+ const geometry = new THREE.BufferGeometry()
160
+ geometry.setAttribute('position', new THREE.BufferAttribute(data.positions, 3))
161
+ geometry.setAttribute('shade', new THREE.BufferAttribute(data.shades, 1))
162
+ geometry.setIndex(new THREE.BufferAttribute(data.indices, 1))
163
+
164
+ const meshes = this.meshes!
165
+ const old = meshes[0].geometry
166
+ for (const mesh of meshes) mesh.geometry = geometry
167
+ old.dispose()
148
168
  }
149
169
 
150
- private syncToCamera(mesh: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>): void {
170
+ private syncToCamera(): void {
151
171
  const { sceneOrigin, viewDistance, worldRendererConfig } = this.worldRenderer
152
- const { uniforms } = mesh.material
153
172
 
154
173
  const radius = THREE.MathUtils.clamp(viewDistance * 16 * 2, MIN_RADIUS, MAX_RADIUS)
155
- uniforms.fadeStart.value = radius * 0.55
156
- uniforms.fadeEnd.value = radius
174
+ this.uniforms.fadeStart.value = radius * 0.55
175
+ this.uniforms.fadeEnd.value = radius
176
+
177
+ // Pattern space is world space shifted by the drift, so world x = pattern x - drift.
178
+ const centerX = Math.floor((sceneOrigin.x + this.drift) / CLOUD_CELL_SIZE)
179
+ const centerZ = Math.floor(sceneOrigin.z / CLOUD_CELL_SIZE)
180
+ if (this.cover) {
181
+ const radiusCells = Math.ceil(radius / CLOUD_CELL_SIZE)
182
+ const key = `${centerX},${centerZ},${radiusCells}`
183
+ if (key !== this.builtKey) {
184
+ this.builtKey = key
185
+ this.rebuild(centerX, centerZ, radiusCells)
186
+ }
187
+ }
157
188
 
158
- // Show the slab bottom from below and its top from above, like a 4-block-thick cloud.
189
+ // Compute in doubles so the GPU only ever sees camera-relative offsets.
159
190
  const bottom = worldRendererConfig.cloudHeight + HEIGHT_BIAS
160
- const fromBelow = sceneOrigin.y < bottom + THICKNESS / 2
161
- const planeY = fromBelow ? bottom : bottom + THICKNESS
162
- mesh.position.set(0, planeY - sceneOrigin.y, 0)
163
- mesh.scale.set(radius, 1, radius)
164
- mesh.updateMatrixWorld()
165
-
166
- // Wrap on the CPU in doubles so the shader never sees large world coordinates.
167
- const wrap = (v: number) => ((v % PATTERN_PERIOD) + PATTERN_PERIOD) % PATTERN_PERIOD
168
- uniforms.patternOffset.value.set(wrap(sceneOrigin.x + this.drift), wrap(sceneOrigin.z))
191
+ this.group.position.set(centerX * CLOUD_CELL_SIZE - this.drift - sceneOrigin.x, bottom - sceneOrigin.y, centerZ * CLOUD_CELL_SIZE - sceneOrigin.z)
192
+ this.group.updateMatrixWorld(true)
169
193
 
170
194
  const day = vanillaDayBrightness(vanillaCelestialAngle(this.timeOfDay))
171
195
  let r = day * 0.9 + 0.1
@@ -178,9 +202,8 @@ export class CloudsModule implements RendererModuleController {
178
202
  g = g * 0.05 + grey * 0.95
179
203
  b = b * 0.05 + grey * 0.95
180
204
  }
181
- const shade = fromBelow ? BOTTOM_SHADE : 1
182
- this.linearColor.setRGB(r * shade, g * shade, b * shade, THREE.SRGBColorSpace)
183
- uniforms.color.value.copy(this.linearColor)
205
+ this.linearColor.setRGB(r, g, b, THREE.SRGBColorSpace)
206
+ this.uniforms.color.value.copy(this.linearColor)
184
207
  }
185
208
  }
186
209