minecraft-renderer 0.1.106 → 0.1.108

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 (98) 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 +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  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 +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. package/src/worldView/worldView.ts +49 -1
@@ -892,3 +892,111 @@ test('GlobalLegacyBuffer: blend reorder metadata survives compaction', () => {
892
892
  buffer.dispose()
893
893
  mat.dispose()
894
894
  })
895
+
896
+ function quadGeometry(quadCount: number): ReturnType<typeof makeQuadGeometry> {
897
+ const indices = new Uint32Array(quadCount * 6)
898
+ for (let i = 0; i < quadCount; i++) {
899
+ const v = i * 4
900
+ indices.set([v, v + 1, v + 2, v, v + 2, v + 3], i * 6)
901
+ }
902
+ return {
903
+ positions: new Float32Array(quadCount * 12),
904
+ colors: new Float32Array(quadCount * 12),
905
+ skyLights: new Float32Array(quadCount * 4),
906
+ blockLights: new Float32Array(quadCount * 4),
907
+ uvs: new Float32Array(quadCount * 8),
908
+ indices
909
+ }
910
+ }
911
+
912
+ function drawableLegacyIndices(buffer: GlobalLegacyBuffer, key: string): number {
913
+ buffer.updateDrawSpans([{ key, distSq: 0 }], 'opaque')
914
+ return buffer.getVisibleIndexSpans().reduce((sum, span) => sum + span.indexCount, 0)
915
+ }
916
+
917
+ test('GlobalLegacyBuffer: compaction must not hide pending-replace displayed indices', () => {
918
+ const scene = new THREE.Scene()
919
+ const mat = createGlobalLegacyBlockMaterial()
920
+ const buffer = new GlobalLegacyBuffer(mat, scene)
921
+
922
+ buffer.addSection('H', quadGeometry(8000), 0, 0, 0)
923
+ buffer.addSection('A', quadGeometry(3000), 0, 0, 0)
924
+ buffer.addSection('B', quadGeometry(500), 0, 0, 0)
925
+ drainUploads(buffer)
926
+ const original = buffer.getSectionDrawStart('A')
927
+
928
+ buffer.addSection('A', quadGeometry(3000), 0, 0, 0)
929
+ buffer.removeSection('H')
930
+ buffer.compactStep()
931
+ buffer.uploadDirtyRange()
932
+ buffer.updateDrawSpans([{ key: 'A', distSq: 0 }], 'opaque')
933
+
934
+ expect(drawableLegacyIndices(buffer, 'A')).toBe(18_000)
935
+ expect(buffer.getSectionDrawStart('A')).toBe(original)
936
+ expect(buffer.getPendingMove()?.key === 'A').toBe(false)
937
+
938
+ buffer.dispose()
939
+ mat.dispose()
940
+ })
941
+
942
+ test('GlobalLegacyBuffer: 27 simultaneous remeshes keep displayed quads on every frame', () => {
943
+ const scene = new THREE.Scene()
944
+ const mat = createGlobalLegacyBlockMaterial()
945
+ const buffer = new GlobalLegacyBuffer(mat, scene)
946
+ const n = 27
947
+ const quads = 1000
948
+ const expectedIndices = quads * 6
949
+
950
+ for (let i = 0; i < n; i++) buffer.addSection(String(i), quadGeometry(quads), 0, 0, 0)
951
+ drainUploads(buffer)
952
+ for (let i = 0; i < n; i++) buffer.addSection(String(i), quadGeometry(quads), 0, 0, 0)
953
+
954
+ for (let frame = 1; frame <= 160; frame++) {
955
+ buffer.compactStep()
956
+ buffer.uploadDirtyRange()
957
+ for (let i = 0; i < n; i++) {
958
+ expect(drawableLegacyIndices(buffer, String(i)), `section ${i} on frame ${frame}`).toBe(expectedIndices)
959
+ }
960
+ if (!buffer.hasPendingUploads() && !buffer.hasPendingReplace() && !buffer.getPendingMove()) break
961
+ }
962
+
963
+ buffer.dispose()
964
+ mat.dispose()
965
+ })
966
+
967
+ test('GlobalLegacyBuffer: remesh during move keeps original displayed slot', () => {
968
+ const scene = new THREE.Scene()
969
+ const mat = createGlobalLegacyBlockMaterial()
970
+ const buffer = new GlobalLegacyBuffer(mat, scene)
971
+
972
+ buffer.addSection('H', quadGeometry(8000), 0, 0, 0)
973
+ buffer.addSection('B', quadGeometry(500), 0, 0, 0)
974
+ buffer.addSection('A', quadGeometry(3000), 0, 0, 0)
975
+ drainUploads(buffer)
976
+ buffer.removeSection('H')
977
+ drainUploads(buffer)
978
+ buffer.compactStep()
979
+ expect(buffer.getPendingMove()?.key).toBe('A')
980
+ const displayed = buffer.getSectionDrawStart('A')
981
+ expect(displayed).toBeDefined()
982
+
983
+ buffer.addSection('A', quadGeometry(3000), 0, 0, 0)
984
+ expect(buffer.getSectionDrawStart('A')).toBe(displayed)
985
+ expect(buffer.getPendingMove()).toBeNull()
986
+ expect(buffer.hasPendingReplace()).toBe(true)
987
+ expect(drawableLegacyIndices(buffer, 'A')).toBe(18_000)
988
+
989
+ buffer.compactStep()
990
+ buffer.uploadDirtyRange()
991
+ expect(buffer.getSectionDrawStart('A')).toBe(displayed)
992
+ expect(buffer.getPendingMove()?.key === 'A').toBe(false)
993
+ expect(drawableLegacyIndices(buffer, 'A')).toBe(18_000)
994
+
995
+ drainUploads(buffer)
996
+ buffer.compactStep()
997
+ finishCurrentMove(buffer)
998
+ expect(drawableLegacyIndices(buffer, 'A')).toBe(18_000)
999
+
1000
+ buffer.dispose()
1001
+ mat.dispose()
1002
+ })
@@ -0,0 +1,25 @@
1
+ //@ts-nocheck
2
+ import { expect, test } from 'vitest'
3
+ import { getWaterFogEnd, getWaterVision, waterFogColorToRgb } from '../waterFog'
4
+
5
+ test('water vision ramps with sqrt over 30 seconds and then stays at 1', () => {
6
+ expect(getWaterVision(0)).toBe(0)
7
+ expect(getWaterVision(7.5)).toBeCloseTo(0.5)
8
+ expect(getWaterVision(30)).toBe(1)
9
+ expect(getWaterVision(300)).toBe(1)
10
+ expect(getWaterVision(-5)).toBe(0)
11
+ })
12
+
13
+ test('fog distance starts at 24 blocks, reaches 96 and breathing skips the ramp', () => {
14
+ expect(getWaterFogEnd(0, false)).toBe(24)
15
+ expect(getWaterFogEnd(7.5, false)).toBeCloseTo(48)
16
+ expect(getWaterFogEnd(30, false)).toBe(96)
17
+ expect(getWaterFogEnd(0, true)).toBe(96)
18
+ })
19
+
20
+ test('default water fog colour is the vanilla dark navy', () => {
21
+ const [r, g, b] = waterFogColorToRgb(0x05_05_33)
22
+ expect(r).toBeCloseTo(5 / 255)
23
+ expect(g).toBeCloseTo(5 / 255)
24
+ expect(b).toBeCloseTo(51 / 255)
25
+ })
@@ -0,0 +1,28 @@
1
+ //@ts-nocheck
2
+ /** Vanilla `Biome.waterFogColor` for most biomes (0x050533). */
3
+ export const DEFAULT_WATER_FOG_COLOR = 0x05_05_33
4
+
5
+ /** Vanilla `FogRenderer`: water fog ends 96 blocks away at full vision. */
6
+ export const WATER_FOG_FULL_DISTANCE = 96
7
+ /** Vision never drops below a quarter of the full distance, so 24 blocks right after diving in. */
8
+ export const WATER_FOG_MIN_VISION = 0.25
9
+ /** `LocalPlayer.waterVisionTime` caps at 600 ticks (30s at 20 TPS). */
10
+ export const WATER_VISION_TICKS = 600
11
+ export const TICKS_PER_SECOND = 20
12
+
13
+ /** Vanilla `LocalPlayer.getWaterVision`: sqrt ramp from 0 to 1 over 30s spent underwater. */
14
+ export function getWaterVision(secondsUnderwater: number): number {
15
+ const ticks = Math.min(Math.max(secondsUnderwater, 0) * TICKS_PER_SECOND, WATER_VISION_TICKS)
16
+ return Math.sqrt(ticks / WATER_VISION_TICKS)
17
+ }
18
+
19
+ /** Distance at which water fog is fully opaque. Conduit-like water breathing gives full vision at once. */
20
+ export function getWaterFogEnd(secondsUnderwater: number, waterBreathing: boolean): number {
21
+ const vision = waterBreathing ? 1 : getWaterVision(secondsUnderwater)
22
+ return WATER_FOG_FULL_DISTANCE * Math.max(WATER_FOG_MIN_VISION, vision)
23
+ }
24
+
25
+ /** 0xRRGGBB to 0..1 components (same raw-component convention the land fog already uses). */
26
+ export function waterFogColorToRgb(color: number): [number, number, number] {
27
+ return [((color >> 16) & 0xff) / 255, ((color >> 8) & 0xff) / 255, (color & 0xff) / 255]
28
+ }
@@ -40,6 +40,9 @@ import { FireworksManager } from './fireworks'
40
40
  import { SceneOrigin } from './sceneOrigin'
41
41
  import { downloadWorldGeometry } from './worldGeometryExport'
42
42
  import { ChunkMeshManager } from './chunkMeshManager'
43
+ import { selectReadySectionFlushes } from '../lighting/versioning/pendingSectionFlush'
44
+ import { isClientLightTraceEnabled, recordClientLightTrace } from '../lighting/debug/clientLightTrace'
45
+ import { EntityLightController } from '../lighting/entity/entityLightController'
43
46
  import { raycastVoxelSolid } from './thirdPersonVoxelRaycast'
44
47
  import type { RendererModuleManifest, RegisteredModule, RendererModuleController } from './rendererModuleSystem'
45
48
  import { BUILTIN_MODULES } from './modules/index'
@@ -89,6 +92,7 @@ export class WorldRendererThree extends WorldRendererCommon {
89
92
  ambientLight = new THREE.AmbientLight(0xcc_cc_cc, WorldRendererThree.LEGACY_TO_PHYSICAL_LIGHT)
90
93
  directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5 * WorldRendererThree.LEGACY_TO_PHYSICAL_LIGHT)
91
94
  entities = new Entities(this, (globalThis as any).mcData)
95
+ entityLightController?: EntityLightController
92
96
  performanceMonitor!: PerformanceMonitor
93
97
  cameraGroupVr?: THREE.Object3D
94
98
  material = new THREE.MeshBasicMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
@@ -204,6 +208,14 @@ export class WorldRendererThree extends WorldRendererCommon {
204
208
 
205
209
  // Initialize chunk mesh manager
206
210
  this.chunkMeshManager = new ChunkMeshManager(this, this.scene, this.material, this.worldSizeParams.worldHeight, this.viewDistance)
211
+ this.entityLightController = new EntityLightController({
212
+ getLight: (x, y, z) => this.rendererLightCache.getLight(x, y, z),
213
+ getSkyLevel: () => this.chunkMeshManager.getSkyLevel(),
214
+ getLightmapParams: () => this.chunkMeshManager.getLightmapParams(),
215
+ getColumnRevision: (cx, cz) => this.rendererLightCache.getColumnRevision(cx, cz),
216
+ getGlobalRevision: () => this.rendererLightCache.getGlobalRevision(),
217
+ lightingEnabled: () => this.worldRendererConfig.enableLighting !== false
218
+ })
207
219
  this.onRenderDistanceChanged = viewDistance => {
208
220
  this.chunkMeshManager.updateViewDistance(viewDistance)
209
221
  }
@@ -563,6 +575,9 @@ export class WorldRendererThree extends WorldRendererCommon {
563
575
  this.cursorBlock.setHighlightCursorBlock(value ? new Vec3(value.x, value.y, value.z) : null, value?.shapes)
564
576
  })
565
577
  this.onReactivePlayerStateUpdated('diggingBlock', value => {
578
+ if (value && (value.stage == null || value.stage === 0) && isClientLightTraceEnabled()) {
579
+ recordClientLightTrace({ phase: 'inputClick', sectionKey: `${value.x},${value.y},${value.z}` })
580
+ }
566
581
  this.cursorBlock.updateBreakAnimation(value ? { x: value.x, y: value.y, z: value.z } : undefined, value?.stage ?? null, value?.mergedShape)
567
582
  })
568
583
  this.onReactivePlayerStateUpdated('perspective', value => {
@@ -594,6 +609,13 @@ export class WorldRendererThree extends WorldRendererCommon {
594
609
  this.onReactiveConfigUpdated('futuristicReveal', () => {
595
610
  this.updateModulesFromConfig()
596
611
  })
612
+ this.onReactiveConfigUpdated(
613
+ 'enableLighting',
614
+ () => {
615
+ this.rerenderAllChunks()
616
+ },
617
+ false
618
+ )
597
619
  this.onReactiveDebugUpdated('disableEntities', () => {
598
620
  this.entities.syncSceneAttachment()
599
621
  })
@@ -732,16 +754,20 @@ export class WorldRendererThree extends WorldRendererCommon {
732
754
  }
733
755
 
734
756
  timeUpdated(newTime: number): void {
735
- // Update starfield module with time
736
- const starfieldModule = this.getModule<any>('starfield')
737
- if (starfieldModule?.updateTimeOfDay) {
738
- starfieldModule.updateTimeOfDay(newTime)
757
+ for (const module of Object.values(this.modules)) {
758
+ ;(module.controller as { updateTimeOfDay?: (time: number) => void }).updateTimeOfDay?.(newTime)
739
759
  }
740
760
 
741
761
  this.skyboxRenderer.updateTime(newTime)
742
762
  this.syncSkyLevelFromTime(newTime)
743
763
  }
744
764
 
765
+ moonPhaseUpdated(phase: number): void {
766
+ for (const module of Object.values(this.modules)) {
767
+ ;(module.controller as { updateMoonPhase?: (phase: number) => void }).updateMoonPhase?.(phase)
768
+ }
769
+ }
770
+
745
771
  private syncSkyLevelFromTime(timeOfDay: number): void {
746
772
  const skyLevel = calculateSkyLightSimple(timeOfDay) / 15
747
773
  this.chunkMeshManager.setSkyLevel(skyLevel)
@@ -826,6 +852,10 @@ export class WorldRendererThree extends WorldRendererCommon {
826
852
  if (camCollisionBytes > 0) {
827
853
  text += `CAM: ${formatCompact(camCollisionBytes)}b `
828
854
  }
855
+ const entityLightBytes = this.rendererLightCache.getAllocatedBytes()
856
+ if (entityLightBytes > 0) {
857
+ text += `ELIGHT: ${formatCompact(entityLightBytes)}b `
858
+ }
829
859
  const pf = formatPerformanceFactorsDebug(this.reactiveState.world.instabilityFactors)
830
860
  if (pf) text += `PF: ${pf} `
831
861
  // entities can be seen in F3
@@ -880,45 +910,37 @@ export class WorldRendererThree extends WorldRendererCommon {
880
910
  if (this.pendingSectionUpdates.size === 0) return
881
911
 
882
912
  const now = performance.now()
883
- const sectionHeight = this.getSectionHeight()
884
- const ready: string[] = []
885
-
886
- for (const key of this.pendingSectionUpdates.keys()) {
887
- const startedAt = this.pendingSectionBufferStartTimes.get(key) ?? now
888
- const sinceFirst = now - startedAt
889
-
890
- if (sinceFirst < WorldRendererThree.MAX_SECTION_UPDATE_BUFFER_MS) {
891
- // Still within this section's grace window — wait if any neighbor is
892
- // currently being re-meshed so we don't briefly expose a hole between
893
- // the just-updated section and a stale neighbor (sky-flicker bug).
894
- const [sx, sy, sz] = key.split(',').map(Number)
895
- const neighborKeys = [
896
- `${sx - 16},${sy},${sz}`,
897
- `${sx + 16},${sy},${sz}`,
898
- `${sx},${sy - sectionHeight},${sz}`,
899
- `${sx},${sy + sectionHeight},${sz}`,
900
- `${sx},${sy},${sz - 16}`,
901
- `${sx},${sy},${sz + 16}`
902
- ]
903
- let neighborBusy = false
904
- for (const neighborKey of neighborKeys) {
905
- if (this.sectionsWaiting.has(neighborKey) && !this.pendingSectionUpdates.has(neighborKey) && this.sectionObjects[neighborKey]) {
906
- neighborBusy = true
907
- break
913
+ // Face-adjacent buffered sections are installed as one group: a lone
914
+ // install would show the neighbour's faces still culled against the old
915
+ // block state, i.e. a see-through hole (sky flash) on dig/place.
916
+ const ready = selectReadySectionFlushes({
917
+ pendingKeys: this.pendingSectionUpdates.keys(),
918
+ startedAt: key => this.pendingSectionBufferStartTimes.get(key),
919
+ now,
920
+ maxBufferMs: WorldRendererThree.MAX_SECTION_UPDATE_BUFFER_MS,
921
+ sectionHeight: this.getSectionHeight(),
922
+ isOutstanding: key => this.sectionsWaiting.has(key) && !this.pendingSectionUpdates.has(key),
923
+ ...(this.lightCoordinator.isSessionLive()
924
+ ? {
925
+ isTopologyOutstanding: (key: string) => this.lightCoordinator.pendingTopologyBySection.has(key) && !this.pendingSectionUpdates.has(key)
908
926
  }
909
- }
910
- if (neighborBusy) continue
911
- }
912
-
913
- ready.push(key)
914
- }
927
+ : {}),
928
+ hasSectionObject: key => !!this.sectionObjects[key]
929
+ })
915
930
 
916
931
  if (ready.length === 0) return
917
932
 
918
- for (const key of ready) {
933
+ for (const { key, reason } of ready) {
919
934
  const update = this.pendingSectionUpdates.get(key)!
920
935
  this.pendingSectionUpdates.delete(key)
921
936
  this.pendingSectionBufferStartTimes.delete(key)
937
+ if (isClientLightTraceEnabled()) {
938
+ recordClientLightTrace({
939
+ phase: 'flush',
940
+ sectionKey: key,
941
+ flushReason: reason
942
+ })
943
+ }
922
944
 
923
945
  const chunkCoords = update.key.split(',')
924
946
  const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
@@ -928,6 +950,14 @@ export class WorldRendererThree extends WorldRendererCommon {
928
950
  continue
929
951
  }
930
952
 
953
+ const decision = this.lightCoordinator.evaluateOwnerGeometry(update)
954
+ if (!decision.accepted || update.geometry?.hadErrors) {
955
+ this.lightCoordinator.noteRejectedOwnerGeometry(update.key, undefined, { expectLaterFinished: false })
956
+ continue
957
+ }
958
+ this.lightCoordinator.acknowledgeAcceptedTopology(update)
959
+ if (decision.keepCovering) this.lightCoordinator.maybeDispatchCoveringRemesh(update.key)
960
+
931
961
  if (!this.chunkMeshManager.sectionHasRenderableContent(update.geometry)) {
932
962
  this.chunkMeshManager.releaseSection(update.key)
933
963
  continue
@@ -962,6 +992,14 @@ export class WorldRendererThree extends WorldRendererCommon {
962
992
  return
963
993
  }
964
994
 
995
+ const decision = this.lightCoordinator.evaluateOwnerGeometry(data)
996
+ if (!decision.accepted || data.geometry?.hadErrors) {
997
+ this.lightCoordinator.noteRejectedOwnerGeometry(data.key, undefined, { expectLaterFinished: true })
998
+ return
999
+ }
1000
+ this.lightCoordinator.acknowledgeAcceptedTopology(data)
1001
+ if (decision.keepCovering) this.lightCoordinator.maybeDispatchCoveringRemesh(data.key)
1002
+
965
1003
  if (this.sectionObjects[data.key]) {
966
1004
  this.pendingSectionUpdates.set(data.key, data)
967
1005
  // Per-section deadline: only set if we don't already have one, so
@@ -969,6 +1007,13 @@ export class WorldRendererThree extends WorldRendererCommon {
969
1007
  if (!this.pendingSectionBufferStartTimes.has(data.key)) {
970
1008
  this.pendingSectionBufferStartTimes.set(data.key, performance.now())
971
1009
  }
1010
+ if (isClientLightTraceEnabled()) {
1011
+ recordClientLightTrace({
1012
+ phase: 'pending',
1013
+ sectionKey: data.key,
1014
+ queueDepth: this.pendingSectionUpdates.size
1015
+ })
1016
+ }
972
1017
  return
973
1018
  }
974
1019
 
@@ -1325,7 +1370,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1325
1370
 
1326
1371
  // Update skybox position to follow camera
1327
1372
  const cameraPos = this.getCameraPosition()
1328
- this.skyboxRenderer.update(cameraPos, this.viewDistance)
1373
+ this.skyboxRenderer.update(cameraPos, this.viewDistance, deltaTime)
1329
1374
 
1330
1375
  this.updateSmoothFov()
1331
1376
  if (sizeChanged) {
@@ -1548,8 +1593,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1548
1593
  }
1549
1594
  }
1550
1595
 
1551
- removeColumn(x, z) {
1552
- super.removeColumn(x, z)
1596
+ removeColumn(x, z, opts?: { preserveClientLight?: boolean }) {
1597
+ super.removeColumn(x, z, opts)
1553
1598
 
1554
1599
  this.clearPendingSectionUpdatesForChunk(x, z)
1555
1600
  const sectionHeight = this.getSectionHeight()
@@ -3,6 +3,7 @@
3
3
  // This file takes WASM output and generates full Three.js buffer geometry
4
4
 
5
5
  import * as THREE from 'three'
6
+ import { isClientLightProbeBlock } from '../../lighting/debug/clientLightProbe'
6
7
  import worldBlockProviderModule, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
7
8
  import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
8
9
  import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
@@ -692,7 +693,26 @@ export function renderWasmOutputToGeometry(
692
693
  },
693
694
  shaderWordBuffer
694
695
  )
695
- if (emitted) continue
696
+ if (emitted) {
697
+ if (isClientLightProbeBlock(bx, by, bz)) {
698
+ console.log('[lightProbe:shaderCube]', {
699
+ time: Date.now(),
700
+ position: [bx, by, bz],
701
+ blockStateId,
702
+ name: prismBlock.name,
703
+ useModelLighting: false,
704
+ wasmSky: block.sky_light_data?.map(values => [...values]),
705
+ visibleFaces: block.visible_faces,
706
+ jsNeighborSky: Object.fromEntries(
707
+ Object.entries(elemFaces).map(([face, info]) => [
708
+ face,
709
+ world?.getChannelLightNorm(new Vec3(bx, by, bz).offset(info.dir[0], info.dir[1], info.dir[2])).sky
710
+ ])
711
+ )
712
+ })
713
+ }
714
+ continue
715
+ }
696
716
  }
697
717
  }
698
718
 
@@ -842,6 +862,20 @@ export function renderWasmOutputToGeometry(
842
862
  const tint = getTint(matchingEFace, cachedModel.blockName, cachedModel.blockProps, biome, world)
843
863
 
844
864
  const useModelLighting = (!cachedModel.isCube || globalMatrix != null) && world
865
+ if (isClientLightProbeBlock(bx, by, bz)) {
866
+ console.log('[lightProbe:face]', {
867
+ time: Date.now(),
868
+ position: [bx, by, bz],
869
+ faceName,
870
+ blockStateId,
871
+ name: prismBlock.name,
872
+ isCube: cachedModel.isCube,
873
+ rotated: globalMatrix != null,
874
+ useModelLighting: Boolean(useModelLighting),
875
+ wasmSky: skyValuesRaw ? [...skyValuesRaw] : null,
876
+ jsNeighborSky: world?.getChannelLightNorm(new Vec3(bx, by, bz).offset(...transformedDirI)).sky
877
+ })
878
+ }
845
879
  const baseChannels = useModelLighting
846
880
  ? world.getChannelLightNorm(new Vec3(bx, by, bz).offset(transformedDirI[0], transformedDirI[1], transformedDirI[2]))
847
881
  : null
@@ -921,6 +955,17 @@ export function renderWasmOutputToGeometry(
921
955
  tgtCol.push(tintAo[0], tintAo[1], tintAo[2])
922
956
  tgtSky.push(skyLightNorm)
923
957
  tgtBlock.push(blockLightNorm)
958
+ if (isClientLightProbeBlock(bx, by, bz)) {
959
+ console.log('[lightProbe:vertex]', {
960
+ time: Date.now(),
961
+ position: [bx, by, bz],
962
+ faceName,
963
+ cornerIdx,
964
+ useModelLighting: Boolean(useModelLighting),
965
+ skyLightNorm,
966
+ blockLightNorm
967
+ })
968
+ }
924
969
 
925
970
  const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
926
971
  const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
@@ -2,6 +2,21 @@
2
2
  /* tslint:disable */
3
3
  /* eslint-disable */
4
4
 
5
+ export class JsLightEngine {
6
+ free(): void;
7
+ [Symbol.dispose](): void;
8
+ pushEvent(event: any): void;
9
+ getSkyLight(x: number, y: number, z: number): number;
10
+ getBlockLight(x: number, y: number, z: number): number;
11
+ setLightTables(emission: Uint8Array, opacity: Uint8Array): void;
12
+ setOcclusionTable(occupancy: Uint8Array): void;
13
+ setSkyLightEnabled(enabled: boolean): void;
14
+ pollCompletedPublication(): any;
15
+ step(budget_ms: number): boolean;
16
+ constructor(world_min_y: number, world_height: number);
17
+ readonly fallbackRelights: number;
18
+ }
19
+
5
20
  /**
6
21
  * Compute wireframe edge positions from a triangle mesh.
7
22
  *
@@ -210,11 +225,11 @@ export function parseMapChunkV18Plus(raw_packet: Uint8Array, num_sections: numbe
210
225
  * `raw_packet` includes the leading packet-id varint (we skip it).
211
226
  * `num_sections` should match the column the light is for (16 in 1.17).
212
227
  *
213
- * Returns `{ x, z, skyLight: Uint8Array(num_sections * 4096),
214
- * blockLight: Uint8Array(num_sections * 4096), bytesRead }`.
215
- * Layout matches the existing 1.18+ light arrays
216
- * (`x + z*16 + y_abs*256`); the JS-side worker reorders into per-section
217
- * stack via the same path used for 1.18+ raw map_chunk parsing.
228
+ * Returns `{ x, z, trustEdges, skyLight, blockLight, skyLightMask,
229
+ * emptySkyLightMask, blockLightMask, emptyBlockLightMask,
230
+ * skyBelow?, skyAbove?, blockBelow?, blockAbove?, bytesRead }`.
231
+ * World arrays are `num_sections * 4096` (`x + z*16 + y_abs*256`).
232
+ * Omitted sections are left 0 and are not authoritative — use the masks.
218
233
  */
219
234
  export function parseUpdateLightV17(raw_packet: Uint8Array, num_sections: number): any;
220
235
 
@@ -227,6 +242,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
227
242
 
228
243
  export interface InitOutput {
229
244
  readonly memory: WebAssembly.Memory;
245
+ readonly __wbg_jslightengine_free: (a: number, b: number) => void;
230
246
  readonly computeWireframeEdges: (a: number, b: number, c: number, d: number) => [number, number];
231
247
  readonly computeWireframeEdgesU16: (a: number, b: number, c: number, d: number) => [number, number];
232
248
  readonly generateGeometryFromDump118: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number, v: number, w: number, x: number, y: number, z: number, a1: number, b1: number, c1: number, d1: number, e1: number, f1: number, g1: number, h1: number, i1: number, j1: number, k1: number) => any;
@@ -236,6 +252,16 @@ export interface InitOutput {
236
252
  readonly generateGeometryFromParsedV16V17Multi: (a: any, b: number, c: number, d: number, e: number, f: number, g: any, h: number, i: any, j: any, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number, v: number, w: number, x: number, y: number, z: number, a1: number, b1: number, c1: number, d1: number, e1: number, f1: number, g1: number, h1: number) => any;
237
253
  readonly generate_geometry: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number, v: number, w: number, x: number, y: number, z: number, a1: number, b1: number) => any;
238
254
  readonly generate_geometry_multi: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number, r: number, s: number, t: number, u: number, v: number, w: number, x: number, y: number, z: number, a1: number, b1: number, c1: number, d1: number, e1: number, f1: number) => any;
255
+ readonly jslightengine_fallbackRelights: (a: number) => number;
256
+ readonly jslightengine_getBlockLight: (a: number, b: number, c: number, d: number) => number;
257
+ readonly jslightengine_getSkyLight: (a: number, b: number, c: number, d: number) => number;
258
+ readonly jslightengine_js_new: (a: number, b: number) => number;
259
+ readonly jslightengine_pollCompletedPublication: (a: number) => any;
260
+ readonly jslightengine_pushEvent: (a: number, b: any) => void;
261
+ readonly jslightengine_setLightTables: (a: number, b: number, c: number, d: number, e: number) => void;
262
+ readonly jslightengine_setOcclusionTable: (a: number, b: number, c: number) => void;
263
+ readonly jslightengine_setSkyLightEnabled: (a: number, b: number) => void;
264
+ readonly jslightengine_step: (a: number, b: number) => number;
239
265
  readonly parseChunkDump118: (a: number, b: number, c: number, d: number, e: number) => any;
240
266
  readonly parseChunkDump118FullColumn: (a: number, b: number, c: number, d: number, e: number) => any;
241
267
  readonly parseChunkDump118FullColumnAll: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number, n: number, o: number, p: number, q: number) => any;