minecraft-renderer 0.1.79 → 0.1.80

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 (200) hide show
  1. package/README.md +29 -25
  2. package/dist/mesher.js +36 -36
  3. package/dist/mesher.js.map +4 -4
  4. package/dist/mesherWasm.js +217 -217
  5. package/dist/minecraft-renderer.js +27 -27
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +133 -133
  8. package/package.json +5 -2
  9. package/src/bundler/bundlePrepare.ts +3 -8
  10. package/src/graphicsBackend/appViewer.ts +10 -7
  11. package/src/graphicsBackend/config.ts +1 -5
  12. package/src/graphicsBackend/playerState.ts +35 -34
  13. package/src/graphicsBackend/preloadWorkers.ts +43 -41
  14. package/src/graphicsBackend/rendererDefaultOptions.ts +68 -70
  15. package/src/graphicsBackend/rendererOptionsSync.ts +24 -50
  16. package/src/graphicsBackend/types.ts +2 -2
  17. package/src/index.ts +6 -27
  18. package/src/lib/DebugGui.ts +34 -31
  19. package/src/lib/animationController.ts +9 -9
  20. package/src/lib/bakeLegacyLight.ts +1 -6
  21. package/src/lib/bindAbortableListener.test.ts +32 -12
  22. package/src/lib/bindAbortableListener.ts +14 -6
  23. package/src/lib/blockEntityLightRegistry.ts +11 -20
  24. package/src/lib/blockEntityLighting.test.ts +1 -6
  25. package/src/lib/blockEntityLighting.ts +5 -5
  26. package/src/lib/buildSharedConfig.mjs +4 -4
  27. package/src/lib/buildWorkerMcDataIndexes.test.ts +4 -4
  28. package/src/lib/buildWorkerMcDataIndexes.ts +11 -19
  29. package/src/lib/cameraBobbing.ts +1 -1
  30. package/src/lib/canvas2DOverlay.example.ts +13 -33
  31. package/src/lib/canvas2DOverlay.quickstart.ts +24 -54
  32. package/src/lib/canvas2DOverlay.ts +28 -47
  33. package/src/lib/cleanupDecorator.ts +2 -2
  34. package/src/lib/createPlayerObject.test.ts +2 -7
  35. package/src/lib/createPlayerObject.ts +18 -25
  36. package/src/lib/frameTimingCollector.ts +2 -5
  37. package/src/lib/guiRenderer.ts +57 -42
  38. package/src/lib/items.ts +15 -9
  39. package/src/lib/mesherlogReader.ts +5 -6
  40. package/src/lib/simpleUtils.ts +5 -5
  41. package/src/lib/skyLight.ts +3 -3
  42. package/src/lib/smoothSwitcher.ts +11 -18
  43. package/src/lib/ui/newStats.ts +10 -12
  44. package/src/lib/utils/proxy.ts +7 -1
  45. package/src/lib/utils/skins.ts +4 -4
  46. package/src/lib/utils.ts +1 -1
  47. package/src/lib/workerMessageSanitize.ts +5 -12
  48. package/src/lib/workerProxy.ts +52 -42
  49. package/src/lib/workerSyncOps.test.ts +58 -41
  50. package/src/lib/worldrendererCommon.reconfigure.test.ts +24 -21
  51. package/src/lib/worldrendererCommon.removeColumn.test.ts +11 -13
  52. package/src/lib/worldrendererCommon.ts +338 -211
  53. package/src/mesher-legacy/mesher.ts +9 -4
  54. package/src/mesher-legacy/test/a.ts +1 -1
  55. package/src/mesher-legacy/test/mesherTester.ts +2 -2
  56. package/src/mesher-legacy/test/playground.ts +1 -1
  57. package/src/mesher-legacy/test/run/test-js.ts +1 -1
  58. package/src/mesher-legacy/test/snapshotUtils.ts +3 -8
  59. package/src/mesher-legacy/test/test-perf.ts +1 -1
  60. package/src/mesher-legacy/test/tests.test.ts +2 -2
  61. package/src/mesher-shared/blockEntityMetadata.test.ts +7 -5
  62. package/src/mesher-shared/blockEntityMetadata.ts +28 -19
  63. package/src/mesher-shared/blockPropertiesForMeshing.ts +2 -2
  64. package/src/mesher-shared/computeHeightmap.ts +2 -4
  65. package/src/mesher-shared/exportedGeometryTypes.ts +3 -3
  66. package/src/mesher-shared/faceOcclusion.ts +324 -0
  67. package/src/mesher-shared/models.ts +80 -131
  68. package/src/mesher-shared/modelsGeometryCommon.ts +30 -15
  69. package/src/mesher-shared/shared.ts +46 -46
  70. package/src/mesher-shared/standaloneRenderer.ts +40 -44
  71. package/src/mesher-shared/tests/faceOcclusion.test.ts +179 -0
  72. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +12 -8
  73. package/src/mesher-shared/vertexShading.ts +2 -11
  74. package/src/mesher-shared/world.ts +19 -19
  75. package/src/mesher-shared/worldLighting.test.ts +2 -2
  76. package/src/performanceMonitor/PerformanceMonitor.ts +4 -13
  77. package/src/performanceMonitor/formatPerformanceFactorsDebug.ts +2 -2
  78. package/src/performanceMonitor/index.ts +1 -5
  79. package/src/performanceMonitor/types.ts +1 -1
  80. package/src/playerState/playerState.ts +55 -50
  81. package/src/playerState/types.ts +2 -8
  82. package/src/playground/allEntitiesDebug.ts +19 -16
  83. package/src/playground/baseScene.ts +29 -32
  84. package/src/playground/mobileControls.tsx +133 -129
  85. package/src/playground/playground.html +76 -73
  86. package/src/playground/playground.ts +14 -1
  87. package/src/playground/playgroundUi.tsx +100 -74
  88. package/src/playground/reactUtils.ts +3 -7
  89. package/src/playground/scenes/entities.ts +10 -7
  90. package/src/playground/scenes/frequentUpdates.ts +5 -5
  91. package/src/playground/scenes/geometryExport.ts +6 -20
  92. package/src/playground/scenes/highPosEntityUpdates.ts +2 -10
  93. package/src/playground/scenes/index.ts +1 -1
  94. package/src/playground/scenes/lightingStarfield.ts +18 -12
  95. package/src/playground/scenes/main.ts +15 -11
  96. package/src/playground/scenes/partialBlockCulling.ts +31 -0
  97. package/src/playground/scenes/railsCobweb.ts +1 -1
  98. package/src/playground/scenes/rotationIssue.ts +1 -1
  99. package/src/playground/scenes/slabsOptimization.ts +1 -2
  100. package/src/playground/scenes/transparencyIssue.ts +1 -1
  101. package/src/playground/shared.ts +2 -2
  102. package/src/resourcesManager/resourcesManager.ts +29 -54
  103. package/src/resourcesManager/resourcesManager.worker.test.ts +6 -10
  104. package/src/shims/minecraftData.ts +70 -24
  105. package/src/sign-renderer/index.html +10 -12
  106. package/src/sign-renderer/index.ts +29 -29
  107. package/src/sign-renderer/playground.ts +22 -15
  108. package/src/sign-renderer/tests.test.ts +16 -13
  109. package/src/sign-renderer/vite.config.ts +2 -2
  110. package/src/three/appShared.ts +46 -25
  111. package/src/three/bannerRenderer.ts +33 -61
  112. package/src/three/cameraShake.ts +15 -6
  113. package/src/three/chunkMeshManager.ts +239 -282
  114. package/src/three/cinimaticScript.ts +26 -15
  115. package/src/three/cubeDrawSpans.ts +27 -37
  116. package/src/three/cubeMultiDraw.ts +9 -22
  117. package/src/three/documentRenderer.ts +15 -18
  118. package/src/three/entities.ts +175 -150
  119. package/src/three/entity/EntityMesh.ts +114 -108
  120. package/src/three/entity/animations.js +3 -3
  121. package/src/three/entity/armorModels.ts +15 -15
  122. package/src/three/entity/gltfAnimationUtils.ts +2 -5
  123. package/src/three/fireworks.ts +34 -32
  124. package/src/three/fireworksRenderer.ts +32 -29
  125. package/src/three/globalBlockBuffer.ts +167 -100
  126. package/src/three/globalLegacyBuffer.ts +257 -166
  127. package/src/three/graphicsBackendBase.ts +19 -25
  128. package/src/three/graphicsBackendOffThread.ts +12 -15
  129. package/src/three/hand.ts +21 -21
  130. package/src/three/handLegacy.ts +3 -13
  131. package/src/three/holdingBlock.ts +116 -79
  132. package/src/three/holdingBlockFactory.ts +1 -4
  133. package/src/three/holdingBlockItemIdentity.test.ts +25 -17
  134. package/src/three/holdingBlockItemIdentity.ts +9 -6
  135. package/src/three/holdingBlockLegacy.ts +113 -132
  136. package/src/three/itemMesh.ts +20 -29
  137. package/src/three/legacyMultiDraw.ts +10 -16
  138. package/src/three/legacySectionCull.ts +8 -14
  139. package/src/three/menuBackground/activeView.ts +1 -4
  140. package/src/three/menuBackground/assetUrl.ts +1 -1
  141. package/src/three/menuBackground/classic.ts +4 -12
  142. package/src/three/menuBackground/gpuPreference.ts +1 -3
  143. package/src/three/menuBackground/index.ts +2 -11
  144. package/src/three/menuBackground/renderer.ts +1 -5
  145. package/src/three/menuBackground/types.ts +1 -4
  146. package/src/three/menuBackground/v2.ts +196 -96
  147. package/src/three/menuBackground/v2Meta.ts +3 -3
  148. package/src/three/menuBackground/worldBlocks.ts +37 -35
  149. package/src/three/modules/blockBreakParticles.ts +59 -28
  150. package/src/three/modules/cameraBobbing.ts +3 -6
  151. package/src/three/modules/index.ts +1 -1
  152. package/src/three/modules/rain.ts +10 -23
  153. package/src/three/modules/sciFiWorldReveal.ts +11 -25
  154. package/src/three/modules/starfield.ts +7 -13
  155. package/src/three/renderSlot.ts +28 -16
  156. package/src/three/rendererModuleSystem.ts +1 -3
  157. package/src/three/sceneOrigin.ts +96 -33
  158. package/src/three/sectionRaycastAabb.ts +30 -18
  159. package/src/three/shaderCubeMesh.ts +6 -17
  160. package/src/three/shaders/cubeBlockShader.ts +85 -96
  161. package/src/three/shaders/legacyBlockShader.ts +21 -43
  162. package/src/three/shaders/textureIndexMapping.ts +89 -95
  163. package/src/three/shaders/tintPalette.ts +156 -170
  164. package/src/three/signTextureCache.ts +9 -12
  165. package/src/three/skyboxRenderer.ts +62 -70
  166. package/src/three/tests/chunkMeshManagerLegacy.test.ts +34 -34
  167. package/src/three/tests/cubeDrawSpans.test.ts +55 -25
  168. package/src/three/tests/globalBlockBuffer.test.ts +217 -0
  169. package/src/three/tests/globalLegacyBuffer.test.ts +300 -41
  170. package/src/three/tests/legacyMultiDraw.test.ts +8 -29
  171. package/src/three/tests/pr3PerFrameCpuCuts.test.ts +106 -29
  172. package/src/three/tests/signTextureCache.test.ts +4 -4
  173. package/src/three/threeJsMedia.ts +55 -72
  174. package/src/three/threeJsParticles.ts +32 -41
  175. package/src/three/threeJsSound.ts +3 -3
  176. package/src/three/threeJsUtils.ts +3 -3
  177. package/src/three/threeWorker.ts +2 -2
  178. package/src/three/waypointSprite.ts +55 -82
  179. package/src/three/waypoints.ts +20 -23
  180. package/src/three/world/cursorBlock.ts +25 -12
  181. package/src/three/world/vr.ts +11 -17
  182. package/src/three/worldGeometryExport.ts +15 -34
  183. package/src/three/worldRendererThree.ts +117 -129
  184. package/src/wasm-mesher/bridge/convertChunk.ts +3 -9
  185. package/src/wasm-mesher/bridge/render-from-wasm.ts +204 -229
  186. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +26 -67
  187. package/src/wasm-mesher/tests/cullingRegression.test.ts +307 -0
  188. package/src/wasm-mesher/tests/heightmapParity.test.ts +8 -18
  189. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +2 -2
  190. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +17 -4
  191. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +103 -141
  192. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +14 -22
  193. package/src/wasm-mesher/worker/mesherWasm.ts +150 -141
  194. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +2 -8
  195. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +1 -3
  196. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +1 -5
  197. package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +42 -42
  198. package/src/worldView/worldView.spiral.test.ts +1 -1
  199. package/src/worldView/worldView.ts +34 -39
  200. package/src/worldView/worldViewWorkerBridge.test.ts +4 -4
@@ -28,7 +28,7 @@ const FACE_NAME_TO_INDEX: Record<string, number> = {
28
28
  east: 2,
29
29
  west: 3,
30
30
  south: 4,
31
- north: 5,
31
+ north: 5
32
32
  }
33
33
 
34
34
  /**
@@ -42,17 +42,13 @@ export const AO_LIGHT_REMAP: readonly (readonly number[])[] = [
42
42
  [0, 1, 2, 3], // EAST
43
43
  [0, 1, 2, 3], // WEST
44
44
  [2, 3, 0, 1], // SOUTH
45
- [2, 3, 0, 1], // NORTH
45
+ [2, 3, 0, 1] // NORTH
46
46
  ] as const
47
47
 
48
48
  /** Reorder per-corner AO or light values for shader-space corners. */
49
- export function remapCornersForShaderFace(
50
- faceIdx: number,
51
- values: number[],
52
- fallback: number,
53
- ): number[] {
49
+ export function remapCornersForShaderFace(faceIdx: number, values: number[], fallback: number): number[] {
54
50
  const map = AO_LIGHT_REMAP[faceIdx] ?? AO_LIGHT_REMAP[0]
55
- return map.map((i) => values[i] ?? fallback)
51
+ return map.map(i => values[i] ?? fallback)
56
52
  }
57
53
 
58
54
  export interface ShaderCubeBlockInput {
@@ -77,8 +73,8 @@ export interface ShaderCubeModelInput {
77
73
  y?: number
78
74
  z?: number
79
75
  elements?: Array<{
80
- rotation?: { axis: string, angle: number, origin: number[] }
81
- faces?: Record<string, { texture?: TextureEntry & { rotation?: number }, tintindex?: number }>
76
+ rotation?: { axis: string; angle: number; origin: number[] }
77
+ faces?: Record<string, { texture?: TextureEntry & { rotation?: number }; tintindex?: number }>
82
78
  }>
83
79
  }
84
80
  }
@@ -88,11 +84,7 @@ let textureIndexMapping: TextureIndexMapping | null = null
88
84
  let tintsMissingWarned = false
89
85
 
90
86
  /** Convert mc-assets texture scales (normalized or negative) to pixel tile size for index lookup. */
91
- function normalizeTextureEntryForTileIndex(
92
- tex: { u?: number, v?: number, su?: number, sv?: number },
93
- atlasWidth: number,
94
- tileSize: number,
95
- ): TextureEntry {
87
+ function normalizeTextureEntryForTileIndex(tex: { u?: number; v?: number; su?: number; sv?: number }, atlasWidth: number, tileSize: number): TextureEntry {
96
88
  let u = tex.u ?? 0
97
89
  let v = tex.v ?? 0
98
90
  let su = tex.su ?? tileSize
@@ -109,20 +101,13 @@ function normalizeTextureEntryForTileIndex(
109
101
  type FaceTextureRef = TextureEntry & { tileIndex?: number }
110
102
 
111
103
  /** Prefer atlas `tileIndex` from block model (legacy path uses the same). */
112
- export function resolveFaceTileIndex(
113
- tex: FaceTextureRef,
114
- texMapping: TextureIndexMapping,
115
- ): number {
104
+ export function resolveFaceTileIndex(tex: FaceTextureRef, texMapping: TextureIndexMapping): number {
116
105
  const fromAtlas = tex.tileIndex
117
106
  // tile 0 is a special/atlas-padding slot; use pixel fallback for block faces
118
107
  if (typeof fromAtlas === 'number' && fromAtlas > 0 && fromAtlas < 4096) {
119
108
  return fromAtlas
120
109
  }
121
- const entry = normalizeTextureEntryForTileIndex(
122
- tex,
123
- texMapping.getTilesPerRow() * 16,
124
- 16,
125
- )
110
+ const entry = normalizeTextureEntryForTileIndex(tex, texMapping.getTilesPerRow() * 16, 16)
126
111
  return texMapping.tileIndexFromTextureEntry(entry)
127
112
  }
128
113
 
@@ -158,7 +143,7 @@ export function getShaderCubeResources(): {
158
143
  height: latest.height,
159
144
  tileSize: latest.tileSize ?? 16,
160
145
  suSv: latest.suSv ?? 16,
161
- textures: latest.textures ?? {},
146
+ textures: latest.textures ?? {}
162
147
  })
163
148
  }
164
149
  return { tintPalette, textureIndexMapping }
@@ -181,7 +166,7 @@ export function isShaderCubeBlock(
181
166
  cached: ShaderCubeModelInput & { isCube: boolean },
182
167
  model: ShaderCubeModelInput['model'],
183
168
  sectionHeight: number,
184
- texMapping: TextureIndexMapping,
169
+ texMapping: TextureIndexMapping
185
170
  ): boolean {
186
171
  if (sectionHeight !== 16) return false
187
172
  if (!cached.isCube) return false
@@ -214,14 +199,7 @@ export function isShaderCubeBlock(
214
199
  return true
215
200
  }
216
201
 
217
- function packWord0(
218
- lx: number,
219
- ly: number,
220
- lz: number,
221
- faceId: number,
222
- tintIndex: number,
223
- ao: number[],
224
- ): number {
202
+ function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[]): number {
225
203
  let w = 0
226
204
  w |= (lx & 0xf) << WORD0.LX_SHIFT
227
205
  w |= (ly & 0xf) << WORD0.LY_SHIFT
@@ -246,13 +224,7 @@ function biasedSectionIndex(sectionBaseCoord: number): number {
246
224
  return (Math.floor(sectionBaseCoord / 16) + WORD3.SECTION_BIAS) & WORD3.SECTION_MASK
247
225
  }
248
226
 
249
- export function packWord2(
250
- texIndex: number,
251
- aoDiagonalFlip: boolean,
252
- sectionBaseX: number,
253
- sectionBaseY: number,
254
- sectionBaseZ: number,
255
- ): number {
227
+ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number): number {
256
228
  let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
257
229
  if (aoDiagonalFlip) {
258
230
  w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
@@ -273,7 +245,7 @@ export function packWord3(sectionBaseX: number, sectionBaseZ: number): number {
273
245
  }
274
246
 
275
247
  /** Decode section base block coords from packed words (round-trip helper for tests). */
276
- export function decodeSectionBaseFromWords(word2: number, word3: number): { x: number, y: number, z: number } {
248
+ export function decodeSectionBaseFromWords(word2: number, word3: number): { x: number; y: number; z: number } {
277
249
  const sX = ((word3 & 0xffff) | (((word2 >>> WORD2.SECTION_X_HI_SHIFT) & 0x3f) << 16)) - WORD3.SECTION_BIAS
278
250
  const sZ = (((word3 >>> 16) & 0xffff) | (((word2 >>> WORD2.SECTION_Z_HI_SHIFT) & 0x3f) << 16)) - WORD3.SECTION_BIAS
279
251
  const sY = ((word2 >>> WORD2.SECTION_Y_SHIFT) & ((1 << WORD2.SECTION_Y_BITS) - 1)) - 4
@@ -290,16 +262,13 @@ export function unpackTexIndexFromWord2(word2: number): number {
290
262
  return word2 & ((1 << WORD2.TEX_INDEX_BITS) - 1)
291
263
  }
292
264
 
293
- function packCornerLightByte (skyNorm: number, blockNorm: number): number {
265
+ function packCornerLightByte(skyNorm: number, blockNorm: number): number {
294
266
  const sky4 = Math.min(15, Math.round(Math.max(0, skyNorm) * 15))
295
267
  const block4 = Math.min(15, Math.round(Math.max(0, blockNorm) * 15))
296
268
  return ((sky4 << 4) | block4) & 0xff
297
269
  }
298
270
 
299
- function lightCombinedForFace(
300
- block: ShaderCubeBlockInput,
301
- faceDataIndex: number,
302
- ): number[] {
271
+ function lightCombinedForFace(block: ShaderCubeBlockInput, faceDataIndex: number): number[] {
303
272
  const packed = block.light_combined?.[faceDataIndex]
304
273
  if (packed && packed.length === 4) {
305
274
  return packed
@@ -310,7 +279,7 @@ function lightCombinedForFace(
310
279
  return sky.map((s, i) => packCornerLightByte(s ?? 1, blockL[i] ?? 0))
311
280
  }
312
281
  const floats = block.light_data?.[faceDataIndex] ?? [1, 1, 1, 1]
313
- return floats.map((f) => packCornerLightByte(f, 0))
282
+ return floats.map(f => packCornerLightByte(f, 0))
314
283
  }
315
284
 
316
285
  function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number> {
@@ -326,7 +295,7 @@ function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number>
326
295
  }
327
296
 
328
297
  export type BuildShaderCubeInstancesOpts = {
329
- sectionOrigin: { x: number, y: number, z: number }
298
+ sectionOrigin: { x: number; y: number; z: number }
330
299
  sectionHeight: number
331
300
  biome?: string
332
301
  tintPalette: TintPalette
@@ -336,6 +305,8 @@ export type BuildShaderCubeInstancesOpts = {
336
305
  * diagonal flip — matches legacy render-from-wasm path.
337
306
  */
338
307
  doAO?: boolean
308
+ /** Bitmask of faces to skip (same indices as visible_faces: 0=up … 5=north). */
309
+ forceCullMask?: number
339
310
  }
340
311
 
341
312
  /**
@@ -347,16 +318,9 @@ export function tryBuildShaderCubeInstances(
347
318
  cached: ShaderCubeModelInput & { isCube: boolean },
348
319
  model: ShaderCubeModelInput['model'],
349
320
  opts: BuildShaderCubeInstancesOpts,
350
- words: number[],
321
+ words: number[]
351
322
  ): boolean {
352
- const {
353
- sectionOrigin,
354
- sectionHeight,
355
- biome,
356
- tintPalette,
357
- textureIndexMapping,
358
- doAO = true,
359
- } = opts
323
+ const { sectionOrigin, sectionHeight, biome, tintPalette, textureIndexMapping, doAO = true, forceCullMask = 0 } = opts
360
324
 
361
325
  if (!isShaderCubeBlock(cached, model, sectionHeight, textureIndexMapping)) {
362
326
  return false
@@ -375,6 +339,7 @@ export function tryBuildShaderCubeInstances(
375
339
  for (const faceName of WASM_FACE_ORDER) {
376
340
  const faceIdx = FACE_NAME_TO_INDEX[faceName]
377
341
  if ((block.visible_faces & (1 << faceIdx)) === 0) continue
342
+ if ((forceCullMask & (1 << faceIdx)) !== 0) continue
378
343
 
379
344
  const faceDataIndex = wasmFaceToDataIndex[faceIdx]
380
345
  if (faceDataIndex === undefined) continue
@@ -404,20 +369,14 @@ export function tryBuildShaderCubeInstances(
404
369
  aoDiagonalFlip = false
405
370
  }
406
371
 
407
- const tintIndex = tintPalette.getTintIndex(
408
- eFace.tintindex,
409
- cached.blockName,
410
- cached.blockProps,
411
- biome ?? 'plains',
412
- )
372
+ const tintIndex = tintPalette.getTintIndex(eFace.tintindex, cached.blockName, cached.blockProps, biome ?? 'plains')
413
373
 
414
374
  words.push(
415
375
  packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
416
376
  packWord1(lightCombined),
417
377
  packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z),
418
- packWord3(sectionOrigin.x, sectionOrigin.z),
378
+ packWord3(sectionOrigin.x, sectionOrigin.z)
419
379
  )
420
-
421
380
  }
422
381
 
423
382
  return true
@@ -429,7 +388,7 @@ export function buildShaderCubesFromWords(wordQuads: number[]): ShaderCubesOutpu
429
388
  return {
430
389
  words: new Uint32Array(wordQuads),
431
390
  count: faceCount,
432
- formatVersion: SHADER_CUBES_FORMAT_VERSION,
391
+ formatVersion: SHADER_CUBES_FORMAT_VERSION
433
392
  }
434
393
  }
435
394
 
@@ -0,0 +1,307 @@
1
+ //@ts-nocheck
2
+ import { test, expect, beforeAll, beforeEach } from 'vitest'
3
+ import { readFileSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { dirname, join } from 'node:path'
6
+ import Chunks from 'prismarine-chunk'
7
+ import MinecraftData from 'minecraft-data'
8
+ import { Vec3 } from 'vec3'
9
+ import PrismarineBlockLoader from 'prismarine-block'
10
+ import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
11
+ import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
12
+ import { World } from '../../mesher-shared/world'
13
+ import { setBlockStatesData, getSectionGeometry } from '../../mesher-shared/models'
14
+ import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
15
+ import { convertChunkToWasm } from '../bridge/convertChunk'
16
+ import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
17
+
18
+ const VERSION = '1.18.2'
19
+ const SECTION_Y = 0
20
+ const SECTION_HEIGHT = 16
21
+
22
+ const DRY_STAIR_PROPS = { facing: 'east', half: 'bottom', shape: 'straight', waterlogged: false } as const
23
+
24
+ type BlockSpec = { x: number; y: number; z: number; name: string; props?: Record<string, string | boolean> }
25
+
26
+ let wasmModule: typeof import('../runtime-build/wasm_mesher.js')
27
+
28
+ beforeAll(async () => {
29
+ wasmModule = await import('../runtime-build/wasm_mesher.js')
30
+ const wasmDir = dirname(fileURLToPath(import.meta.url))
31
+ const wasmBytes = readFileSync(join(wasmDir, '../runtime-build/wasm_mesher_bg.wasm'))
32
+ wasmModule.initSync(wasmBytes)
33
+ })
34
+
35
+ beforeEach(() => {
36
+ resetFaceOcclusionCache()
37
+ const mcData = MinecraftData(VERSION)
38
+ setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
39
+ ;(globalThis as any).__wasmBlockModelCache = new Map()
40
+ })
41
+
42
+ function resolveStateId(mcData: ReturnType<typeof MinecraftData>, name: string, props?: Record<string, string | boolean>) {
43
+ const block = mcData.blocksByName[name]
44
+ if (!block) throw new Error(`Unknown block: ${name}`)
45
+ if (!props || Object.keys(props).length === 0) return block.defaultState
46
+ const Block = PrismarineBlockLoader(VERSION)
47
+ const requested = { ...props }
48
+ if (!('waterlogged' in requested)) requested.waterlogged = false
49
+
50
+ const matches: number[] = []
51
+ for (let id = block.minStateId; id <= block.maxStateId; id++) {
52
+ const stateProps = Block.fromStateId(id, 1).getProperties() as Record<string, string | boolean>
53
+ let match = true
54
+ for (const [key, val] of Object.entries(requested)) {
55
+ if (stateProps[key] !== val) {
56
+ match = false
57
+ break
58
+ }
59
+ }
60
+ if (match) matches.push(id)
61
+ }
62
+ if (matches.length === 0) throw new Error(`No state for ${name} ${JSON.stringify(props)}`)
63
+ return matches[0]!
64
+ }
65
+
66
+ function buildWorld(blocks: BlockSpec[]): World {
67
+ const mcData = MinecraftData(VERSION)
68
+ const Chunk = Chunks(VERSION) as any
69
+ const chunk = new Chunk(undefined as any)
70
+
71
+ for (const b of blocks) {
72
+ const id = resolveStateId(mcData, b.name, b.props)
73
+ chunk.setBlockStateId(new Vec3(b.x, b.y, b.z), id)
74
+ chunk.setBlockLight(new Vec3(b.x, b.y, b.z), 15)
75
+ chunk.setSkyLight(new Vec3(b.x, b.y, b.z), 15)
76
+ }
77
+
78
+ const world = new World(VERSION)
79
+ world.addColumn(0, 0, chunk.toJson())
80
+ return world
81
+ }
82
+
83
+ function countQuadsFromLegacy(world: World): number {
84
+ const geo = getSectionGeometry(0, SECTION_Y, 0, world, SECTION_HEIGHT)
85
+ const opaque = geo.indicesCount / 6
86
+ const blend = geo.blend ? geo.blend.indices.length / 6 : 0
87
+ return opaque + blend
88
+ }
89
+
90
+ function countQuadsFromWasm(world: World, shaderCubes = false): number {
91
+ const column = world.getColumn(0, 0)!
92
+ const conversion = convertChunkToWasm(column, VERSION, 0, 0, SECTION_Y, SECTION_Y + SECTION_HEIGHT, SECTION_Y, SECTION_HEIGHT)
93
+ const wasmResult = wasmModule.generate_geometry(
94
+ 0,
95
+ SECTION_Y,
96
+ 0,
97
+ SECTION_HEIGHT,
98
+ SECTION_Y,
99
+ SECTION_Y + SECTION_HEIGHT,
100
+ SECTION_Y,
101
+ conversion.blockStates,
102
+ conversion.blockLight,
103
+ conversion.skyLight,
104
+ conversion.biomesArray,
105
+ conversion.invisibleBlocks,
106
+ conversion.transparentBlocks,
107
+ conversion.noAoBlocks,
108
+ conversion.cullIdenticalBlocks,
109
+ conversion.occludingBlocks,
110
+ true,
111
+ false,
112
+ 15
113
+ )
114
+ const section = renderWasmOutputToGeometry(wasmResult, VERSION, '0,0,0', { x: 8, y: 8, z: 8 }, world, {
115
+ sectionHeight: SECTION_HEIGHT,
116
+ shaderCubes
117
+ })
118
+ const opaque = section.geometry.indices.length / 6
119
+ const blend = section.blendGeometry?.indices.length ? section.blendGeometry.indices.length / 6 : 0
120
+ const shader = shaderCubes ? (section.shaderCubes?.count ?? 0) : 0
121
+ return opaque + blend + shader
122
+ }
123
+
124
+ function assertMesherParity(world: World, expectedQuads: number) {
125
+ const legacy = countQuadsFromLegacy(world)
126
+ const wasmLegacy = countQuadsFromWasm(world, false)
127
+ const wasmShader = countQuadsFromWasm(world, true)
128
+ expect(legacy).toBe(expectedQuads)
129
+ expect(wasmLegacy).toBe(expectedQuads)
130
+ expect(wasmShader).toBe(expectedQuads)
131
+ }
132
+
133
+ function farmlandFieldBlocks(): BlockSpec[] {
134
+ const blocks: BlockSpec[] = []
135
+ for (let z = 0; z < 16; z++) {
136
+ for (let x = 0; x < 16; x++) {
137
+ blocks.push({ x, y: 4, z, name: 'dirt' })
138
+ blocks.push({ x, y: 5, z, name: 'farmland' })
139
+ }
140
+ }
141
+ return blocks
142
+ }
143
+
144
+ function slabFieldBlocks(): BlockSpec[] {
145
+ const blocks: BlockSpec[] = []
146
+ for (let z = 0; z < 4; z++) {
147
+ for (let x = 0; x < 4; x++) {
148
+ blocks.push({ x, y: 0, z, name: 'stone' })
149
+ blocks.push({ x, y: 1, z, name: 'stone_slab' })
150
+ }
151
+ }
152
+ return blocks
153
+ }
154
+
155
+ function cutCopperStairsRunBlocks(): BlockSpec[] {
156
+ const blocks: BlockSpec[] = []
157
+ for (let x = 0; x < 8; x++) {
158
+ blocks.push({ x, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
159
+ blocks.push({ x, y: 0, z: 1, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
160
+ }
161
+ return blocks
162
+ }
163
+
164
+ function cutCopperStairsAscentBlocks(): BlockSpec[] {
165
+ const blocks: BlockSpec[] = []
166
+ for (let x = 0; x < 6; x++) {
167
+ blocks.push({ x, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
168
+ blocks.push({ x, y: 1, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
169
+ }
170
+ return blocks
171
+ }
172
+
173
+ function cutCopperStairsWestRunBlocks(): BlockSpec[] {
174
+ const blocks: BlockSpec[] = []
175
+ for (let x = 0; x < 8; x++) {
176
+ blocks.push({
177
+ x,
178
+ y: 0,
179
+ z: 0,
180
+ name: 'cut_copper_stairs',
181
+ props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false }
182
+ })
183
+ blocks.push({
184
+ x,
185
+ y: 0,
186
+ z: 1,
187
+ name: 'cut_copper_stairs',
188
+ props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false }
189
+ })
190
+ }
191
+ return blocks
192
+ }
193
+
194
+ function cutCopperStairsEastWestMirrorBlocks(): BlockSpec[] {
195
+ const blocks: BlockSpec[] = []
196
+ for (let x = 0; x < 3; x++) {
197
+ blocks.push({ x, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
198
+ }
199
+ for (let x = 0; x < 3; x++) {
200
+ blocks.push({
201
+ x: x + 4,
202
+ y: 0,
203
+ z: 0,
204
+ name: 'cut_copper_stairs',
205
+ props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false }
206
+ })
207
+ }
208
+ return blocks
209
+ }
210
+
211
+ function countQuadsForFacing(facing: 'east' | 'west' | 'south' | 'north', half: 'bottom' | 'top'): number {
212
+ const world = buildWorld([{ x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { facing, half, shape: 'straight', waterlogged: false } }])
213
+ return countQuadsFromWasm(world)
214
+ }
215
+
216
+ function glassLeavesClusterBlocks(): BlockSpec[] {
217
+ return [
218
+ { x: 0, y: 0, z: 0, name: 'stone' },
219
+ { x: 1, y: 0, z: 0, name: 'glass' },
220
+ { x: 0, y: 0, z: 1, name: 'oak_leaves' },
221
+ { x: 1, y: 0, z: 1, name: 'glass' }
222
+ ]
223
+ }
224
+
225
+ test('culling regression: farmland field — internal side faces culled (pre-fix ~8704 quads)', () => {
226
+ const world = buildWorld(farmlandFieldBlocks())
227
+ // Post-fix: 640 quads (legacy + WASM agree). Pre-fix was ~8704 with all internal farmland sides drawn.
228
+ assertMesherParity(world, 640)
229
+ })
230
+
231
+ test('culling regression: slab field', () => {
232
+ const world = buildWorld(slabFieldBlocks())
233
+ assertMesherParity(world, 64)
234
+ })
235
+
236
+ test('culling regression: single cut copper stair legacy vs wasm', () => {
237
+ const world = buildWorld([{ x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } }])
238
+ assertMesherParity(world, 11)
239
+ })
240
+
241
+ test('culling regression: cut copper stairs run (east-facing)', () => {
242
+ const world = buildWorld(cutCopperStairsRunBlocks())
243
+ assertMesherParity(world, 116)
244
+ })
245
+
246
+ test('culling regression: cut copper stairs run (west-facing, rotated)', () => {
247
+ const world = buildWorld(cutCopperStairsWestRunBlocks())
248
+ assertMesherParity(world, 116)
249
+ })
250
+
251
+ test('culling regression: cut copper stairs ascent', () => {
252
+ const world = buildWorld(cutCopperStairsAscentBlocks())
253
+ assertMesherParity(world, 106)
254
+ })
255
+
256
+ test('culling regression: 3 east + 3 west stairs mirror user scenario', () => {
257
+ const world = buildWorld(cutCopperStairsEastWestMirrorBlocks())
258
+ const legacy = countQuadsFromLegacy(world)
259
+ const wasmLegacy = countQuadsFromWasm(world, false)
260
+ const wasmShader = countQuadsFromWasm(world, true)
261
+ expect(legacy).toBe(wasmLegacy)
262
+ expect(legacy).toBe(wasmShader)
263
+ // 6 stairs with 2 internal interfaces culled per row; gap at x=3 prevents east↔west culling
264
+ expect(wasmShader).toBe(58)
265
+ expect(wasmShader).toBeLessThan(3 * 11 * 2)
266
+ })
267
+
268
+ test('culling regression: all stair facings and top half match east baseline', () => {
269
+ const east = countQuadsForFacing('east', 'bottom')
270
+ expect(countQuadsForFacing('west', 'bottom')).toBe(east)
271
+ expect(countQuadsForFacing('south', 'bottom')).toBe(east)
272
+ expect(countQuadsForFacing('north', 'bottom')).toBe(east)
273
+ expect(countQuadsForFacing('east', 'top')).toBe(east)
274
+ })
275
+
276
+ test('culling regression: glass/leaves cluster — see-through blocks not over-culled', () => {
277
+ const world = buildWorld(glassLeavesClusterBlocks())
278
+ // Leaves never occlude; glass self-culls. Baseline locks parity (pre shape-cull guard was 24 legacy-only).
279
+ assertMesherParity(world, 20)
280
+ })
281
+
282
+ test('shader cubes: dirt UP face culled under farmland', () => {
283
+ const world = buildWorld([
284
+ { x: 0, y: 0, z: 0, name: 'dirt' },
285
+ { x: 0, y: 1, z: 0, name: 'farmland' }
286
+ ])
287
+ assertMesherParity(world, 10)
288
+ })
289
+
290
+ test('shader cubes: dirt DOWN face culled under top slab', () => {
291
+ const world = buildWorld([
292
+ { x: 0, y: 0, z: 0, name: 'stone_slab', props: { type: 'top' } },
293
+ { x: 0, y: 1, z: 0, name: 'dirt' }
294
+ ])
295
+ assertMesherParity(world, 10)
296
+ })
297
+
298
+ test('shader cubes: dirt side not culled beside bottom slab', () => {
299
+ const world = buildWorld([
300
+ { x: 0, y: 0, z: 0, name: 'dirt' },
301
+ { x: 1, y: 0, z: 0, name: 'stone_slab', props: { type: 'bottom' } }
302
+ ])
303
+ const legacy = countQuadsFromLegacy(world)
304
+ const wasmShader = countQuadsFromWasm(world, true)
305
+ expect(wasmShader).toBe(legacy)
306
+ expect(wasmShader).toBeGreaterThan(5)
307
+ })
@@ -37,9 +37,9 @@ import { extractColumnHeightmap, WasmGeometryOutput } from '../bridge/render-fro
37
37
 
38
38
  const VERSION = '1.16.5'
39
39
 
40
- type BlockSpec = { x: number, y: number, z: number, name: string }
40
+ type BlockSpec = { x: number; y: number; z: number; name: string }
41
41
 
42
- function buildWorld(blocks: BlockSpec[]): { world: World, invisibleStateIds: Set<number> } {
42
+ function buildWorld(blocks: BlockSpec[]): { world: World; invisibleStateIds: Set<number> } {
43
43
  const mcData = MinecraftData(VERSION)
44
44
  const Chunk = Chunks(VERSION) as any
45
45
  const chunk = new Chunk(undefined as any)
@@ -74,12 +74,7 @@ function buildWorld(blocks: BlockSpec[]): { world: World, invisibleStateIds: Set
74
74
  * Returns a plain `number[]` matching the on-the-wire shape of Rust's
75
75
  * `Vec<i16>` (length 256, indexed `z*16+x`, sentinel `-32768`).
76
76
  */
77
- function simulateRustColumnHeightmap(
78
- world: World,
79
- invisibleStateIds: Set<number>,
80
- worldMinY: number,
81
- worldMaxY: number
82
- ): number[] {
77
+ function simulateRustColumnHeightmap(world: World, invisibleStateIds: Set<number>, worldMinY: number, worldMaxY: number): number[] {
83
78
  const heightmap = new Array<number>(256).fill(-32768)
84
79
  const column = world.getColumn(0, 0)
85
80
  if (!column) return heightmap
@@ -105,20 +100,15 @@ function makeWasmOutputWithHeightmap(heightmap: number[]): WasmGeometryOutput {
105
100
  blocks: [],
106
101
  block_count: 0,
107
102
  block_iterations: 0,
108
- heightmap,
103
+ heightmap
109
104
  }
110
105
  }
111
106
 
112
- function runParity(blocks: BlockSpec[]): { js: Int16Array, rust: Int16Array } {
107
+ function runParity(blocks: BlockSpec[]): { js: Int16Array; rust: Int16Array } {
113
108
  const { world, invisibleStateIds } = buildWorld(blocks)
114
109
  const js = computeHeightmap(world, 0, 0)
115
110
 
116
- const rustShaped = simulateRustColumnHeightmap(
117
- world,
118
- invisibleStateIds,
119
- world.config.worldMinY,
120
- world.config.worldMaxY
121
- )
111
+ const rustShaped = simulateRustColumnHeightmap(world, invisibleStateIds, world.config.worldMinY, world.config.worldMaxY)
122
112
  const wasmOutput = makeWasmOutputWithHeightmap(rustShaped)
123
113
  const rust = extractColumnHeightmap(wasmOutput)
124
114
  expect(rust).not.toBeNull()
@@ -156,7 +146,7 @@ test('heightmap parity: varied heights, every column populated (Rust == JS)', ()
156
146
  blocks.push({ x: 2, y: 80, z: 0, name: 'oak_log' }) // top wins over y=64 below
157
147
  blocks.push({ x: 2, y: 64, z: 0, name: 'stone' })
158
148
  blocks.push({ x: 5, y: 255, z: 5, name: 'stone' }) // high-Y edge (worldMaxY-1)
159
- blocks.push({ x: 6, y: 0, z: 6, name: 'stone' }) // low-Y edge (worldMinY)
149
+ blocks.push({ x: 6, y: 0, z: 6, name: 'stone' }) // low-Y edge (worldMinY)
160
150
  // Invisible block above a real surface must be skipped on both sides.
161
151
  blocks.push({ x: 7, y: 10, z: 7, name: 'stone' })
162
152
  blocks.push({ x: 7, y: 11, z: 7, name: 'barrier' })
@@ -172,7 +162,7 @@ test('heightmap parity: varied heights, every column populated (Rust == JS)', ()
172
162
  expect(js[0 * 16 + 1]).toBe(70)
173
163
  expect(js[0 * 16 + 2]).toBe(80)
174
164
  expect(js[5 * 16 + 5]).toBe(255)
175
- expect(js[6 * 16 + 6]).toBe(5) // y=0 stone is below the y=5 floor stone
165
+ expect(js[6 * 16 + 6]).toBe(5) // y=0 stone is below the y=5 floor stone
176
166
  expect(js[7 * 16 + 7]).toBe(10) // barrier above must be skipped
177
167
  expect(js[8 * 16 + 8]).toBe(12) // cave_air above must be skipped
178
168
  })
@@ -5,7 +5,7 @@ import {
5
5
  _peekConversionCache,
6
6
  clearConversionCache,
7
7
  getOrConvertColumn,
8
- invalidateConversion,
8
+ invalidateConversion
9
9
  } from '../worker/mesherWasmConversionCache'
10
10
  import type { ChunkConversionResult } from '../bridge/convertChunk'
11
11
 
@@ -19,7 +19,7 @@ const makeResult = (tag: number): ChunkConversionResult => ({
19
19
  noAoBlocks: new Uint16Array(0),
20
20
  cullIdenticalBlocks: new Uint16Array(0),
21
21
  occludingBlocks: new Uint16Array(0),
22
- blockCount: 0,
22
+ blockCount: 0
23
23
  })
24
24
 
25
25
  describe('mesherWasmConversionCache', () => {
@@ -5,7 +5,7 @@ import {
5
5
  raycastAabb,
6
6
  raycastShaderBlocksAabb,
7
7
  raycastSectionAabb,
8
- sectionAabbIntersectsRay,
8
+ sectionAabbIntersectsRay
9
9
  } from '../../three/sectionRaycastAabb'
10
10
  import { LEGACY_SECTION_HALF_EXTENT } from '../../three/legacySectionCull'
11
11
  import { SHADER_CUBES_WORDS_PER_FACE } from '../bridge/shaderCubeBridge'
@@ -85,9 +85,22 @@ test('raycastShaderBlocksAabb: eye inside solid block uses exit distance', () =>
85
85
  const oy = sectionCenterY - 8 + ly + 0.5
86
86
  const oz = sectionCenterZ - 8 + lz + 0.5
87
87
  const t = raycastShaderBlocksAabb(
88
- words, 0, 1, SHADER_CUBES_WORDS_PER_FACE,
89
- sectionCenterX, sectionCenterY, sectionCenterZ,
90
- ox, oy, oz, 0, 0, 1, 10, visitGen, visitStamp,
88
+ words,
89
+ 0,
90
+ 1,
91
+ SHADER_CUBES_WORDS_PER_FACE,
92
+ sectionCenterX,
93
+ sectionCenterY,
94
+ sectionCenterZ,
95
+ ox,
96
+ oy,
97
+ oz,
98
+ 0,
99
+ 0,
100
+ 1,
101
+ 10,
102
+ visitGen,
103
+ visitStamp
91
104
  )!
92
105
  expect(t).toBeGreaterThan(0)
93
106
  expect(t).toBeLessThanOrEqual(10)