minecraft-renderer 0.1.71 → 0.1.73

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 (65) hide show
  1. package/README.md +3 -3
  2. package/dist/mesher.js +81 -81
  3. package/dist/mesher.js.map +3 -3
  4. package/dist/mesherWasm.js +1183 -943
  5. package/dist/minecraft-renderer.js +250 -79
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +1732 -1001
  8. package/package.json +3 -3
  9. package/src/graphicsBackend/rendererDefaultOptions.ts +5 -10
  10. package/src/graphicsBackend/rendererOptionsSync.ts +1 -1
  11. package/src/lib/bakeLegacyLight.ts +17 -0
  12. package/src/lib/blockEntityLightRegistry.test.ts +18 -0
  13. package/src/lib/blockEntityLightRegistry.ts +75 -0
  14. package/src/lib/blockEntityLighting.test.ts +30 -0
  15. package/src/lib/blockEntityLighting.ts +53 -0
  16. package/src/lib/worldrendererCommon.reconfigure.test.ts +202 -0
  17. package/src/lib/worldrendererCommon.ts +152 -22
  18. package/src/mesher-shared/blockEntityMetadata.test.ts +33 -0
  19. package/src/mesher-shared/blockEntityMetadata.ts +19 -3
  20. package/src/mesher-shared/exportedGeometryTypes.ts +11 -0
  21. package/src/mesher-shared/models.ts +161 -92
  22. package/src/mesher-shared/shared.ts +15 -4
  23. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +40 -0
  24. package/src/mesher-shared/world.ts +12 -0
  25. package/src/mesher-shared/worldLighting.test.ts +54 -0
  26. package/src/playground/baseScene.ts +1 -1
  27. package/src/three/bannerRenderer.ts +10 -3
  28. package/src/three/chunkMeshManager.ts +663 -69
  29. package/src/three/cubeDrawSpans.ts +74 -0
  30. package/src/three/cubeMultiDraw.ts +119 -0
  31. package/src/three/documentRenderer.ts +0 -2
  32. package/src/three/entities.ts +5 -6
  33. package/src/three/entity/EntityMesh.ts +7 -5
  34. package/src/three/entity/gltfAnimationUtils.ts +5 -3
  35. package/src/three/globalBlockBuffer.ts +208 -12
  36. package/src/three/globalLegacyBuffer.ts +701 -0
  37. package/src/three/graphicsBackendOffThread.ts +16 -1
  38. package/src/three/itemMesh.ts +5 -2
  39. package/src/three/legacySectionCull.ts +85 -0
  40. package/src/three/modules/sciFiWorldReveal.ts +347 -703
  41. package/src/three/modules/starfield.ts +3 -2
  42. package/src/three/sectionRaycastAabb.ts +25 -0
  43. package/src/three/shaders/cubeBlockShader.ts +80 -17
  44. package/src/three/shaders/legacyBlockShader.ts +292 -0
  45. package/src/three/skyboxRenderer.ts +1 -1
  46. package/src/three/tests/chunkMeshManagerLegacy.test.ts +286 -0
  47. package/src/three/tests/cubeDrawSpans.test.ts +73 -0
  48. package/src/three/tests/globalLegacyBuffer.test.ts +360 -0
  49. package/src/three/tests/legacySectionCull.test.ts +80 -0
  50. package/src/three/tests/signTextureCache.test.ts +83 -0
  51. package/src/three/threeJsMedia.ts +2 -2
  52. package/src/three/waypointSprite.ts +2 -2
  53. package/src/three/world/cursorBlock.ts +1 -0
  54. package/src/three/world/vr.ts +2 -2
  55. package/src/three/worldGeometryExport.ts +83 -26
  56. package/src/three/worldRendererThree.ts +94 -25
  57. package/src/wasm-mesher/bridge/render-from-wasm.ts +214 -72
  58. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +18 -6
  59. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  60. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +20 -0
  61. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +67 -5
  62. package/src/wasm-mesher/worker/mesherWasm.ts +70 -14
  63. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +11 -0
  64. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +15 -0
  65. package/src/worldView/worldView.ts +11 -0
@@ -8,6 +8,7 @@ import { worldColumnKey, World } from '../../mesher-shared/world'
8
8
  import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from '../../mesher-shared/computeHeightmap'
9
9
  import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMeta } from '../../mesher-shared/blockEntityMetadata'
10
10
  import { SectionRequestTracker } from './mesherWasmRequestTracker'
11
+ import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
11
12
  import {
12
13
  CONVERSION_CACHE_LIMIT,
13
14
  clearConversionCache,
@@ -18,6 +19,7 @@ import {
18
19
 
19
20
  let wasm: typeof import('../runtime-build/wasm_mesher.js') | null = null
20
21
  let wasmInitialized = false
22
+ let wasmReady = false // true ONLY after wasm.default() instantiates the module; gates light-packet parsing
21
23
 
22
24
  // Pending raw `update_light` packets that arrived before WASM finished
23
25
  // loading. Parsed and drained once `initWasm` resolves. Without this queue
@@ -33,7 +35,7 @@ const pendingUpdateLightV17: Array<{ rawPacket: Uint8Array, numSections: number
33
35
  const pendingUpdateLightV16: Array<{ rawPacket: Uint8Array }> = []
34
36
 
35
37
  function processUpdateLightV17 (rawPacket: Uint8Array, numSections: number): void {
36
- if (!wasm || !(wasm as any).parseUpdateLightV17) {
38
+ if (!wasmReady) {
37
39
  pendingUpdateLightV17.push({ rawPacket, numSections })
38
40
  return
39
41
  }
@@ -41,11 +43,13 @@ function processUpdateLightV17 (rawPacket: Uint8Array, numSections: number): voi
41
43
  const parsed: any = (wasm as any).parseUpdateLightV17(rawPacket, numSections)
42
44
  const x = (parsed.x as number) * 16
43
45
  const z = (parsed.z as number) * 16
46
+ const skyLight = parsed.skyLight as Uint8Array
44
47
  updateLightV17Cache.set(rawCacheKey(x, z), {
45
- skyLight: parsed.skyLight as Uint8Array,
48
+ skyLight,
46
49
  blockLight: parsed.blockLight as Uint8Array,
47
50
  })
48
51
  invalidateConversion(x, z)
52
+ dirtyColumnSectionsForLightUpdate(x, z)
49
53
  } catch (err) {
50
54
  console.warn('[WASM Mesher] parseUpdateLightV17 failed:', err)
51
55
  }
@@ -57,7 +61,7 @@ function processUpdateLightV17 (rawPacket: Uint8Array, numSections: number): voi
57
61
  // chunk cache has the entry, and crossing the streams could mismatch a
58
62
  // stale 1.17 column with 1.16 light or vice versa during version switches.
59
63
  function processUpdateLightV16 (rawPacket: Uint8Array): void {
60
- if (!wasm || !(wasm as any).parseUpdateLightV17) {
64
+ if (!wasmReady) {
61
65
  pendingUpdateLightV16.push({ rawPacket })
62
66
  return
63
67
  }
@@ -70,6 +74,7 @@ function processUpdateLightV16 (rawPacket: Uint8Array): void {
70
74
  blockLight: parsed.blockLight as Uint8Array,
71
75
  })
72
76
  invalidateConversion(x, z)
77
+ dirtyColumnSectionsForLightUpdate(x, z)
73
78
  } catch (err) {
74
79
  console.warn('[WASM Mesher] parseUpdateLightV17 (v16) failed:', err)
75
80
  }
@@ -81,6 +86,7 @@ async function initWasm() {
81
86
  wasmInitialized = true
82
87
  wasm = await import('../runtime-build/wasm_mesher.js')
83
88
  await wasm.default('/wasm_mesher_bg.wasm') as any
89
+ wasmReady = true // instance is now usable; drained packets below will pass the guard
84
90
 
85
91
  if (pendingUpdateLightV17.length > 0) {
86
92
  console.log('[WASM Mesher] draining', pendingUpdateLightV17.length, 'pending update_light v17 packets')
@@ -94,7 +100,10 @@ async function initWasm() {
94
100
  for (const item of queue) processUpdateLightV16(item.rawPacket)
95
101
  }
96
102
  } catch (err) {
97
- console.error('Failed to initialize WASM mesher:', err)
103
+ console.error(
104
+ '[WASM Mesher] Failed to initialize WASM mesher — block lighting may stay at full brightness:',
105
+ err,
106
+ )
98
107
  wasmInitialized = true // Don't try to initialize again
99
108
  // Don't throw - allow worker to continue without WASM (will fail on first use)
100
109
  }
@@ -195,6 +204,15 @@ function setSectionDirty(pos: Vec3, value = true) {
195
204
  }
196
205
  }
197
206
 
207
+ /** Re-mesh every section in a column after `update_light` updates the light cache. */
208
+ function dirtyColumnSectionsForLightUpdate (x: number, z: number) {
209
+ const worldMinY = config?.worldMinY ?? 0
210
+ const worldMaxY = config?.worldMaxY ?? 256
211
+ for (const y of sectionYsForLightColumnDirty(worldMinY, worldMaxY, SECTION_HEIGHT)) {
212
+ setSectionDirty(new Vec3(x, y, z))
213
+ }
214
+ }
215
+
198
216
  const softCleanup = () => {
199
217
  world = new World(world.config.version)
200
218
  globalThis.world = world
@@ -340,7 +358,7 @@ const convertParsedV17ToWasm = (
340
358
  } else {
341
359
  blockLight = new Uint8Array(totalBlocks)
342
360
  skyLight = new Uint8Array(totalBlocks)
343
- skyLight.fill(15)
361
+ skyLight.fill(config?.skyLight ?? 15)
344
362
  }
345
363
  const biomesArray: Uint8Array = parsed.biomes
346
364
  let blockCount = 0
@@ -406,7 +424,7 @@ const convertParsedV16ToWasm = (
406
424
  } else {
407
425
  blockLight = new Uint8Array(totalBlocks)
408
426
  skyLight = new Uint8Array(totalBlocks)
409
- skyLight.fill(15)
427
+ skyLight.fill(config?.skyLight ?? 15)
410
428
  }
411
429
  const biomesArray: Uint8Array = parsed.biomes
412
430
  let blockCount = 0
@@ -465,7 +483,7 @@ const meshColumnFromRawV18Plus = (
465
483
  meta.occludingBlocks,
466
484
  config?.enableLighting !== false,
467
485
  config?.smoothLighting !== false,
468
- config?.skyLight || 15
486
+ config?.skyLight ?? 15
469
487
  )
470
488
  } catch (err) {
471
489
  console.warn('[WASM Mesher] generateGeometryFromMapChunkV18Plus failed, falling back:', err)
@@ -511,7 +529,7 @@ const meshColumnFromParsedV16V17 = (
511
529
  meta.occludingBlocks,
512
530
  config?.enableLighting !== false,
513
531
  config?.smoothLighting !== false,
514
- config?.skyLight || 15
532
+ config?.skyLight ?? 15
515
533
  )
516
534
  } catch (err) {
517
535
  console.warn('[WASM Mesher] generateGeometryFromParsedV16V17 failed, falling back:', err)
@@ -572,7 +590,7 @@ const meshMultiColumnsFromRawV18Plus = (
572
590
  meta.occludingBlocks,
573
591
  config?.enableLighting !== false,
574
592
  config?.smoothLighting !== false,
575
- config?.skyLight || 15
593
+ config?.skyLight ?? 15
576
594
  )
577
595
  } catch (err) {
578
596
  console.warn('[WASM Mesher] generateGeometryFromMapChunkV18PlusMulti failed:', err)
@@ -672,7 +690,7 @@ const meshMultiColumnsFromParsedV16V17 = (
672
690
  meta.occludingBlocks,
673
691
  config?.enableLighting !== false,
674
692
  config?.smoothLighting !== false,
675
- config?.skyLight || 15
693
+ config?.skyLight ?? 15
676
694
  )
677
695
  } catch (err) {
678
696
  console.warn('[WASM Mesher] generateGeometryFromParsedV16V17Multi failed:', err)
@@ -976,6 +994,8 @@ function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeig
976
994
  positions: new Float32Array(0),
977
995
  normals: new Float32Array(0),
978
996
  colors: new Float32Array(0),
997
+ skyLights: new Float32Array(0),
998
+ blockLights: new Float32Array(0),
979
999
  uvs: new Float32Array(0),
980
1000
  indices: new Uint32Array(0),
981
1001
  indicesCount: 0,
@@ -1033,6 +1053,7 @@ function processColumnTick() {
1033
1053
  let preCacheHits = 0
1034
1054
  let preCacheMisses = 0
1035
1055
  let hadError = false
1056
+ let columnMeshPath = 'none'
1036
1057
  // Outer-scope timestamps so we can finalize `processTime` and
1037
1058
  // `postPhase` AFTER the per-section emit loop runs (the loop builds
1038
1059
  // typed arrays, walks block-entity metadata, and calls postMessage —
@@ -1052,6 +1073,7 @@ function processColumnTick() {
1052
1073
  let wasmResult: any
1053
1074
  let t1 = 0
1054
1075
  let usedFusedPath = false
1076
+ columnMeshPath = 'none'
1055
1077
 
1056
1078
  const meta = getBlockMeta(version)
1057
1079
 
@@ -1067,6 +1089,7 @@ function processColumnTick() {
1067
1089
 
1068
1090
  if (rawEntry) {
1069
1091
  wasmResult = meshColumnFromRawV18Plus(rawEntry, x, z, worldMinY, worldMaxY, meta)
1092
+ if (wasmResult) columnMeshPath = 'v18_fused'
1070
1093
  } else if (v17Entry) {
1071
1094
  const v17Light = updateLightV17Cache.get(rawCacheKey(x, z))
1072
1095
  wasmResult = meshColumnFromParsedV16V17(
@@ -1075,6 +1098,7 @@ function processColumnTick() {
1075
1098
  v17Light?.skyLight ?? null, v17Light?.blockLight ?? null,
1076
1099
  x, z, worldMinY, worldMaxY, meta
1077
1100
  )
1101
+ if (wasmResult) columnMeshPath = 'v17_fused'
1078
1102
  } else if (v16Entry) {
1079
1103
  const v16Light = updateLightV16Cache.get(rawCacheKey(x, z))
1080
1104
  const bitMapLoHi = new Uint32Array([v16Entry.bitMap >>> 0, 0])
@@ -1084,6 +1108,7 @@ function processColumnTick() {
1084
1108
  v16Light?.skyLight ?? null, v16Light?.blockLight ?? null,
1085
1109
  x, z, worldMinY, worldMaxY, meta
1086
1110
  )
1111
+ if (wasmResult) columnMeshPath = 'v16_fused'
1087
1112
  }
1088
1113
 
1089
1114
  if (wasmResult) {
@@ -1104,6 +1129,7 @@ function processColumnTick() {
1104
1129
  wasmResult = meshMultiColumnsFromRawV18Plus(chunksToUse, x, z, worldMinY, worldMaxY, meta)
1105
1130
  ?? meshMultiColumnsFromParsedV16V17(chunksToUse, x, z, worldMinY, worldMaxY, meta)
1106
1131
  if (wasmResult) {
1132
+ columnMeshPath = 'multi_fused'
1107
1133
  usedFusedPath = true
1108
1134
  t1 = performance.now()
1109
1135
  wasmPhase = t1 - t0
@@ -1181,8 +1207,9 @@ function processColumnTick() {
1181
1207
  invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
1182
1208
  config?.enableLighting !== false,
1183
1209
  config?.smoothLighting !== false,
1184
- config?.skyLight || 15
1210
+ config?.skyLight ?? 15
1185
1211
  )
1212
+ columnMeshPath = 'two_step_single'
1186
1213
  } else {
1187
1214
  const tBuildStart = performance.now()
1188
1215
  const perChunkLen = conversions[0].blockStates.length
@@ -1214,8 +1241,9 @@ function processColumnTick() {
1214
1241
  invisibleBlocks, transparentBlocks, noAoBlocks, cullIdenticalBlocks, occludingBlocks,
1215
1242
  config?.enableLighting !== false,
1216
1243
  config?.smoothLighting !== false,
1217
- config?.skyLight || 15
1244
+ config?.skyLight ?? 15
1218
1245
  )
1246
+ columnMeshPath = 'two_step_multi'
1219
1247
  }
1220
1248
  }
1221
1249
 
@@ -1306,14 +1334,16 @@ function processColumnTick() {
1306
1334
  for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
1307
1335
  const b = world.getBlock(cursor)
1308
1336
  if (!b) continue
1309
- collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts)
1337
+ collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts, world)
1310
1338
  }
1311
1339
  }
1312
1340
  }
1313
1341
 
1314
1342
  let geometry: MesherGeometryOutput
1315
1343
  let transferable: any[] = []
1316
- const hasLegacyMesh = (exported?.geometry.indices.length ?? 0) > 0
1344
+ const hasOpaqueMesh = (exported?.geometry.indices.length ?? 0) > 0
1345
+ const hasBlendMesh = (exported?.blendGeometry?.indices.length ?? 0) > 0
1346
+ const hasLegacyMesh = hasOpaqueMesh || hasBlendMesh
1317
1347
  const hasShaderCubes = (exported?.shaderCubes?.count ?? 0) > 0
1318
1348
  if (exported && (hasLegacyMesh || hasShaderCubes)) {
1319
1349
  const maxIndex = exported.geometry.indices.length > 0
@@ -1335,6 +1365,8 @@ function processColumnTick() {
1335
1365
  positions: new Float32Array(exported.geometry.positions),
1336
1366
  normals: new Float32Array(exported.geometry.normals),
1337
1367
  colors: new Float32Array(exported.geometry.colors),
1368
+ skyLights: new Float32Array(exported.geometry.skyLights),
1369
+ blockLights: new Float32Array(exported.geometry.blockLights),
1338
1370
  uvs: new Float32Array(exported.geometry.uvs),
1339
1371
  indices: using32Array
1340
1372
  ? new Uint32Array(exported.geometry.indices)
@@ -1360,13 +1392,37 @@ function processColumnTick() {
1360
1392
  formatVersion: 3,
1361
1393
  }
1362
1394
  }
1395
+ if (exported.blendGeometry && hasBlendMesh) {
1396
+ const blendMax = Math.max(...exported.blendGeometry.indices)
1397
+ geometry.blend = {
1398
+ positions: new Float32Array(exported.blendGeometry.positions),
1399
+ normals: new Float32Array(exported.blendGeometry.normals),
1400
+ colors: new Float32Array(exported.blendGeometry.colors),
1401
+ skyLights: new Float32Array(exported.blendGeometry.skyLights),
1402
+ blockLights: new Float32Array(exported.blendGeometry.blockLights),
1403
+ uvs: new Float32Array(exported.blendGeometry.uvs),
1404
+ indices: blendMax > 65535
1405
+ ? new Uint32Array(exported.blendGeometry.indices)
1406
+ : new Uint16Array(exported.blendGeometry.indices),
1407
+ }
1408
+ }
1363
1409
  transferable = [
1364
1410
  geometry.positions?.buffer,
1365
1411
  geometry.normals?.buffer,
1366
1412
  geometry.colors?.buffer,
1413
+ geometry.skyLights?.buffer,
1414
+ geometry.blockLights?.buffer,
1367
1415
  geometry.uvs?.buffer,
1368
1416
  //@ts-ignore
1369
1417
  geometry.indices?.buffer,
1418
+ geometry.blend?.positions?.buffer,
1419
+ geometry.blend?.normals?.buffer,
1420
+ geometry.blend?.colors?.buffer,
1421
+ geometry.blend?.skyLights?.buffer,
1422
+ geometry.blend?.blockLights?.buffer,
1423
+ geometry.blend?.uvs?.buffer,
1424
+ //@ts-ignore
1425
+ geometry.blend?.indices?.buffer,
1370
1426
  geometry.shaderCubes?.words?.buffer,
1371
1427
  ].filter(Boolean)
1372
1428
 
@@ -0,0 +1,11 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import { sectionYsForLightColumnDirty } from './mesherWasmLightDirty'
4
+
5
+ describe('sectionYsForLightColumnDirty', () => {
6
+ it('covers every section in a 256-high overworld column', () => {
7
+ expect(sectionYsForLightColumnDirty(0, 256)).toEqual(
8
+ Array.from({ length: 16 }, (_, i) => i * 16),
9
+ )
10
+ })
11
+ })
@@ -0,0 +1,15 @@
1
+ //@ts-nocheck
2
+ import { SECTION_HEIGHT } from '../../mesher-shared/shared'
3
+
4
+ /** Section Y values to dirty for a column after `update_light` updates the light cache. */
5
+ export function sectionYsForLightColumnDirty (
6
+ worldMinY: number,
7
+ worldMaxY: number,
8
+ sectionHeight = SECTION_HEIGHT,
9
+ ): number[] {
10
+ const ys: number[] = []
11
+ for (let y = worldMinY; y < worldMaxY; y += sectionHeight) {
12
+ ys.push(y)
13
+ }
14
+ return ys
15
+ }
@@ -320,6 +320,17 @@ export class WorldView extends (EventEmitter as new () => TypedEmitter<WorldView
320
320
  }
321
321
  }
322
322
 
323
+ /**
324
+ * Re-fetch and re-emit every loaded chunk (e.g. after mesher workers are recreated).
325
+ */
326
+ async reloadLoadedChunks(): Promise<void> {
327
+ const coords = Object.keys(this.loadedChunks)
328
+ for (const key of coords) {
329
+ const [x, z] = key.split(',').map(Number)
330
+ await this.loadChunk({ x, z }, false, 'mesher-reconfigure')
331
+ }
332
+ }
333
+
323
334
  /**
324
335
  * Unload all chunks.
325
336
  */