minecraft-renderer 0.1.94 → 0.1.96

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.
@@ -6,12 +6,12 @@
6
6
  */
7
7
 
8
8
  import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
9
+ import { SHADER_CUBES_FORMAT_VERSION, SHADER_CUBES_WORDS_PER_FACE } from '../../mesher-shared/shaderCubeFormat'
9
10
  import { WORD0, WORD1, WORD2, WORD3 } from '../../three/shaders/cubeBlockShader'
10
11
  import { TextureIndexMapping, type TextureEntry } from '../../three/shaders/textureIndexMapping'
11
12
  import { TintPalette } from '../../three/shaders/tintPalette'
12
13
 
13
- export const SHADER_CUBES_FORMAT_VERSION = 3 as const
14
- export const SHADER_CUBES_WORDS_PER_FACE = 4 as const
14
+ export { SHADER_CUBES_FORMAT_VERSION, SHADER_CUBES_WORDS_PER_FACE } from '../../mesher-shared/shaderCubeFormat'
15
15
 
16
16
  export type ShaderCubesOutput = {
17
17
  words: Uint32Array
@@ -83,32 +83,78 @@ let tintPalette: TintPalette | null = null
83
83
  let textureIndexMapping: TextureIndexMapping | null = null
84
84
  let tintsMissingWarned = false
85
85
 
86
- /** Convert mc-assets texture scales (normalized or negative) to pixel tile size for index lookup. */
87
- function normalizeTextureEntryForTileIndex(tex: { u?: number; v?: number; su?: number; sv?: number }, atlasWidth: number, tileSize: number): TextureEntry {
86
+ type FaceTextureRef = TextureEntry & { tileIndex?: number }
87
+
88
+ export type ResolvedFaceUv = { tileIndex: number; flipU: boolean; flipV: boolean }
89
+
90
+ const UV_RESOLVE_EPS = 1e-6
91
+
92
+ function isNearInteger(value: number): boolean {
93
+ return Math.abs(value - Math.round(value)) <= UV_RESOLVE_EPS
94
+ }
95
+
96
+ /**
97
+ * Resolve a face to (atlas tile + mirror flags).
98
+ * null ⇒ face does not fit exactly one aligned tile; block must use legacy path.
99
+ */
100
+ export function resolveFaceUv(tex: FaceTextureRef, texMapping: TextureIndexMapping): ResolvedFaceUv | null {
101
+ const tileSize = 16
88
102
  let u = tex.u ?? 0
89
103
  let v = tex.v ?? 0
90
104
  let su = tex.su ?? tileSize
91
105
  let sv = tex.sv ?? tileSize
92
- if (u > 0 && u <= 1) u = Math.round(u * atlasWidth)
93
- if (v > 0 && v <= 1) v = Math.round(v * atlasWidth)
94
- if (Math.abs(su) > 0 && Math.abs(su) <= 1) su = Math.round(Math.abs(su) * atlasWidth) || tileSize
95
- else if (su < 0) su = tileSize
96
- if (Math.abs(sv) > 0 && Math.abs(sv) <= 1) sv = Math.round(Math.abs(sv) * atlasWidth) || tileSize
97
- else if (sv < 0) sv = tileSize
98
- return { u, v, su, sv }
99
- }
100
106
 
101
- type FaceTextureRef = TextureEntry & { tileIndex?: number }
107
+ if (!Number.isFinite(u) || !Number.isFinite(v) || !Number.isFinite(su) || !Number.isFinite(sv)) {
108
+ return null
109
+ }
102
110
 
103
- /** Prefer atlas `tileIndex` from block model (legacy path uses the same). */
104
- export function resolveFaceTileIndex(tex: FaceTextureRef, texMapping: TextureIndexMapping): number {
105
- const fromAtlas = tex.tileIndex
106
- // tile 0 is a special/atlas-padding slot; use pixel fallback for block faces
107
- if (typeof fromAtlas === 'number' && fromAtlas > 0 && fromAtlas < 4096) {
108
- return fromAtlas
111
+ const atlasWidth = texMapping.getTilesPerRow() * tileSize
112
+ if (Math.abs(su) <= 1) {
113
+ u *= atlasWidth
114
+ v *= atlasWidth
115
+ su *= atlasWidth
116
+ sv *= atlasWidth
109
117
  }
110
- const entry = normalizeTextureEntryForTileIndex(tex, texMapping.getTilesPerRow() * 16, 16)
111
- return texMapping.tileIndexFromTextureEntry(entry)
118
+
119
+ if (!isNearInteger(u) || !isNearInteger(v) || !isNearInteger(su) || !isNearInteger(sv)) {
120
+ return null
121
+ }
122
+
123
+ const uPx = Math.round(u)
124
+ const vPx = Math.round(v)
125
+ const suPx = Math.round(su)
126
+ const svPx = Math.round(sv)
127
+
128
+ const flipU = suPx < 0
129
+ const flipV = svPx < 0
130
+
131
+ if (Math.abs(suPx) !== tileSize || Math.abs(svPx) !== tileSize) {
132
+ return null
133
+ }
134
+
135
+ const uMin = flipU ? uPx + suPx : uPx
136
+ const vMin = flipV ? vPx + svPx : vPx
137
+
138
+ if (uMin % tileSize !== 0 || vMin % tileSize !== 0) {
139
+ return null
140
+ }
141
+ if (uMin < 0 || vMin < 0) {
142
+ return null
143
+ }
144
+
145
+ const derived = texMapping.tileIndexFromPixelCoords(uMin, vMin)
146
+ if (derived < 0) {
147
+ return null
148
+ }
149
+
150
+ if (typeof tex.tileIndex === 'number') {
151
+ if (tex.tileIndex !== derived) {
152
+ return null
153
+ }
154
+ return { tileIndex: derived, flipU, flipV }
155
+ }
156
+
157
+ return { tileIndex: derived, flipU, flipV }
112
158
  }
113
159
 
114
160
  /** Main thread + worker: use `loadedData` set by the app / mesher (see mesherWasm). */
@@ -158,8 +204,8 @@ export function resetShaderCubeResources(): void {
158
204
 
159
205
  /**
160
206
  * Returns true when the block is a plain 1×1×1 cube that the instanced shader path
161
- * can render exactly like the legacy mesher (no model rotation, single un-rotated
162
- * element with all 6 cardinal faces present, atlas matches shader gate).
207
+ * can render with pixel-parity UV (no model rotation, single un-rotated element with
208
+ * all 6 cardinal faces present, each face exactly one aligned atlas tile).
163
209
  * Pass the already-resolved model variant (`modelVars[variantIndex][0]`).
164
210
  */
165
211
  export function isShaderCubeBlock(
@@ -191,7 +237,7 @@ export function isShaderCubeBlock(
191
237
  if ((eFace as { rotation?: number }).rotation) return false
192
238
  const tex = eFace.texture
193
239
  if (!tex) return false
194
- if (resolveFaceTileIndex(tex as FaceTextureRef, texMapping) < 0) {
240
+ if (resolveFaceUv(tex as FaceTextureRef, texMapping) === null) {
195
241
  return false
196
242
  }
197
243
  }
@@ -199,7 +245,7 @@ export function isShaderCubeBlock(
199
245
  return true
200
246
  }
201
247
 
202
- function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[]): number {
248
+ function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[], flipV: boolean): number {
203
249
  let w = 0
204
250
  w |= (lx & 0xf) << WORD0.LX_SHIFT
205
251
  w |= (ly & 0xf) << WORD0.LY_SHIFT
@@ -209,6 +255,9 @@ function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex
209
255
  for (let i = 0; i < WORD0.NUM_CORNERS; i++) {
210
256
  w |= ((ao[i] ?? 3) & 3) << (WORD0.AO_SHIFT + i * WORD0.AO_BITS_PER_CORNER)
211
257
  }
258
+ if (flipV) {
259
+ w |= 1 << WORD0.FLIP_V_SHIFT
260
+ }
212
261
  return w >>> 0
213
262
  }
214
263
 
@@ -224,7 +273,7 @@ function biasedSectionIndex(sectionBaseCoord: number): number {
224
273
  return (Math.floor(sectionBaseCoord / 16) + WORD3.SECTION_BIAS) & WORD3.SECTION_MASK
225
274
  }
226
275
 
227
- export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number): number {
276
+ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number, flipU = false): number {
228
277
  let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
229
278
  if (aoDiagonalFlip) {
230
279
  w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
@@ -235,6 +284,9 @@ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBase
235
284
  const sz = biasedSectionIndex(sectionBaseZ)
236
285
  w |= ((sx >>> 16) & 0x3f) << WORD2.SECTION_X_HI_SHIFT
237
286
  w |= ((sz >>> 16) & 0x3f) << WORD2.SECTION_Z_HI_SHIFT
287
+ if (flipU) {
288
+ w |= 1 << WORD2.FLIP_U_SHIFT
289
+ }
238
290
  return w >>> 0
239
291
  }
240
292
 
@@ -346,11 +398,12 @@ export function tryBuildShaderCubeInstances(
346
398
 
347
399
  const eFace = faces[faceName]
348
400
  const tex = eFace.texture! as FaceTextureRef
349
- const texIndex = resolveFaceTileIndex(tex, textureIndexMapping)
350
- if (texIndex < 0) {
401
+ const resolved = resolveFaceUv(tex, textureIndexMapping)
402
+ if (resolved === null) {
351
403
  words.length = wordsStart
352
404
  return false
353
405
  }
406
+ const { tileIndex: texIndex, flipU, flipV } = resolved
354
407
 
355
408
  const rawAo = block.ao_data[faceDataIndex] ?? [3, 3, 3, 3]
356
409
  const rawLight = lightCombinedForFace(block, faceDataIndex)
@@ -372,9 +425,9 @@ export function tryBuildShaderCubeInstances(
372
425
  const tintIndex = tintPalette.getTintIndex(eFace.tintindex, cached.blockName, cached.blockProps, biome ?? 'plains')
373
426
 
374
427
  words.push(
375
- packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
428
+ packWord0(lx, ly, lz, faceIdx, tintIndex, ao, flipV),
376
429
  packWord1(lightCombined),
377
- packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z),
430
+ packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z, flipU),
378
431
  packWord3(sectionOrigin.x, sectionOrigin.z)
379
432
  )
380
433
  }
@@ -0,0 +1,226 @@
1
+ //@ts-nocheck
2
+ import { test, expect, beforeEach, vi } from 'vitest'
3
+ import * as THREE from 'three'
4
+ import { elemFaces } from '../../mesher-shared/modelsGeometryCommon'
5
+ import { SHADER_CUBES_FORMAT_VERSION } from '../../mesher-shared/shaderCubeFormat'
6
+ import { FACE_UV_TERMS, buildFaceUvGlsl, shaderCubeFaceUv, cubeBlockVertexShader, WORD0, WORD2 } from '../../three/shaders/cubeBlockShader'
7
+ import {
8
+ AO_LIGHT_REMAP,
9
+ resetShaderCubeResources,
10
+ getShaderCubeResources,
11
+ resolveFaceUv,
12
+ isShaderCubeBlock,
13
+ tryBuildShaderCubeInstances,
14
+ SHADER_CUBES_WORDS_PER_FACE
15
+ } from '../bridge/shaderCubeBridge'
16
+ import { createMeshesFromExport } from '../../three/worldGeometryExport'
17
+
18
+ const FACE_NAMES = ['up', 'down', 'east', 'west', 'south', 'north'] as const
19
+ const SIGN_COMBOS: Array<[boolean, boolean]> = [
20
+ [false, false],
21
+ [true, false],
22
+ [false, true],
23
+ [true, true]
24
+ ]
25
+
26
+ function requireShaderCubeResources() {
27
+ const resources = getShaderCubeResources()
28
+ if (!resources) throw new Error('shader cube resources unavailable in test')
29
+ return resources
30
+ }
31
+
32
+ /** Legacy tile-local UV (render-from-wasm.ts elemFaces + down +180°). */
33
+ function legacyTileLocalUv(faceName: string, cornerIdx: number, suSign: number, svSign: number): [number, number] {
34
+ const [, , , cu, cv] = (elemFaces as Record<string, { corners: number[][] }>)[faceName]!.corners[cornerIdx]!
35
+ const r = faceName === 'down' ? 180 : 0
36
+ const uvcs = Math.cos((r * Math.PI) / 180)
37
+ const uvsn = -Math.sin((r * Math.PI) / 180)
38
+ const baseu = (cu - 0.5) * uvcs - (cv - 0.5) * uvsn + 0.5
39
+ const basev = (cu - 0.5) * uvsn + (cv - 0.5) * uvcs + 0.5
40
+ return [suSign < 0 ? 1 - baseu : baseu, svSign < 0 ? 1 - basev : basev]
41
+ }
42
+
43
+ beforeEach(() => {
44
+ resetShaderCubeResources()
45
+ })
46
+
47
+ test('shaderCubeFaceUv: 6 faces × 4 corners × 4 sign combos match legacy (96 cases)', () => {
48
+ for (let faceIdx = 0; faceIdx < 6; faceIdx++) {
49
+ const faceName = FACE_NAMES[faceIdx]!
50
+ for (let vi = 0; vi < 4; vi++) {
51
+ for (const [flipU, flipV] of SIGN_COMBOS) {
52
+ const suSign = flipU ? -1 : 1
53
+ const svSign = flipV ? -1 : 1
54
+ const elemCorner = AO_LIGHT_REMAP[faceIdx]![vi]!
55
+ const legacy = legacyTileLocalUv(faceName, elemCorner, suSign, svSign)
56
+ const shader = shaderCubeFaceUv(faceIdx, vi, flipU, flipV)
57
+ expect(shader[0]).toBeCloseTo(legacy[0], 9)
58
+ expect(shader[1]).toBeCloseTo(legacy[1], 9)
59
+ }
60
+ }
61
+ }
62
+ })
63
+
64
+ test('buildFaceUvGlsl: 6 branches match FACE_UV_TERMS and vertex shader embeds generated table', () => {
65
+ const glsl = buildFaceUvGlsl()
66
+ for (let i = 0; i < 6; i++) {
67
+ expect(glsl).toContain(`faceId == ${i}u`)
68
+ const [ut, vt] = FACE_UV_TERMS[i]!
69
+ const glslU = ut === 'u' ? 'u' : ut === 'v' ? 'v' : ut === '1-u' ? '1.0 - u' : '1.0 - v'
70
+ const glslV = vt === 'u' ? 'u' : vt === 'v' ? 'v' : vt === '1-u' ? '1.0 - u' : '1.0 - v'
71
+ expect(glsl).toContain(`vec2(${glslU}, ${glslV})`)
72
+ }
73
+ expect(cubeBlockVertexShader).toContain(glsl)
74
+ expect(cubeBlockVertexShader).toContain('a_w2 >> 31u')
75
+ expect(cubeBlockVertexShader).toContain('a_w0 >> 31u')
76
+ })
77
+
78
+ test('resolveFaceUv: crafting_table north/west regression', () => {
79
+ const { textureIndexMapping } = requireShaderCubeResources()
80
+
81
+ const north = resolveFaceUv({ u: 0.53125, v: 0, su: -0.015625, sv: 0.015625, tileIndex: 33 }, textureIndexMapping)
82
+ expect(north).toEqual({ tileIndex: 33, flipU: true, flipV: false })
83
+
84
+ const west = resolveFaceUv({ u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 }, textureIndexMapping)
85
+ expect(west).toEqual({ tileIndex: 33, flipU: false, flipV: false })
86
+ })
87
+
88
+ test('tryBuildShaderCubeInstances: crafting_table north sets FLIP_U_SHIFT', () => {
89
+ const { textureIndexMapping, tintPalette } = requireShaderCubeResources()
90
+ const craftingFaces = {
91
+ up: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
92
+ down: { texture: { u: 0.53125, v: 0, su: -0.015625, sv: 0.015625, tileIndex: 33 } },
93
+ east: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
94
+ west: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
95
+ south: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
96
+ north: { texture: { u: 0.53125, v: 0, su: -0.015625, sv: 0.015625, tileIndex: 33 } }
97
+ }
98
+ const model = { elements: [{ faces: craftingFaces }] }
99
+ const block = {
100
+ position: [0, 0, 0] as [number, number, number],
101
+ visible_faces: 1 << 5,
102
+ ao_data: [[3, 3, 3, 3]],
103
+ light_combined: [[255, 255, 255, 255]]
104
+ }
105
+ const words: number[] = []
106
+ const ok = tryBuildShaderCubeInstances(
107
+ block,
108
+ { blockName: 'crafting_table', blockProps: {}, isCube: true, model },
109
+ model,
110
+ { sectionOrigin: { x: 0, y: 0, z: 0 }, sectionHeight: 16, tintPalette, textureIndexMapping },
111
+ words
112
+ )
113
+ expect(ok).toBe(true)
114
+ expect(words.length).toBe(SHADER_CUBES_WORDS_PER_FACE)
115
+ expect(words[2]! & (1 << WORD2.FLIP_U_SHIFT)).not.toBe(0)
116
+ })
117
+
118
+ test('tryBuildShaderCubeInstances: sv<0 up face sets FLIP_V_SHIFT', () => {
119
+ const { textureIndexMapping, tintPalette } = requireShaderCubeResources()
120
+ const stdFace = { u: 0, v: 0, su: 16, sv: 16 }
121
+ const upSvNeg = { u: 0.515625, v: 0.015625, su: 0.015625, sv: -0.015625, tileIndex: 33 }
122
+ expect(resolveFaceUv(upSvNeg, textureIndexMapping)).toEqual({ tileIndex: 33, flipU: false, flipV: true })
123
+
124
+ const model = {
125
+ elements: [
126
+ {
127
+ faces: {
128
+ up: { texture: upSvNeg },
129
+ down: { texture: stdFace },
130
+ east: { texture: stdFace },
131
+ west: { texture: stdFace },
132
+ south: { texture: stdFace },
133
+ north: { texture: stdFace }
134
+ }
135
+ }
136
+ ]
137
+ }
138
+ const block = {
139
+ position: [0, 0, 0] as [number, number, number],
140
+ visible_faces: 1 << 0,
141
+ ao_data: [[3, 3, 3, 3]],
142
+ light_combined: [[255, 255, 255, 255]]
143
+ }
144
+ const words: number[] = []
145
+ const ok = tryBuildShaderCubeInstances(
146
+ block,
147
+ { blockName: 'observer', blockProps: { facing: 'north' }, isCube: true, model },
148
+ model,
149
+ { sectionOrigin: { x: 0, y: 0, z: 0 }, sectionHeight: 16, tintPalette, textureIndexMapping },
150
+ words
151
+ )
152
+ expect(ok).toBe(true)
153
+ expect(words.length).toBe(SHADER_CUBES_WORDS_PER_FACE)
154
+ expect(words[0]! & (1 << WORD0.FLIP_V_SHIFT)).not.toBe(0)
155
+ expect(words[2]! & (1 << WORD2.FLIP_U_SHIFT)).toBe(0)
156
+ })
157
+
158
+ test('resolveFaceUv gate: rejects crop, non-integer scale, misaligned tile, tileIndex mismatch, NaN', () => {
159
+ const { textureIndexMapping } = requireShaderCubeResources()
160
+
161
+ expect(resolveFaceUv({ u: 0, v: 0, su: 8, sv: 16 }, textureIndexMapping)).toBeNull()
162
+ expect(resolveFaceUv({ u: 0, v: 0, su: 15.6, sv: 16 }, textureIndexMapping)).toBeNull()
163
+ expect(resolveFaceUv({ u: 529, v: 0, su: 16, sv: 16 }, textureIndexMapping)).toBeNull()
164
+ expect(resolveFaceUv({ u: 0, v: 0, su: 16, sv: 16, tileIndex: 999 }, textureIndexMapping)).toBeNull()
165
+ expect(resolveFaceUv({ u: 0, v: 0, su: Number.NaN, sv: 16 }, textureIndexMapping)).toBeNull()
166
+ })
167
+
168
+ test('isShaderCubeBlock: rejects faces that fail resolveFaceUv gate', () => {
169
+ const { textureIndexMapping } = requireShaderCubeResources()
170
+ const baseFaces = {
171
+ up: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
172
+ down: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
173
+ east: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
174
+ west: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
175
+ south: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
176
+ north: { texture: { u: 0, v: 0, su: 16, sv: 16 } }
177
+ }
178
+ const model = { elements: [{ faces: baseFaces }] }
179
+ expect(isShaderCubeBlock({ blockName: 'stone', blockProps: {}, isCube: true, model }, model, 16, textureIndexMapping)).toBe(true)
180
+
181
+ const cropped = {
182
+ elements: [{ faces: { ...baseFaces, up: { texture: { u: 0, v: 0, su: 8, sv: 16 } } } }]
183
+ }
184
+ expect(isShaderCubeBlock({ blockName: 'stone', blockProps: {}, isCube: true, model: cropped }, cropped, 16, textureIndexMapping)).toBe(false)
185
+ })
186
+
187
+ test('createMeshesFromExport: skips shader-cube sections with old formatVersion', () => {
188
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
189
+ const material = new THREE.MeshBasicMaterial()
190
+ const v3Words = new Uint32Array([1, 2, 3, 4])
191
+ const v4Words = new Uint32Array([5, 6, 7, 8])
192
+
193
+ const groups = createMeshesFromExport(
194
+ {
195
+ version: 'test',
196
+ exportedAt: '',
197
+ camera: { position: { x: 0, y: 0, z: 0 }, rotation: { pitch: 0, yaw: 0 } },
198
+ sections: [
199
+ {
200
+ key: 'old',
201
+ position: { x: 0, y: 0, z: 0 },
202
+ geometry: { positions: [], normals: [], colors: [], skyLights: [], blockLights: [], uvs: [], indices: [] },
203
+ shaderCubes: { words: v3Words, count: 1, formatVersion: 3 as unknown as typeof SHADER_CUBES_FORMAT_VERSION }
204
+ },
205
+ {
206
+ key: 'new',
207
+ position: { x: 16, y: 0, z: 0 },
208
+ geometry: { positions: [], normals: [], colors: [], skyLights: [], blockLights: [], uvs: [], indices: [] },
209
+ shaderCubes: { words: v4Words, count: 1, formatVersion: SHADER_CUBES_FORMAT_VERSION }
210
+ }
211
+ ]
212
+ },
213
+ material,
214
+ new THREE.ShaderMaterial({ vertexShader: 'void main() {}', fragmentShader: 'void main() {}' })
215
+ )
216
+
217
+ expect(groups).toHaveLength(1)
218
+ expect(groups[0]!.children).toHaveLength(1)
219
+ expect(warn).toHaveBeenCalledOnce()
220
+ expect(warn.mock.calls[0]![0]).toContain('Skipped 1 shader-cube section')
221
+ expect(warn.mock.calls[0]![0]).toContain('3')
222
+ expect(warn.mock.calls[0]![0]).toContain(String(SHADER_CUBES_FORMAT_VERSION))
223
+
224
+ warn.mockRestore()
225
+ material.dispose()
226
+ })
@@ -4,6 +4,7 @@ import { convertChunkToWasm, getBlockMeta, type ChunkConversionResult } from '..
4
4
  import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../bridge/render-from-wasm'
5
5
  import { setBlockStatesData as setMesherData } from '../../mesher-shared/models'
6
6
  import { defaultMesherConfig, type MesherGeometryOutput, SECTION_HEIGHT } from '../../mesher-shared/shared'
7
+ import { SHADER_CUBES_FORMAT_VERSION } from '../../mesher-shared/shaderCubeFormat'
7
8
  import { packVisibilitySet, VISIBILITY_SET_ALL_TRUE } from '../../mesher-shared/visibilitySet'
8
9
  import { VisGraph } from '../../mesher-shared/visGraph'
9
10
  import { worldColumnKey, World } from '../../mesher-shared/world'
@@ -1532,7 +1533,7 @@ function processColumnTick() {
1532
1533
  geometry.shaderCubes = {
1533
1534
  words: new Uint32Array(exported.shaderCubes.words),
1534
1535
  count: exported.shaderCubes.count,
1535
- formatVersion: 3
1536
+ formatVersion: SHADER_CUBES_FORMAT_VERSION
1536
1537
  }
1537
1538
  }
1538
1539
  if (exported.blendGeometry && hasBlendMesh) {