minecraft-renderer 0.1.47 → 0.1.49

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 (34) hide show
  1. package/dist/mesher.js +1 -1
  2. package/dist/mesher.js.map +2 -2
  3. package/dist/mesherWasm.js +3740 -183
  4. package/dist/minecraft-renderer.js +332 -60
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +705 -433
  7. package/package.json +1 -1
  8. package/src/graphicsBackend/config.ts +4 -0
  9. package/src/graphicsBackend/playerState.ts +1 -0
  10. package/src/graphicsBackend/rendererOptionsSync.ts +1 -0
  11. package/src/lib/worldrendererCommon.ts +13 -0
  12. package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
  13. package/src/mesher-shared/shared.ts +8 -0
  14. package/src/playerState/playerState.ts +2 -0
  15. package/src/three/chunkMeshManager.ts +312 -39
  16. package/src/three/globalBlockBuffer.ts +292 -0
  17. package/src/three/menuBackground/config.ts +1 -1
  18. package/src/three/menuBackground/defaultOptions.ts +27 -19
  19. package/src/three/modules/sciFiWorldReveal.ts +162 -68
  20. package/src/three/modules/starfield.ts +9 -1
  21. package/src/three/sectionRaycastAabb.ts +167 -0
  22. package/src/three/shaderCubeMesh.ts +93 -0
  23. package/src/three/shaders/cubeBlockShader.ts +354 -0
  24. package/src/three/shaders/textureIndexMapping.ts +122 -0
  25. package/src/three/shaders/tintPalette.ts +198 -0
  26. package/src/three/worldGeometryExport.ts +53 -25
  27. package/src/three/worldRendererThree.ts +93 -26
  28. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  29. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +396 -0
  30. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  31. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
  32. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
  33. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
  34. package/src/wasm-mesher/worker/mesherWasm.ts +17 -2
@@ -0,0 +1,360 @@
1
+ //@ts-nocheck
2
+ import { test, expect, beforeEach } from 'vitest'
3
+ import { WORD0, WORD2, WORD3 } from '../../three/shaders/cubeBlockShader'
4
+ import {
5
+ resetShaderCubeResources,
6
+ getShaderCubeResources,
7
+ isShaderCubeBlock,
8
+ tryBuildShaderCubeInstances,
9
+ buildShaderCubesFromWords,
10
+ countVisibleFaces,
11
+ unpackTexIndexFromWord2,
12
+ decodeSectionBaseFromWords,
13
+ packWord3,
14
+ SHADER_CUBES_FORMAT_VERSION,
15
+ SHADER_CUBES_WORDS_PER_FACE,
16
+ } from '../bridge/shaderCubeBridge'
17
+ import { GlobalBlockBuffer } from '../../three/globalBlockBuffer'
18
+ import { createCubeBlockMaterial } from '../../three/shaders/cubeBlockShader'
19
+ import * as THREE from 'three'
20
+ import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
21
+
22
+ const VERSION = '1.16.5'
23
+ const STONE = 1
24
+ /** mc-assets blocksAtlases.json → stone */
25
+ const STONE_ATLAS_TILE_INDEX = 552
26
+
27
+ beforeEach(() => {
28
+ resetShaderCubeResources()
29
+ })
30
+
31
+ test('packWord2: AO diagonal flip sets bit 12', () => {
32
+ const words: number[] = []
33
+ const block = {
34
+ position: [3, 5, 7] as [number, number, number],
35
+ visible_faces: 1 << 0, // up only
36
+ ao_data: [[0, 1, 2, 3]], // 0+3 >= 1+2 → flip
37
+ light_data: [[1, 1, 1, 1]],
38
+ light_combined: [[255, 255, 255, 255]],
39
+ }
40
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
41
+ const model = {
42
+ elements: [{
43
+ faces: {
44
+ up: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
45
+ down: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
46
+ east: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
47
+ west: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
48
+ south: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
49
+ north: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
50
+ },
51
+ }],
52
+ }
53
+ const ok = tryBuildShaderCubeInstances(
54
+ block,
55
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
56
+ model,
57
+ {
58
+ sectionOrigin: { x: 0, y: 0, z: 0 },
59
+ sectionHeight: 16,
60
+ tintPalette,
61
+ textureIndexMapping,
62
+ },
63
+ words,
64
+ )
65
+ expect(ok).toBe(true)
66
+ expect(words.length).toBe(SHADER_CUBES_WORDS_PER_FACE)
67
+ expect(words[2]! & (1 << WORD2.DIAGONAL_FLAG_SHIFT)).not.toBe(0)
68
+ })
69
+
70
+ test('packWord0: section-local lx/ly/lz and face id', () => {
71
+ const words: number[] = []
72
+ const block = {
73
+ position: [10, 17, 4] as [number, number, number],
74
+ visible_faces: 1 << 2, // east
75
+ ao_data: [[3, 3, 3, 3]],
76
+ light_data: [[0.5, 0.5, 0.5, 0.5]],
77
+ light_combined: [[128, 128, 128, 128]],
78
+ }
79
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
80
+ const model = {
81
+ elements: [{
82
+ faces: {
83
+ up: { texture: { u: 16, v: 0, su: 16, sv: 16 } },
84
+ down: { texture: { u: 16, v: 0, su: 16, sv: 16 } },
85
+ east: { texture: { u: 16, v: 0, su: 16, sv: 16 } },
86
+ west: { texture: { u: 16, v: 0, su: 16, sv: 16 } },
87
+ south: { texture: { u: 16, v: 0, su: 16, sv: 16 } },
88
+ north: { texture: { u: 16, v: 0, su: 16, sv: 16 } },
89
+ },
90
+ }],
91
+ }
92
+ tryBuildShaderCubeInstances(
93
+ block,
94
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
95
+ model,
96
+ {
97
+ sectionOrigin: { x: 0, y: 16, z: 0 },
98
+ sectionHeight: 16,
99
+ tintPalette,
100
+ textureIndexMapping,
101
+ },
102
+ words,
103
+ )
104
+ const w0 = words[0]!
105
+ expect(w0 & 0xf).toBe(10) // lx
106
+ expect((w0 >> WORD0.LY_SHIFT) & 0xf).toBe(1) // ly = 17 - 16
107
+ expect((w0 >> WORD0.LZ_SHIFT) & 0xf).toBe(4)
108
+ expect((w0 >> WORD0.FACE_SHIFT) & 7).toBe(2) // east
109
+ })
110
+
111
+ test('isShaderCubeBlock: rejects model rotation and sectionHeight !== 16', () => {
112
+ const { textureIndexMapping } = getShaderCubeResources()
113
+ const baseModel = {
114
+ elements: [{
115
+ faces: {
116
+ up: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
117
+ down: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
118
+ east: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
119
+ west: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
120
+ south: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
121
+ north: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
122
+ },
123
+ }],
124
+ }
125
+ expect(isShaderCubeBlock(
126
+ { blockName: 'stone', blockProps: {}, isCube: true, model: baseModel },
127
+ baseModel,
128
+ 16,
129
+ textureIndexMapping,
130
+ )).toBe(true)
131
+ expect(isShaderCubeBlock(
132
+ { blockName: 'stone', blockProps: {}, isCube: true, model: baseModel },
133
+ baseModel,
134
+ 24,
135
+ textureIndexMapping,
136
+ )).toBe(false)
137
+ expect(isShaderCubeBlock(
138
+ { blockName: 'stone', blockProps: {}, isCube: true, model: { ...baseModel, y: 90 } },
139
+ { ...baseModel, y: 90 },
140
+ 16,
141
+ textureIndexMapping,
142
+ )).toBe(false)
143
+ })
144
+
145
+ test('renderWasmOutputToGeometry: stone emits shaderCubes and skips legacy vertices when enabled', () => {
146
+ const block = {
147
+ position: [0, 0, 0] as [number, number, number],
148
+ block_state_id: STONE,
149
+ visible_faces: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5),
150
+ ao_data: Array.from({ length: 6 }, () => [3, 3, 3, 3]),
151
+ light_data: Array.from({ length: 6 }, () => [1, 1, 1, 1]),
152
+ light_combined: Array.from({ length: 6 }, () => [255, 255, 255, 255]),
153
+ }
154
+ const out = renderWasmOutputToGeometry(
155
+ { blocks: [block], block_count: 1, block_iterations: 0 },
156
+ VERSION,
157
+ '0,0,0',
158
+ { x: 8, y: 8, z: 8 },
159
+ undefined,
160
+ { shaderCubes: true },
161
+ )
162
+ expect(out.shaderCubes?.count).toBe(6)
163
+ expect(out.shaderCubes?.formatVersion).toBe(SHADER_CUBES_FORMAT_VERSION)
164
+ expect(out.shaderCubes?.words.length).toBe(6 * SHADER_CUBES_WORDS_PER_FACE)
165
+ expect(out.geometry.positions.length).toBe(0)
166
+ expect(out.geometry.indices.length).toBe(0)
167
+ const words = out.shaderCubes!.words
168
+ for (let i = 0; i < words.length; i += SHADER_CUBES_WORDS_PER_FACE) {
169
+ expect(unpackTexIndexFromWord2(words[i + 2]!)).toBe(STONE_ATLAS_TILE_INDEX)
170
+ }
171
+ })
172
+
173
+ test('renderWasmOutputToGeometry: shaderCubes false keeps legacy path for stone', () => {
174
+ const block = {
175
+ position: [0, 0, 0] as [number, number, number],
176
+ block_state_id: STONE,
177
+ visible_faces: 1 << 0,
178
+ ao_data: [[3, 3, 3, 3]],
179
+ light_data: [[1, 1, 1, 1]],
180
+ }
181
+ const out = renderWasmOutputToGeometry(
182
+ { blocks: [block], block_count: 1, block_iterations: 0 },
183
+ VERSION,
184
+ '0,0,0',
185
+ { x: 8, y: 8, z: 8 },
186
+ undefined,
187
+ { shaderCubes: false },
188
+ )
189
+ expect(out.shaderCubes).toBeUndefined()
190
+ expect(out.geometry.positions.length).toBeGreaterThan(0)
191
+ })
192
+
193
+ test('buildShaderCubesFromWords: empty → undefined', () => {
194
+ expect(buildShaderCubesFromWords([])).toBeUndefined()
195
+ })
196
+
197
+ test('countVisibleFaces', () => {
198
+ expect(countVisibleFaces(0b101010)).toBe(3)
199
+ })
200
+
201
+ const SIX_FACE_TEXTURES = {
202
+ up: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
203
+ down: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
204
+ east: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
205
+ west: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
206
+ south: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
207
+ north: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
208
+ }
209
+
210
+ function aoCorner0FromWord0(w0: number): number {
211
+ return (w0 >> WORD0.AO_SHIFT) & 3
212
+ }
213
+
214
+ test('south face: AO corners remapped to shader order (elemFaces [0,3,1,2] → shader [2,3,0,1])', () => {
215
+ const words: number[] = []
216
+ const block = {
217
+ position: [0, 0, 0] as [number, number, number],
218
+ visible_faces: 1 << 4, // south
219
+ ao_data: [[0, 3, 1, 2]],
220
+ light_data: [[1, 1, 1, 1]],
221
+ light_combined: [[10, 20, 30, 40]],
222
+ }
223
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
224
+ const model = { elements: [{ faces: SIX_FACE_TEXTURES }] }
225
+ tryBuildShaderCubeInstances(
226
+ block,
227
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
228
+ model,
229
+ { sectionOrigin: { x: 0, y: 0, z: 0 }, sectionHeight: 16, tintPalette, textureIndexMapping },
230
+ words,
231
+ )
232
+ // Shader vi=0 must get elemFaces ao[2]=1, not ao[0]=0
233
+ expect(aoCorner0FromWord0(words[0]!)).toBe(1)
234
+ // Shader vi=1 → ao[3]=2
235
+ expect((words[0]! >> (WORD0.AO_SHIFT + WORD0.AO_BITS_PER_CORNER)) & 3).toBe(2)
236
+ })
237
+
238
+ test('south face: diagonal flip uses remapped AO (differs from raw elemFaces formula)', () => {
239
+ const wordsFlip: number[] = []
240
+ const wordsNoFlip: number[] = []
241
+ const ao = [0, 3, 1, 2] // raw: 0+2 < 3+1 → no flip; remapped [1,2,3,0]: 1+0 >= 2+3 → flip
242
+ const block = {
243
+ position: [0, 0, 0] as [number, number, number],
244
+ visible_faces: 1 << 4,
245
+ ao_data: [ao],
246
+ light_data: [[1, 1, 1, 1]],
247
+ light_combined: [[255, 255, 255, 255]],
248
+ }
249
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
250
+ const model = { elements: [{ faces: SIX_FACE_TEXTURES }] }
251
+ const opts = {
252
+ sectionOrigin: { x: 0, y: 0, z: 0 },
253
+ sectionHeight: 16,
254
+ tintPalette,
255
+ textureIndexMapping,
256
+ }
257
+ tryBuildShaderCubeInstances(
258
+ block,
259
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
260
+ model,
261
+ opts,
262
+ wordsFlip,
263
+ )
264
+ expect(wordsFlip[2]! & (1 << WORD2.DIAGONAL_FLAG_SHIFT)).not.toBe(0)
265
+
266
+ // elemFaces [3,0,0,3] → remapped [0,3,3,0]: 0+0 < 3+3 → no diagonal flip
267
+ tryBuildShaderCubeInstances(
268
+ { ...block, ao_data: [[3, 0, 0, 3]] },
269
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
270
+ model,
271
+ opts,
272
+ wordsNoFlip,
273
+ )
274
+ expect(wordsNoFlip[2]! & (1 << WORD2.DIAGONAL_FLAG_SHIFT)).toBe(0)
275
+ })
276
+
277
+ test('doAO false: full bright AO/light and no diagonal flip', () => {
278
+ const words: number[] = []
279
+ const block = {
280
+ position: [0, 0, 0] as [number, number, number],
281
+ visible_faces: 1 << 0,
282
+ ao_data: [[0, 0, 0, 0]],
283
+ light_data: [[0, 0, 0, 0]],
284
+ light_combined: [[0, 0, 0, 0]],
285
+ }
286
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
287
+ const model = { elements: [{ faces: SIX_FACE_TEXTURES }] }
288
+ tryBuildShaderCubeInstances(
289
+ block,
290
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
291
+ model,
292
+ {
293
+ sectionOrigin: { x: 0, y: 0, z: 0 },
294
+ sectionHeight: 16,
295
+ tintPalette,
296
+ textureIndexMapping,
297
+ doAO: false,
298
+ },
299
+ words,
300
+ )
301
+ expect(aoCorner0FromWord0(words[0]!)).toBe(3)
302
+ for (let i = 0; i < 4; i++) {
303
+ expect((words[1]! >> (i * 8)) & 0xff).toBe(255)
304
+ }
305
+ expect(words[2]! & (1 << WORD2.DIAGONAL_FLAG_SHIFT)).toBe(0)
306
+ })
307
+
308
+ test('section base coords round-trip in word2/word3', () => {
309
+ const words: number[] = []
310
+ const block = {
311
+ position: [10, 17, 4] as [number, number, number],
312
+ visible_faces: 1 << 2,
313
+ ao_data: [[3, 3, 3, 3]],
314
+ light_data: [[1, 1, 1, 1]],
315
+ light_combined: [[255, 255, 255, 255]],
316
+ }
317
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
318
+ const model = { elements: [{ faces: SIX_FACE_TEXTURES }] }
319
+ const sectionOrigin = { x: 0, y: 16, z: 32 }
320
+ tryBuildShaderCubeInstances(
321
+ block,
322
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
323
+ model,
324
+ { sectionOrigin, sectionHeight: 16, tintPalette, textureIndexMapping },
325
+ words,
326
+ )
327
+ const base = decodeSectionBaseFromWords(words[2]!, words[3]!)
328
+ expect(base).toEqual(sectionOrigin)
329
+ const sX = (words[3]! & 0xffff) - WORD3.SECTION_BIAS
330
+ const sZ = ((words[3]! >>> 16) & 0xffff) - WORD3.SECTION_BIAS
331
+ const sY = ((words[2]! >>> WORD2.SECTION_Y_SHIFT) & 0x1f) - 4
332
+ expect(sX * 16).toBe(sectionOrigin.x)
333
+ expect(sY * 16).toBe(sectionOrigin.y)
334
+ expect(sZ * 16).toBe(sectionOrigin.z)
335
+ })
336
+
337
+ test('GlobalBlockBuffer: free-list reuses slot with EMPTY sentinel', () => {
338
+ const scene = new THREE.Scene()
339
+ const mat = createCubeBlockMaterial()
340
+ const buffer = new GlobalBlockBuffer(mat, scene)
341
+
342
+ const words = new Uint32Array([
343
+ 1, 2, 0, packWord3(0, 0),
344
+ 3, 4, 0, packWord3(0, 0),
345
+ ])
346
+ buffer.addSection('a', words, 2)
347
+ expect(buffer.mesh.geometry.instanceCount).toBe(2)
348
+
349
+ buffer.removeSection('a')
350
+ const w2Attr = buffer.mesh.geometry.getAttribute('a_w2') as THREE.InstancedBufferAttribute
351
+ expect(w2Attr.array[0]! & (1 << WORD2.EMPTY_SHIFT)).not.toBe(0)
352
+ expect(w2Attr.array[1]! & (1 << WORD2.EMPTY_SHIFT)).not.toBe(0)
353
+
354
+ const wordsB = new Uint32Array([5, 6, 0, packWord3(16, 0)])
355
+ buffer.addSection('b', wordsB, 1)
356
+ expect(buffer.mesh.geometry.getAttribute('a_w0').array[0]).toBe(5)
357
+
358
+ buffer.dispose()
359
+ mat.dispose()
360
+ })
@@ -77,7 +77,10 @@ test('splitColumnWasmOutputToSections: per-section split is equivalent to manual
77
77
  { x: 0, y: 64, z: 0 }, // contains the isolated block at Y=64
78
78
  ]
79
79
 
80
- const split = splitColumnWasmOutputToSections(fullColumn, requested, { version: VERSION })
80
+ const split = splitColumnWasmOutputToSections(fullColumn, requested, {
81
+ version: VERSION,
82
+ shaderCubes: false,
83
+ })
81
84
 
82
85
  expect(split.size).toBe(4)
83
86
  for (const r of requested) {
@@ -100,7 +103,8 @@ test('splitColumnWasmOutputToSections: per-section split is equivalent to manual
100
103
  VERSION,
101
104
  `${r.x},${r.y},${r.z}`,
102
105
  { x: r.x + 8, y: r.y + 8, z: r.z + 8 },
103
- undefined
106
+ undefined,
107
+ { shaderCubes: false },
104
108
  )
105
109
  const got = split.get(`${r.x},${r.y},${r.z}`)!.exported
106
110
  expect(got.key).toBe(reference.key)
@@ -143,7 +147,10 @@ test('splitColumnWasmOutputToSections: per-section split is equivalent to manual
143
147
 
144
148
  test('splitColumnWasmOutputToSections: empty requested-keys list returns empty map', () => {
145
149
  const fullColumn = makeSeamFixture()
146
- const out = splitColumnWasmOutputToSections(fullColumn, [], { version: VERSION })
150
+ const out = splitColumnWasmOutputToSections(fullColumn, [], {
151
+ version: VERSION,
152
+ shaderCubes: false,
153
+ })
147
154
  expect(out.size).toBe(0)
148
155
  })
149
156
 
@@ -154,7 +161,7 @@ test('splitColumnWasmOutputToSections: blocks outside requested sections are dro
154
161
  const out = splitColumnWasmOutputToSections(
155
162
  fullColumn,
156
163
  [{ x: 0, y: 32, z: 0 }],
157
- { version: VERSION }
164
+ { version: VERSION, shaderCubes: false },
158
165
  )
159
166
  const empty = out.get('0,32,0')!
160
167
  expect(empty.exported.geometry.positions).toEqual([])
@@ -1224,7 +1224,12 @@ function processColumnTick() {
1224
1224
  exportedMap = splitColumnWasmOutputToSections(
1225
1225
  wasmResult,
1226
1226
  requestedSectionKeys,
1227
- { version, world, sectionHeight }
1227
+ {
1228
+ version,
1229
+ world,
1230
+ sectionHeight,
1231
+ shaderCubes: config?.shaderCubeBlocks === true,
1232
+ },
1228
1233
  )
1229
1234
 
1230
1235
  // Push heightmap from the WASM column output. With column meshing as
@@ -1301,7 +1306,9 @@ function processColumnTick() {
1301
1306
 
1302
1307
  let geometry: MesherGeometryOutput
1303
1308
  let transferable: any[] = []
1304
- if (exported && exported.geometry.indices.length > 0) {
1309
+ const hasLegacyMesh = (exported?.geometry.indices.length ?? 0) > 0
1310
+ const hasShaderCubes = (exported?.shaderCubes?.count ?? 0) > 0
1311
+ if (exported && (hasLegacyMesh || hasShaderCubes)) {
1305
1312
  const maxIndex = exported.geometry.indices.length > 0
1306
1313
  ? Math.max(...exported.geometry.indices)
1307
1314
  : 0
@@ -1339,6 +1346,13 @@ function processColumnTick() {
1339
1346
  // section's geometry.
1340
1347
  blocksCount: sectionBlocksCount,
1341
1348
  }
1349
+ if (exported.shaderCubes) {
1350
+ geometry.shaderCubes = {
1351
+ words: new Uint32Array(exported.shaderCubes.words),
1352
+ count: exported.shaderCubes.count,
1353
+ formatVersion: 2,
1354
+ }
1355
+ }
1342
1356
  transferable = [
1343
1357
  geometry.positions?.buffer,
1344
1358
  geometry.normals?.buffer,
@@ -1346,6 +1360,7 @@ function processColumnTick() {
1346
1360
  geometry.uvs?.buffer,
1347
1361
  //@ts-ignore
1348
1362
  geometry.indices?.buffer,
1363
+ geometry.shaderCubes?.words?.buffer,
1349
1364
  ].filter(Boolean)
1350
1365
 
1351
1366
  if (exported.geometry.indices.length > 0 && config.computeWireframeEdges) {