minecraft-renderer 0.1.18 → 0.1.20

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.
@@ -0,0 +1,1000 @@
1
+ //@ts-nocheck
2
+ // Renderer that converts WASM mesher output to Three.js geometry
3
+ // This file takes WASM output and generates full Three.js buffer geometry
4
+
5
+ import * as THREE from 'three'
6
+ import worldBlockProviderModule, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
7
+ import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
8
+ import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
9
+ import MinecraftData from 'minecraft-data'
10
+ import PrismarineBlockLoader from 'prismarine-block'
11
+ import { Vec3 } from 'vec3'
12
+ import { elemFaces, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 } from '../mesher/modelsGeometryCommon'
13
+ import type { ExportedWorldGeometry, ExportedSection } from '../three/worldGeometryExport'
14
+ import type { MesherGeometryOutput } from '../mesher/shared'
15
+ import type { World } from '../mesher/world'
16
+
17
+ // Handle both default and named export
18
+ const worldBlockProvider = (worldBlockProviderModule as any).default || worldBlockProviderModule
19
+
20
+ // Initialize tints (same as in models.ts)
21
+ const tints: any = {}
22
+ let tintsInitialized = false
23
+
24
+ function initializeTints() {
25
+ if (tintsInitialized) return
26
+ let tintsData
27
+ try {
28
+ tintsData = require('esbuild-data').tints
29
+ } catch (err) {
30
+ tintsData = require('minecraft-data/minecraft-data/data/pc/1.16.2/tints.json')
31
+ }
32
+ for (const key of Object.keys(tintsData)) {
33
+ tints[key] = prepareTints(tintsData[key])
34
+ }
35
+ tintsInitialized = true
36
+ }
37
+
38
+ function prepareTints(tints: any) {
39
+ const map = new Map()
40
+ const defaultValue = tintToGl(tints.default)
41
+ for (let { keys, color } of tints.data) {
42
+ color = tintToGl(color)
43
+ for (const key of keys) {
44
+ map.set(`${key}`, color)
45
+ }
46
+ }
47
+ return new Proxy(map, {
48
+ get(target, key) {
49
+ return target.has(key) ? target.get(key) : defaultValue
50
+ }
51
+ })
52
+ }
53
+
54
+ function tintToGl(tint: number) {
55
+ const r = (tint >> 16) & 0xff
56
+ const g = (tint >> 8) & 0xff
57
+ const b = tint & 0xff
58
+ return [r / 255, g / 255, b / 255]
59
+ }
60
+
61
+ // Cached model definition with precomputed matrices
62
+ interface CachedBlockModel {
63
+ blockName: string
64
+ blockProps: Record<string, any>
65
+ models: any // BlockModelPartsResolved
66
+ isCube: boolean
67
+ // Precomputed per-model variant
68
+ modelVariants: Array<{
69
+ model: any
70
+ globalMatrix: any
71
+ globalShift: any
72
+ // Precomputed per-element
73
+ elements: Array<{
74
+ element: any
75
+ localMatrix: any
76
+ localShift: any
77
+ }>
78
+ }>
79
+ }
80
+
81
+ interface WasmBlockFaceData {
82
+ position: [number, number, number]
83
+ block_state_id: number
84
+ visible_faces: number
85
+ ao_data: number[][]
86
+ light_data: number[][]
87
+ }
88
+
89
+ interface WasmGeometryOutput {
90
+ blocks: WasmBlockFaceData[]
91
+ block_count: number
92
+ block_iterations: number
93
+ }
94
+
95
+ /**
96
+ * Get or create cached block model with precomputed matrices
97
+ */
98
+ function getCachedBlockModel(
99
+ blockStateId: number,
100
+ version: string,
101
+ blockProvider: WorldBlockProvider,
102
+ PrismarineBlock: any
103
+ ): CachedBlockModel | null {
104
+ // Use a module-level cache
105
+ const cacheKey = `${version}:${blockStateId}`
106
+ if (!(globalThis as any).__wasmBlockModelCache) {
107
+ (globalThis as any).__wasmBlockModelCache = new Map()
108
+ }
109
+ const cache = (globalThis as any).__wasmBlockModelCache
110
+
111
+ if (cache.has(cacheKey)) {
112
+ return cache.get(cacheKey)
113
+ }
114
+
115
+ try {
116
+ const blockObj = PrismarineBlock.fromStateId(blockStateId, 1)
117
+ const blockName = blockObj.name
118
+ const blockProps = blockObj.getProperties()
119
+
120
+ const models = blockProvider.getAllResolvedModels0_1(
121
+ { name: blockName, properties: blockProps },
122
+ false
123
+ )
124
+
125
+ if (!models || models.length === 0) return null
126
+
127
+ // Precompute matrices for all model variants
128
+ const modelVariants = models.map((modelVars) => {
129
+ return modelVars.map((model) => {
130
+ // Calculate global matrix and shift for model rotation
131
+ let globalMatrix = null as any
132
+ let globalShift = null as any
133
+ for (const axis of ['x', 'y', 'z'] as const) {
134
+ if (axis in model) {
135
+ globalMatrix = globalMatrix
136
+ ? matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0)))
137
+ : buildRotationMatrix(axis, -(model[axis] ?? 0))
138
+ }
139
+ }
140
+ if (globalMatrix) {
141
+ globalShift = [8, 8, 8]
142
+ globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
143
+ }
144
+
145
+ // Precompute element matrices
146
+ const elements = (model.elements ?? []).map((element: any) => {
147
+ let localMatrix = null as any
148
+ let localShift = null as any
149
+ if (element.rotation) {
150
+ localMatrix = buildRotationMatrix(
151
+ element.rotation.axis,
152
+ element.rotation.angle
153
+ )
154
+ localShift = vecsub3(
155
+ element.rotation.origin,
156
+ matmul3(localMatrix, element.rotation.origin)
157
+ )
158
+ }
159
+ return { element, localMatrix, localShift }
160
+ })
161
+
162
+ return { model, globalMatrix, globalShift, elements }
163
+ })
164
+ }).flat()
165
+
166
+ const isCube = (() => {
167
+ try {
168
+ if (!models?.length || models.length !== 1) return false
169
+ if (blockObj.transparent) return false
170
+ return models[0].every((v) => v.elements.every((e) => {
171
+ return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
172
+ }))
173
+ } catch {
174
+ return false
175
+ }
176
+ })()
177
+
178
+ const cached: CachedBlockModel = {
179
+ blockName,
180
+ blockProps,
181
+ models,
182
+ modelVariants,
183
+ isCube,
184
+ }
185
+
186
+ cache.set(cacheKey, cached)
187
+ return cached
188
+ } catch (err) {
189
+ console.warn(`Failed to get model for state ${blockStateId}:`, err)
190
+ return null
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Get tint for a face (matching TypeScript logic)
196
+ */
197
+ function getTint(
198
+ eFace: any,
199
+ blockName: string,
200
+ blockProps: Record<string, any>,
201
+ biome: string | undefined,
202
+ world: World | undefined
203
+ ): [number, number, number] {
204
+ if (eFace.tintindex === undefined) return [1, 1, 1]
205
+
206
+ if (eFace.tintindex === 0) {
207
+ if (blockName === 'redstone_wire') {
208
+ initializeTints()
209
+ return tints.redstone[`${blockProps.power}`] || [1, 1, 1]
210
+ } else if (
211
+ blockName === 'birch_leaves' ||
212
+ blockName === 'spruce_leaves' ||
213
+ blockName === 'lily_pad'
214
+ ) {
215
+ initializeTints()
216
+ return tints.constant[blockName] || [1, 1, 1]
217
+ } else if (blockName.includes('leaves') || blockName === 'vine') {
218
+ initializeTints()
219
+ return tints.foliage[biome || 'plains'] || [1, 1, 1]
220
+ } else {
221
+ initializeTints()
222
+ return tints.grass[biome || 'plains'] || [1, 1, 1]
223
+ }
224
+ }
225
+
226
+ return [1, 1, 1]
227
+ }
228
+
229
+ const ALWAYS_WATERLOGGED = new Set([
230
+ 'seagrass',
231
+ 'tall_seagrass',
232
+ 'kelp',
233
+ 'kelp_plant',
234
+ 'bubble_column'
235
+ ])
236
+
237
+ const isBlockWaterlogged = (block: any) => {
238
+ const props = block?.getProperties?.()
239
+ return props?.waterlogged === true || props?.waterlogged === 'true' || ALWAYS_WATERLOGGED.has(block?.name)
240
+ }
241
+
242
+ const getVec = (v: Vec3, dir: Vec3) => {
243
+ for (const coord of ['x', 'y', 'z'] as const) {
244
+ if (Math.abs((dir as any)[coord]) > 0) (v as any)[coord] = 0
245
+ }
246
+ return v.plus(dir)
247
+ }
248
+
249
+ const getLiquidRenderHeight = (world: World, block: any, type: number, pos: Vec3, isWater: boolean, isRealWater: boolean) => {
250
+ if ((isWater && !isRealWater) || (block && isBlockWaterlogged(block))) return 8 / 9
251
+ if (!block || block.type !== type) return 1 / 9
252
+ if (block.metadata === 0) {
253
+ const blockAbove = world.getBlock(pos.offset(0, 1, 0))
254
+ if (blockAbove && blockAbove.type === type) return 1
255
+ return 8 / 9
256
+ }
257
+ return ((block.metadata >= 8 ? 8 : 7 - block.metadata) + 1) / 9
258
+ }
259
+
260
+ const renderLiquidToGeometry = (
261
+ world: World,
262
+ cursor: Vec3,
263
+ texture: any,
264
+ type: number,
265
+ biome: string,
266
+ water: boolean,
267
+ isRealWater: boolean,
268
+ positions: number[],
269
+ normals: number[],
270
+ colors: number[],
271
+ uvs: number[],
272
+ indices: number[],
273
+ ) => {
274
+ const heights: number[] = []
275
+ for (let z = -1; z <= 1; z++) {
276
+ for (let x = -1; x <= 1; x++) {
277
+ const pos = cursor.offset(x, 0, z)
278
+ heights.push(getLiquidRenderHeight(world, world.getBlock(pos), type, pos, water, isRealWater))
279
+ }
280
+ }
281
+
282
+ const cornerHeights = [
283
+ Math.max(Math.max(heights[0], heights[1]), Math.max(heights[3], heights[4])),
284
+ Math.max(Math.max(heights[1], heights[2]), Math.max(heights[4], heights[5])),
285
+ Math.max(Math.max(heights[3], heights[4]), Math.max(heights[6], heights[7])),
286
+ Math.max(Math.max(heights[4], heights[5]), Math.max(heights[7], heights[8]))
287
+ ]
288
+
289
+ for (const face in elemFaces) {
290
+ const { dir, corners, mask1, mask2 } = (elemFaces as any)[face]
291
+ const isUp = dir[1] === 1
292
+
293
+ const neighborPos = cursor.offset(dir[0], dir[1], dir[2])
294
+ const neighbor = world.getBlock(neighborPos)
295
+ if (!neighbor) continue
296
+ if (neighbor.type === type || (water && (neighbor.name === 'water' || isBlockWaterlogged(neighbor)))) continue
297
+ if (neighbor.isCube && !neighbor.transparent && !isUp) continue
298
+
299
+ let tint: [number, number, number] = [1, 1, 1]
300
+ if (water) {
301
+ initializeTints()
302
+ let m = 1
303
+ if (Math.abs(dir[0]) > 0) m = 0.6
304
+ else if (Math.abs(dir[2]) > 0) m = 0.8
305
+ const wt = tints.water[biome] || [1, 1, 1]
306
+ tint = [wt[0] * m, wt[1] * m, wt[2] * m]
307
+ }
308
+
309
+ const u = texture.u || 0
310
+ const v = texture.v || 0
311
+ const su = texture.su || 1
312
+ const sv = texture.sv || 1
313
+
314
+ const baseLight = world.getLight(neighborPos, undefined, undefined, water ? 'water' : 'lava') / 15
315
+
316
+ const baseIndex = positions.length / 3
317
+
318
+ for (const pos of corners) {
319
+ const height = cornerHeights[pos[2] * 2 + pos[0]]
320
+ const OFFSET = 0.0001
321
+
322
+ positions.push(
323
+ (pos[0] ? 1 - OFFSET : OFFSET) + (cursor.x & 15) - 8,
324
+ (pos[1] ? height - OFFSET : OFFSET) + (cursor.y & 15) - 8,
325
+ (pos[2] ? 1 - OFFSET : OFFSET) + (cursor.z & 15) - 8
326
+ )
327
+
328
+ normals.push(dir[0], dir[1], dir[2])
329
+ uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
330
+
331
+ let cornerLightResult = baseLight
332
+ if (world.config.smoothLighting) {
333
+ const dx = pos[0] * 2 - 1
334
+ const dy = pos[1] * 2 - 1
335
+ const dz = pos[2] * 2 - 1
336
+ const cornerDir: [number, number, number] = [dx, dy, dz]
337
+ const side1Dir: [number, number, number] = [dx * mask1[0], dy * mask1[1], dz * mask1[2]]
338
+ const side2Dir: [number, number, number] = [dx * mask2[0], dy * mask2[1], dz * mask2[2]]
339
+
340
+ const dirVec = new Vec3(dir[0], dir[1], dir[2])
341
+
342
+ const side1LightDir = getVec(new Vec3(side1Dir[0], side1Dir[1], side1Dir[2]), dirVec)
343
+ const side1Light = world.getLight(cursor.plus(side1LightDir)) / 15
344
+ const side2DirLight = getVec(new Vec3(side2Dir[0], side2Dir[1], side2Dir[2]), dirVec)
345
+ const side2Light = world.getLight(cursor.plus(side2DirLight)) / 15
346
+ const cornerLightDir = getVec(new Vec3(cornerDir[0], cornerDir[1], cornerDir[2]), dirVec)
347
+ const cornerLight = world.getLight(cursor.plus(cornerLightDir)) / 15
348
+
349
+ cornerLightResult = (side1Light + side2Light + cornerLight + baseLight) / 4
350
+ }
351
+
352
+ colors.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult)
353
+ }
354
+
355
+ indices.push(
356
+ baseIndex,
357
+ baseIndex + 1,
358
+ baseIndex + 2,
359
+ baseIndex + 2,
360
+ baseIndex + 1,
361
+ baseIndex + 3,
362
+ baseIndex,
363
+ baseIndex + 2,
364
+ baseIndex + 1,
365
+ baseIndex + 2,
366
+ baseIndex + 3,
367
+ baseIndex + 1,
368
+ )
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Render WASM mesher output to Three.js geometry
374
+ */
375
+ export function renderWasmOutputToGeometry(
376
+ wasmOutput: WasmGeometryOutput,
377
+ version: string,
378
+ sectionKey: string,
379
+ sectionPosition: { x: number, y: number, z: number },
380
+ world?: World
381
+ ): ExportedSection {
382
+ const DEBUG = false
383
+ const log = (...args) => {
384
+ if (DEBUG) {
385
+ console.log(...args)
386
+ }
387
+ }
388
+
389
+ const mcData = MinecraftData(version)
390
+ const PrismarineBlock = PrismarineBlockLoader(version)
391
+
392
+ let blockProvider: WorldBlockProvider
393
+ if ((globalThis as any).blockProvider) {
394
+ blockProvider = (globalThis as any).blockProvider
395
+ } else if (typeof worldBlockProvider === 'function') {
396
+ blockProvider = worldBlockProvider(blockStatesModels, blocksAtlasesJson, version)
397
+ } else {
398
+ const wbp = require('mc-assets/dist/worldBlockProvider')
399
+ blockProvider = (wbp.default || wbp)(blockStatesModels, blocksAtlasesJson, version)
400
+ }
401
+
402
+ // Initialize tints if world is provided
403
+ if (world) {
404
+ initializeTints()
405
+ }
406
+
407
+ const positions: number[] = []
408
+ const normals: number[] = []
409
+ const colors: number[] = []
410
+ const uvs: number[] = []
411
+ const indices: number[] = []
412
+
413
+ const liquidQueue: Array<{
414
+ pos: Vec3,
415
+ type: number,
416
+ biome: string,
417
+ water: boolean,
418
+ isRealWater: boolean,
419
+ }> = []
420
+
421
+ let currentIndex = 0
422
+
423
+ for (const block of wasmOutput.blocks) {
424
+ const [bx, by, bz] = block.position
425
+ const blockStateId = block.block_state_id
426
+
427
+ log(`[WASM] Processing block at (${bx}, ${by}, ${bz}), stateId=${blockStateId}, visible_faces=0b${block.visible_faces.toString(2).padStart(6, '0')}`)
428
+
429
+ const prismBlock = PrismarineBlock.fromStateId(blockStateId, 1)
430
+
431
+ let biome: string | undefined
432
+ if (world) {
433
+ const blockObj = world.getBlock(new Vec3(bx, by, bz))
434
+ biome = blockObj?.biome?.name
435
+ }
436
+
437
+ if (world) {
438
+ const waterlogged = prismBlock.name !== 'water' && prismBlock.name !== 'lava' && isBlockWaterlogged(prismBlock)
439
+
440
+ if (prismBlock.name === 'water' || waterlogged) {
441
+ liquidQueue.push({
442
+ pos: new Vec3(bx, by, bz),
443
+ type: prismBlock.type,
444
+ biome: biome || 'plains',
445
+ water: true,
446
+ isRealWater: prismBlock.name === 'water' && !waterlogged,
447
+ })
448
+ }
449
+
450
+ if (prismBlock.name === 'lava') {
451
+ liquidQueue.push({
452
+ pos: new Vec3(bx, by, bz),
453
+ type: prismBlock.type,
454
+ biome: biome || 'plains',
455
+ water: false,
456
+ isRealWater: false,
457
+ })
458
+ }
459
+
460
+ if (prismBlock.name === 'water' || prismBlock.name === 'lava') {
461
+ continue
462
+ }
463
+ }
464
+
465
+ const cachedModel = getCachedBlockModel(blockStateId, version, blockProvider, PrismarineBlock)
466
+ if (!cachedModel) continue
467
+
468
+ if (false) {
469
+ // For now, use first model variant (can be extended later)
470
+ const modelVariant = cachedModel.modelVariants[0]
471
+ if (!modelVariant) continue
472
+
473
+ const { model, globalMatrix, globalShift, elements } = modelVariant
474
+
475
+ // Get biome for tint calculation if world is provided
476
+ let biome: string | undefined
477
+ if (world) {
478
+ const blockObj = world.getBlock(new Vec3(bx, by, bz))
479
+ biome = blockObj?.biome?.name
480
+ }
481
+
482
+ // Process faces in the same order as TypeScript (iterate through model's faces)
483
+ // TypeScript uses: for (const face in element.faces)
484
+ // We need to match this order to get the same vertex ordering
485
+
486
+ // Find the element that contains faces (use cached element data)
487
+ const faceElements = elements.filter(elemData => elemData.element.faces && Object.keys(elemData.element.faces).length > 0)
488
+
489
+ if (faceElements.length === 0) continue
490
+
491
+ // Map face names to their index in WASM output
492
+ const faceNameToIndex: Record<string, number> = {
493
+ 'up': 0,
494
+ 'down': 1,
495
+ 'east': 2,
496
+ 'west': 3,
497
+ 'south': 4,
498
+ 'north': 5
499
+ }
500
+
501
+ // WASM processes faces in fixed order: [up, down, east, west, south, north]
502
+ // Build a mapping from WASM face order to data index
503
+ const wasmFaceOrder = ['up', 'down', 'east', 'west', 'south', 'north']
504
+ const wasmFaceToDataIndex: Record<string, number> = {}
505
+ let dataIndex = 0
506
+ for (const faceName of wasmFaceOrder) {
507
+ const faceIdx = faceNameToIndex[faceName]
508
+ if ((block.visible_faces & (1 << faceIdx)) !== 0) {
509
+ wasmFaceToDataIndex[faceName] = dataIndex++
510
+ }
511
+ }
512
+
513
+ // Process faces in the order they appear in the model (matching TS)
514
+ for (const elemData of faceElements) {
515
+ const element = elemData.element
516
+ const localMatrix = elemData.localMatrix
517
+ const localShift = elemData.localShift
518
+
519
+ // eslint-disable-next-line guard-for-in
520
+ for (const faceName in element.faces) {
521
+ const faceIdx = faceNameToIndex[faceName]
522
+ if (faceIdx === undefined) continue
523
+
524
+ // Check if this face is visible in WASM output
525
+ if ((block.visible_faces & (1 << faceIdx)) === 0) {
526
+ continue
527
+ }
528
+
529
+ const matchingEFace = element.faces[faceName]
530
+ const { dir, corners, mask1, mask2 } = elemFaces[faceName]
531
+
532
+ // Get the correct data index for this face based on WASM's processing order
533
+ const faceDataIndex = wasmFaceToDataIndex[faceName]
534
+ if (faceDataIndex === undefined) continue
535
+
536
+ const aoValues = block.ao_data[faceDataIndex]
537
+ const lightValues = block.light_data[faceDataIndex]
538
+
539
+ log(`[WASM] Face ${faceIdx} (${faceName}): dir=[${dir.join(',')}], ao=[${aoValues.join(',')}], light=[${lightValues.map(l => l.toFixed(3)).join(',')}]`)
540
+
541
+ const texture = matchingEFace.texture as any
542
+ const u = texture.u || 0
543
+ const v = texture.v || 0
544
+ const su = texture.su || 1
545
+ const sv = texture.sv || 1
546
+
547
+ // UV rotation (matching reference implementation)
548
+ let r = matchingEFace.rotation || 0
549
+ if (faceName === 'down') {
550
+ r += 180
551
+ }
552
+ const uvcs = Math.cos(r * Math.PI / 180)
553
+ const uvsn = -Math.sin(r * Math.PI / 180)
554
+
555
+ // Get tint (use cached model data and world if available)
556
+ const tint = getTint(matchingEFace, cachedModel.blockName, cachedModel.blockProps, biome, world)
557
+
558
+ const minx = element.from[0]
559
+ const miny = element.from[1]
560
+ const minz = element.from[2]
561
+ const maxx = element.to[0]
562
+ const maxy = element.to[1]
563
+ const maxz = element.to[2]
564
+
565
+ // Calculate transformed direction
566
+ const transformedDir = matmul3(globalMatrix, dir)
567
+
568
+ // Add 4 vertices for this face
569
+ const baseIndex = currentIndex
570
+ for (let cornerIdx = 0; cornerIdx < 4; cornerIdx++) {
571
+ const pos = corners[cornerIdx]
572
+
573
+ // Calculate vertex position (matching reference)
574
+ let vertex = [
575
+ (pos[0] ? maxx : minx),
576
+ (pos[1] ? maxy : miny),
577
+ (pos[2] ? maxz : minz)
578
+ ]
579
+
580
+ // Apply element rotation
581
+ vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
582
+ // Apply model rotation
583
+ vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
584
+ // Convert to block coordinates (0-1)
585
+ vertex = vertex.map(v => v / 16)
586
+
587
+ // World position (relative to section)
588
+ const worldPos = [
589
+ vertex[0] + (bx & 15) - 8,
590
+ vertex[1] + (by & 15) - 8,
591
+ vertex[2] + (bz & 15) - 8
592
+ ]
593
+
594
+ log(`[WASM] Corner ${cornerIdx}: corner=[${pos.join(',')}], vertex=[${vertex.map(v => v.toFixed(3)).join(',')}], worldPos=[${worldPos.map(v => v.toFixed(3)).join(',')}]`)
595
+
596
+ positions.push(...worldPos)
597
+
598
+ // Normal (transformed direction)
599
+ normals.push(transformedDir[0], transformedDir[1], transformedDir[2])
600
+
601
+ // Color (with AO and light from WASM) - matching TS formula exactly
602
+ const ao = aoValues[cornerIdx]
603
+
604
+ // TS calculation:
605
+ // baseLight = world.getLight(neighborPos, ...) / 15 (0-1 range)
606
+ // cornerLightResult = baseLight * 15 (0-15 range, or interpolated if smooth lighting)
607
+ // light = (ao + 1) / 4 * (cornerLightResult / 15)
608
+ // finalColor = baseLight * tint * light
609
+
610
+ // WASM provides lightValues in 0-1 range (already divided by 15)
611
+ // But WASM light calculation seems to return 0.0, so we need to handle that
612
+ // In the test case, TypeScript gets baseLight = 1.0 (full brightness)
613
+ // So we should use 1.0 as the base light value when WASM returns 0
614
+ const baseLight = lightValues[cornerIdx]
615
+ const cornerLightResult = baseLight * 15
616
+
617
+ const light = (ao + 1) / 4 * (cornerLightResult / 15)
618
+
619
+ colors.push(tint[0] * light, tint[1] * light, tint[2] * light)
620
+
621
+ // UV calculation (matching reference exactly)
622
+ const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
623
+ const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
624
+ const finalU = baseu * su + u
625
+ const finalV = basev * sv + v
626
+ log(`[WASM] UV: cornerUV=[${pos[3]},${pos[4]}], baseUV=[${baseu.toFixed(6)},${basev.toFixed(6)}], finalUV=[${finalU.toFixed(6)},${finalV.toFixed(6)}], texture=[u=${u},v=${v},su=${su},sv=${sv}], rotation=${r}`)
627
+ uvs.push(finalU, finalV)
628
+
629
+ currentIndex++
630
+ }
631
+
632
+ // Add indices (2 triangles) - matching TS AO-optimized winding
633
+ // TS uses: if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { optimized } else { standard }
634
+ let tri1: number[], tri2: number[]
635
+ if (aoValues[0] + aoValues[3] >= aoValues[1] + aoValues[2]) {
636
+ // AO-optimized winding
637
+ tri1 = [baseIndex, baseIndex + 3, baseIndex + 2]
638
+ tri2 = [baseIndex, baseIndex + 1, baseIndex + 3]
639
+ log(`[WASM] Indices (AO optimized): tri1=[${tri1.join(',')}], tri2=[${tri2.join(',')}], aos=[${aoValues.join(',')}]`)
640
+ } else {
641
+ // Standard winding
642
+ tri1 = [baseIndex, baseIndex + 1, baseIndex + 2]
643
+ tri2 = [baseIndex + 2, baseIndex + 1, baseIndex + 3]
644
+ log(`[WASM] Indices (standard): tri1=[${tri1.join(',')}], tri2=[${tri2.join(',')}], aos=[${aoValues.join(',')}]`)
645
+ }
646
+ indices.push(...tri1, ...tri2)
647
+ }
648
+ }
649
+ }
650
+
651
+ const models = cachedModel.models
652
+ if (!models || models.length == 0) continue
653
+
654
+ const faceNameToIndex: Record<string, number> = {
655
+ 'up': 0,
656
+ 'down': 1,
657
+ 'east': 2,
658
+ 'west': 3,
659
+ 'south': 4,
660
+ 'north': 5
661
+ }
662
+
663
+ const dirKeyToIndex: Record<string, number> = {
664
+ '0,1,0': 0,
665
+ '0,-1,0': 1,
666
+ '1,0,0': 2,
667
+ '-1,0,0': 3,
668
+ '0,0,1': 4,
669
+ '0,0,-1': 5
670
+ }
671
+
672
+ const wasmFaceOrder = ['up', 'down', 'east', 'west', 'south', 'north']
673
+ const wasmFaceToDataIndex: Record<number, number> = {}
674
+ let dataIndex = 0
675
+ for (const faceName of wasmFaceOrder) {
676
+ const faceIdx = faceNameToIndex[faceName]
677
+ if ((block.visible_faces & (1 << faceIdx)) !== 0) {
678
+ wasmFaceToDataIndex[faceIdx] = dataIndex++
679
+ }
680
+ }
681
+
682
+ for (const modelVars of models ?? []) {
683
+ const model = modelVars[0]
684
+ if (!model) continue
685
+
686
+ let globalMatrix = null as any
687
+ let globalShift = null as any
688
+ for (const axis of ['x', 'y', 'z'] as const) {
689
+ if (axis in model) {
690
+ globalMatrix = globalMatrix
691
+ ? matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0)))
692
+ : buildRotationMatrix(axis, -(model[axis] ?? 0))
693
+ }
694
+ }
695
+ if (globalMatrix) {
696
+ globalShift = [8, 8, 8]
697
+ globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
698
+ }
699
+
700
+ for (const element of model.elements ?? []) {
701
+ let localMatrix = null as any
702
+ let localShift = null as any
703
+ if (element.rotation) {
704
+ localMatrix = buildRotationMatrix(
705
+ element.rotation.axis,
706
+ element.rotation.angle
707
+ )
708
+ localShift = vecsub3(
709
+ element.rotation.origin,
710
+ matmul3(localMatrix, element.rotation.origin)
711
+ )
712
+ }
713
+
714
+ // eslint-disable-next-line guard-for-in
715
+ for (const faceName in element.faces) {
716
+ const matchingEFace = element.faces[faceName]
717
+ const { dir, corners, mask1, mask2 } = elemFaces[faceName]
718
+
719
+ const transformedDir = matmul3(globalMatrix, dir)
720
+ const transformedDirI: [number, number, number] = [
721
+ Math.round(transformedDir[0]),
722
+ Math.round(transformedDir[1]),
723
+ Math.round(transformedDir[2]),
724
+ ]
725
+ const dirKey = `${transformedDirI[0]},${transformedDirI[1]},${transformedDirI[2]}`
726
+ const faceIdx = dirKeyToIndex[dirKey]
727
+ if (faceIdx === undefined) continue
728
+
729
+ const minx = element.from[0]
730
+ const miny = element.from[1]
731
+ const minz = element.from[2]
732
+ const maxx = element.to[0]
733
+ const maxy = element.to[1]
734
+ const maxz = element.to[2]
735
+
736
+ if (matchingEFace.cullface) {
737
+ if ((block.visible_faces & (1 << faceIdx)) === 0) {
738
+ continue
739
+ }
740
+ }
741
+
742
+ const faceDataIndex = wasmFaceToDataIndex[faceIdx]
743
+ const aoValuesRaw = faceDataIndex === undefined ? undefined : block.ao_data[faceDataIndex]
744
+ const lightValuesRaw = faceDataIndex === undefined ? undefined : block.light_data[faceDataIndex]
745
+
746
+ const texture = matchingEFace.texture as any
747
+ const u = texture.u || 0
748
+ const v = texture.v || 0
749
+ const su = texture.su || 1
750
+ const sv = texture.sv || 1
751
+
752
+ let r = matchingEFace.rotation || 0
753
+ if (faceName === 'down') {
754
+ r += 180
755
+ }
756
+ const uvcs = Math.cos(r * Math.PI / 180)
757
+ const uvsn = -Math.sin(r * Math.PI / 180)
758
+
759
+ const tint = getTint(matchingEFace, cachedModel.blockName, cachedModel.blockProps, biome, world)
760
+
761
+ const baseIndex = currentIndex
762
+ const computedAoValues = [3, 3, 3, 3]
763
+ for (let cornerIdx = 0; cornerIdx < 4; cornerIdx++) {
764
+ const pos = corners[cornerIdx]
765
+
766
+ let vertex = [
767
+ (pos[0] ? maxx : minx),
768
+ (pos[1] ? maxy : miny),
769
+ (pos[2] ? maxz : minz)
770
+ ]
771
+
772
+ vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
773
+ vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
774
+ vertex = vertex.map(v => v / 16)
775
+
776
+ const worldPos = [
777
+ vertex[0] + (bx & 15) - 8,
778
+ vertex[1] + (by & 15) - 8,
779
+ vertex[2] + (bz & 15) - 8
780
+ ]
781
+
782
+ positions.push(...worldPos)
783
+
784
+ normals.push(transformedDir[0], transformedDir[1], transformedDir[2])
785
+
786
+ const useModelLighting = !cachedModel.isCube && world
787
+
788
+ let ao = 3
789
+ let cornerLightResult = 15
790
+
791
+ if (useModelLighting) {
792
+ const cursor = new Vec3(bx, by, bz)
793
+
794
+ const dx = pos[0] * 2 - 1
795
+ const dy = pos[1] * 2 - 1
796
+ const dz = pos[2] * 2 - 1
797
+
798
+ const cornerDir = matmul3(globalMatrix, [dx, dy, dz])
799
+ const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]])
800
+ const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]])
801
+
802
+ const cornerDirI: [number, number, number] = [Math.round(cornerDir[0]), Math.round(cornerDir[1]), Math.round(cornerDir[2])]
803
+ const side1DirI: [number, number, number] = [Math.round(side1Dir[0]), Math.round(side1Dir[1]), Math.round(side1Dir[2])]
804
+ const side2DirI: [number, number, number] = [Math.round(side2Dir[0]), Math.round(side2Dir[1]), Math.round(side2Dir[2])]
805
+
806
+ const side1 = world.getBlock(cursor.offset(side1DirI[0], side1DirI[1], side1DirI[2]))
807
+ const side2 = world.getBlock(cursor.offset(side2DirI[0], side2DirI[1], side2DirI[2]))
808
+ const corner = world.getBlock(cursor.offset(cornerDirI[0], cornerDirI[1], cornerDirI[2]))
809
+
810
+ const side1Block = world.shouldMakeAo(side1) ? 1 : 0
811
+ const side2Block = world.shouldMakeAo(side2) ? 1 : 0
812
+ const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0
813
+
814
+ ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
815
+ computedAoValues[cornerIdx] = ao
816
+
817
+ const neighborPos = cursor.offset(transformedDirI[0], transformedDirI[1], transformedDirI[2])
818
+ const baseLight15 = world.getLight(neighborPos)
819
+
820
+ if (world.config.smoothLighting) {
821
+ const dirVec = new Vec3(transformedDirI[0], transformedDirI[1], transformedDirI[2])
822
+ const getVec = (v: Vec3) => {
823
+ for (const coord of ['x', 'y', 'z'] as const) {
824
+ if (Math.abs((dirVec as any)[coord]) > 0) (v as any)[coord] = 0
825
+ }
826
+ return v.plus(dirVec)
827
+ }
828
+
829
+ const side1LightDir = getVec(new Vec3(side1DirI[0], side1DirI[1], side1DirI[2]))
830
+ const side2LightDir = getVec(new Vec3(side2DirI[0], side2DirI[1], side2DirI[2]))
831
+ const cornerLightDir = getVec(new Vec3(cornerDirI[0], cornerDirI[1], cornerDirI[2]))
832
+
833
+ const side1Light = world.getLight(cursor.plus(side1LightDir))
834
+ const side2Light = world.getLight(cursor.plus(side2LightDir))
835
+ const cornerLight = world.getLight(cursor.plus(cornerLightDir))
836
+
837
+ cornerLightResult = (side1Light + side2Light + cornerLight + baseLight15) / 4
838
+ } else {
839
+ cornerLightResult = baseLight15
840
+ }
841
+ } else {
842
+ const aoValues = aoValuesRaw ?? [3, 3, 3, 3]
843
+ const lightValues = lightValuesRaw ?? [1, 1, 1, 1]
844
+
845
+ ao = aoValues[cornerIdx] ?? 3
846
+ computedAoValues[cornerIdx] = ao
847
+
848
+ const baseLight = lightValues[cornerIdx] ?? 1
849
+ cornerLightResult = baseLight * 15
850
+ }
851
+
852
+ const light = (ao + 1) / 4 * (cornerLightResult / 15)
853
+
854
+ colors.push(tint[0] * light, tint[1] * light, tint[2] * light)
855
+
856
+ const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
857
+ const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
858
+ const finalU = baseu * su + u
859
+ const finalV = basev * sv + v
860
+ uvs.push(finalU, finalV)
861
+
862
+ currentIndex++
863
+ }
864
+
865
+ const aoValues = computedAoValues
866
+
867
+ let tri1: number[], tri2: number[]
868
+ if (aoValues[0] + aoValues[3] >= aoValues[1] + aoValues[2]) {
869
+ tri1 = [baseIndex, baseIndex + 3, baseIndex + 2]
870
+ tri2 = [baseIndex, baseIndex + 1, baseIndex + 3]
871
+ } else {
872
+ tri1 = [baseIndex, baseIndex + 1, baseIndex + 2]
873
+ tri2 = [baseIndex + 2, baseIndex + 1, baseIndex + 3]
874
+ }
875
+ indices.push(...tri1, ...tri2)
876
+ }
877
+ }
878
+ }
879
+
880
+ }
881
+
882
+ if (world && liquidQueue.length) {
883
+ const waterTex = (blockProvider as any).getTextureInfo?.('water_still')
884
+ const lavaTex = (blockProvider as any).getTextureInfo?.('lava_still')
885
+
886
+ for (const q of liquidQueue) {
887
+ const tex = q.water ? waterTex : lavaTex
888
+ if (!tex) continue
889
+ renderLiquidToGeometry(
890
+ world,
891
+ q.pos,
892
+ tex,
893
+ q.type,
894
+ q.biome,
895
+ q.water,
896
+ q.isRealWater,
897
+ positions,
898
+ normals,
899
+ colors,
900
+ uvs,
901
+ indices,
902
+ )
903
+ }
904
+ }
905
+
906
+ const result = {
907
+ key: sectionKey,
908
+ position: sectionPosition,
909
+ geometry: {
910
+ positions,
911
+ normals,
912
+ colors,
913
+ uvs,
914
+ indices,
915
+ },
916
+ }
917
+
918
+ console.log(`[WASM] Final geometry summary:`)
919
+ console.log(`[WASM] Total vertices: ${positions.length / 3}`)
920
+ console.log(`[WASM] Total triangles: ${indices.length / 3}`)
921
+ console.log(`[WASM] Positions: [${positions.slice(0, 12).join(',')}...] (first 4 vertices)`)
922
+ console.log(`[WASM] Indices: [${indices.slice(0, 12).join(',')}...] (first 2 faces)`)
923
+
924
+ return result
925
+ }
926
+
927
+ /**
928
+ * Convert WASM output to exported geometry format
929
+ */
930
+ export function wasmOutputToExportFormat(
931
+ wasmOutput: WasmGeometryOutput,
932
+ version: string,
933
+ sectionKey: string,
934
+ sectionPosition: { x: number, y: number, z: number },
935
+ cameraPosition = { x: 0, y: 0, z: 0 },
936
+ cameraRotation = { pitch: 0, yaw: 0 },
937
+ world?: World
938
+ ): ExportedWorldGeometry {
939
+ const section = renderWasmOutputToGeometry(wasmOutput, version, sectionKey, sectionPosition, world)
940
+
941
+ return {
942
+ version,
943
+ exportedAt: new Date().toISOString(),
944
+ camera: {
945
+ position: cameraPosition,
946
+ rotation: cameraRotation,
947
+ },
948
+ sections: [section],
949
+ }
950
+ }
951
+
952
+ /**
953
+ * Convert mesher geometry output to exported geometry format
954
+ * Takes the output from getSectionGeometry() and converts it to ExportedWorldGeometry
955
+ */
956
+ export function mesherGeometryToExportFormat(
957
+ mesherGeometry: MesherGeometryOutput,
958
+ version: string,
959
+ cameraPosition = { x: 0, y: 0, z: 0 },
960
+ cameraRotation = { pitch: 0, yaw: 0 }
961
+ ): ExportedWorldGeometry {
962
+ // Convert typed arrays to regular number arrays
963
+ const positions = Array.from(mesherGeometry.positions) as number[]
964
+ const normals = mesherGeometry.normals ? (Array.from(mesherGeometry.normals) as number[]) : []
965
+ const colors = mesherGeometry.colors ? (Array.from(mesherGeometry.colors) as number[]) : []
966
+ const uvs = mesherGeometry.uvs ? (Array.from(mesherGeometry.uvs) as number[]) : []
967
+ const indices = Array.from(mesherGeometry.indices) as number[]
968
+
969
+ // Generate section key from chunk key and section coordinates, or use chunk key directly
970
+ const sectionKey = mesherGeometry.chunkKey || `${mesherGeometry.sx},${mesherGeometry.sy},${mesherGeometry.sz}`
971
+
972
+ // Use section coordinates for position
973
+ const sectionPosition = {
974
+ x: mesherGeometry.sx,
975
+ y: mesherGeometry.sy,
976
+ z: mesherGeometry.sz,
977
+ }
978
+
979
+ const section: ExportedSection = {
980
+ key: sectionKey,
981
+ position: sectionPosition,
982
+ geometry: {
983
+ positions,
984
+ normals,
985
+ colors,
986
+ uvs,
987
+ indices,
988
+ },
989
+ }
990
+
991
+ return {
992
+ version,
993
+ exportedAt: new Date().toISOString(),
994
+ camera: {
995
+ position: cameraPosition,
996
+ rotation: cameraRotation,
997
+ },
998
+ sections: [section],
999
+ }
1000
+ }