minecraft-renderer 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +3 -3
  2. package/dist/mesher.js +81 -81
  3. package/dist/mesher.js.map +3 -3
  4. package/dist/mesherWasm.js +1183 -943
  5. package/dist/minecraft-renderer.js +250 -79
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +1732 -1001
  8. package/package.json +3 -3
  9. package/src/graphicsBackend/rendererDefaultOptions.ts +5 -10
  10. package/src/graphicsBackend/rendererOptionsSync.ts +1 -1
  11. package/src/lib/bakeLegacyLight.ts +17 -0
  12. package/src/lib/blockEntityLightRegistry.test.ts +18 -0
  13. package/src/lib/blockEntityLightRegistry.ts +75 -0
  14. package/src/lib/blockEntityLighting.test.ts +30 -0
  15. package/src/lib/blockEntityLighting.ts +53 -0
  16. package/src/lib/worldrendererCommon.reconfigure.test.ts +202 -0
  17. package/src/lib/worldrendererCommon.ts +152 -22
  18. package/src/mesher-shared/blockEntityMetadata.test.ts +33 -0
  19. package/src/mesher-shared/blockEntityMetadata.ts +19 -3
  20. package/src/mesher-shared/exportedGeometryTypes.ts +11 -0
  21. package/src/mesher-shared/models.ts +161 -92
  22. package/src/mesher-shared/shared.ts +15 -4
  23. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +40 -0
  24. package/src/mesher-shared/world.ts +12 -0
  25. package/src/mesher-shared/worldLighting.test.ts +54 -0
  26. package/src/playground/baseScene.ts +1 -1
  27. package/src/three/bannerRenderer.ts +10 -3
  28. package/src/three/chunkMeshManager.ts +663 -69
  29. package/src/three/cubeDrawSpans.ts +74 -0
  30. package/src/three/cubeMultiDraw.ts +119 -0
  31. package/src/three/documentRenderer.ts +0 -2
  32. package/src/three/entities.ts +5 -6
  33. package/src/three/entity/EntityMesh.ts +7 -5
  34. package/src/three/entity/gltfAnimationUtils.ts +5 -3
  35. package/src/three/globalBlockBuffer.ts +208 -12
  36. package/src/three/globalLegacyBuffer.ts +701 -0
  37. package/src/three/graphicsBackendOffThread.ts +16 -1
  38. package/src/three/itemMesh.ts +5 -2
  39. package/src/three/legacySectionCull.ts +85 -0
  40. package/src/three/modules/sciFiWorldReveal.ts +347 -703
  41. package/src/three/modules/starfield.ts +3 -2
  42. package/src/three/sectionRaycastAabb.ts +25 -0
  43. package/src/three/shaders/cubeBlockShader.ts +80 -17
  44. package/src/three/shaders/legacyBlockShader.ts +292 -0
  45. package/src/three/skyboxRenderer.ts +1 -1
  46. package/src/three/tests/chunkMeshManagerLegacy.test.ts +286 -0
  47. package/src/three/tests/cubeDrawSpans.test.ts +73 -0
  48. package/src/three/tests/globalLegacyBuffer.test.ts +360 -0
  49. package/src/three/tests/legacySectionCull.test.ts +80 -0
  50. package/src/three/tests/signTextureCache.test.ts +83 -0
  51. package/src/three/threeJsMedia.ts +2 -2
  52. package/src/three/waypointSprite.ts +2 -2
  53. package/src/three/world/cursorBlock.ts +1 -0
  54. package/src/three/world/vr.ts +2 -2
  55. package/src/three/worldGeometryExport.ts +83 -26
  56. package/src/three/worldRendererThree.ts +94 -25
  57. package/src/wasm-mesher/bridge/render-from-wasm.ts +214 -72
  58. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +18 -6
  59. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  60. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +20 -0
  61. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +67 -5
  62. package/src/wasm-mesher/worker/mesherWasm.ts +70 -14
  63. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +11 -0
  64. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +15 -0
  65. package/src/worldView/worldView.ts +11 -0
@@ -5,7 +5,7 @@ import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
5
5
  import { BlockType } from '../playground/shared'
6
6
  import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock, worldColumnKey } from './world'
7
7
  import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
8
- import { getSideShading, vertexLightFromAo } from './vertexShading'
8
+ import { getSideShading } from './vertexShading'
9
9
  import { INVISIBLE_BLOCKS } from './worldConstants'
10
10
  import { MesherGeometryOutput, HighestBlockInfo } from './shared'
11
11
  import { collectBlockEntityMetadata } from './blockEntityMetadata'
@@ -23,6 +23,38 @@ const tints: any = {}
23
23
  let needTiles = false
24
24
  let semiTransparentBlocks: string[] = []
25
25
 
26
+ /** Mutable geometry bucket used while meshing (opaque or blend). */
27
+ export type MesherGeometryBucket = {
28
+ positions: number[]
29
+ normals: number[]
30
+ colors: number[]
31
+ skyLights: number[]
32
+ blockLights: number[]
33
+ uvs: number[]
34
+ indices: number[]
35
+ indicesCount: number
36
+ }
37
+
38
+ function vertexTintAoColor (
39
+ tint: [number, number, number],
40
+ ao: number,
41
+ faceDir: [number, number, number],
42
+ shadingTheme: 'vanilla' | 'high-contrast',
43
+ cardinalLight: string,
44
+ ): [number, number, number] {
45
+ const sideShading = getSideShading(faceDir, shadingTheme, cardinalLight)
46
+ if (shadingTheme === 'high-contrast') {
47
+ const f = sideShading * ((ao + 1) / 4)
48
+ return [tint[0] * f, tint[1] * f, tint[2] * f]
49
+ }
50
+ const f = sideShading * (ao * 0.2 + 0.4)
51
+ return [tint[0] * f, tint[1] * f, tint[2] * f]
52
+ }
53
+
54
+ export function isSemiTransparentBlockName (name: string): boolean {
55
+ return semiTransparentBlocks.includes(name)
56
+ }
57
+
26
58
  let tintsData
27
59
  try {
28
60
  tintsData = require('esbuild-data').tints
@@ -89,7 +121,7 @@ const getVec = (v: Vec3, dir: Vec3) => {
89
121
  return v.plus(dir)
90
122
  }
91
123
 
92
- function renderLiquid(world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: MesherGeometryOutput, isRealWater: boolean) {
124
+ function renderLiquid(world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, bucket: MesherGeometryBucket, attr: MesherGeometryOutput, isRealWater: boolean) {
93
125
  const heights: number[] = []
94
126
  for (let z = -1; z <= 1; z++) {
95
127
  for (let x = -1; x <= 1; x++) {
@@ -144,21 +176,23 @@ function renderLiquid(world: World, cursor: Vec3, texture: any | undefined, type
144
176
  const { su } = texture
145
177
  const { sv } = texture
146
178
 
147
- // Get base light value for the face
148
- const baseLight = world.getLight(neighborPos, undefined, undefined, water ? 'water' : 'lava') / 15
179
+ const baseChannels = world.getChannelLightNorm(neighborPos)
180
+
181
+ const baseIndex = bucket.positions.length / 3
149
182
 
150
183
  for (const pos of corners) {
151
184
  const height = cornerHeights[pos[2] * 2 + pos[0]]
152
185
  const OFFSET = 0.0001
153
- attr.t_positions!.push(
186
+ bucket.positions.push(
154
187
  (pos[0] ? 1 - OFFSET : OFFSET) + (cursor.x & 15) - 8,
155
188
  (pos[1] ? height - OFFSET : OFFSET) + (cursor.y & 15) - 8,
156
189
  (pos[2] ? 1 - OFFSET : OFFSET) + (cursor.z & 15) - 8
157
190
  )
158
- attr.t_normals!.push(...dir)
159
- attr.t_uvs!.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
191
+ bucket.normals.push(...dir)
192
+ bucket.uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
160
193
 
161
- let cornerLightResult = baseLight
194
+ let skyNorm = baseChannels.sky
195
+ let blockNorm = baseChannels.block
162
196
  if (world.config.smoothLighting) {
163
197
  const dx = pos[0] * 2 - 1
164
198
  const dy = pos[1] * 2 - 1
@@ -170,18 +204,47 @@ function renderLiquid(world: World, cursor: Vec3, texture: any | undefined, type
170
204
  const dirVec = new Vec3(...dir as [number, number, number])
171
205
 
172
206
  const side1LightDir = getVec(new Vec3(...side1Dir), dirVec)
173
- const side1Light = world.getLight(cursor.plus(side1LightDir)) / 15
174
207
  const side2DirLight = getVec(new Vec3(...side2Dir), dirVec)
175
- const side2Light = world.getLight(cursor.plus(side2DirLight)) / 15
176
208
  const cornerLightDir = getVec(new Vec3(...cornerDir), dirVec)
177
- const cornerLight = world.getLight(cursor.plus(cornerLightDir)) / 15
178
- // interpolate
179
- const lights = [side1Light, side2Light, cornerLight, baseLight]
180
- cornerLightResult = lights.reduce((acc, cur) => acc + cur, 0) / lights.length
209
+ const s1 = world.getChannelLightNorm(cursor.plus(side1LightDir))
210
+ const s2 = world.getChannelLightNorm(cursor.plus(side2DirLight))
211
+ const sc = world.getChannelLightNorm(cursor.plus(cornerLightDir))
212
+ blockNorm = (s1.block + s2.block + sc.block + baseChannels.block) / 4
213
+ skyNorm = (s1.sky + s2.sky + sc.sky + baseChannels.sky) / 4
181
214
  }
182
215
 
183
- // Apply light value to tint
184
- attr.t_colors!.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult)
216
+ bucket.colors.push(tint[0], tint[1], tint[2])
217
+ bucket.skyLights.push(skyNorm)
218
+ bucket.blockLights.push(blockNorm)
219
+ }
220
+
221
+ if (!needTiles) {
222
+ // Quad A (front face)
223
+ bucket.indices[bucket.indicesCount++] = baseIndex
224
+ bucket.indices[bucket.indicesCount++] = baseIndex + 1
225
+ bucket.indices[bucket.indicesCount++] = baseIndex + 2
226
+ bucket.indices[bucket.indicesCount++] = baseIndex + 2
227
+ bucket.indices[bucket.indicesCount++] = baseIndex + 1
228
+ bucket.indices[bucket.indicesCount++] = baseIndex + 3
229
+
230
+ // Quad B (back face) — duplicate verts so global buffer keeps 6/4 invariant
231
+ const dupBase = bucket.positions.length / 3
232
+ for (let v = 0; v < 4; v++) {
233
+ const src = (baseIndex + v) * 3
234
+ bucket.positions.push(bucket.positions[src]!, bucket.positions[src + 1]!, bucket.positions[src + 2]!)
235
+ bucket.normals.push(-dir[0], -dir[1], -dir[2])
236
+ const uvSrc = (baseIndex + v) * 2
237
+ bucket.uvs.push(bucket.uvs[uvSrc]!, bucket.uvs[uvSrc + 1]!)
238
+ bucket.colors.push(bucket.colors[src]!, bucket.colors[src + 1]!, bucket.colors[src + 2]!)
239
+ bucket.skyLights.push(bucket.skyLights[baseIndex + v]!)
240
+ bucket.blockLights.push(bucket.blockLights[baseIndex + v]!)
241
+ }
242
+ bucket.indices[bucket.indicesCount++] = dupBase
243
+ bucket.indices[bucket.indicesCount++] = dupBase + 2
244
+ bucket.indices[bucket.indicesCount++] = dupBase + 1
245
+ bucket.indices[bucket.indicesCount++] = dupBase + 1
246
+ bucket.indices[bucket.indicesCount++] = dupBase + 2
247
+ bucket.indices[bucket.indicesCount++] = dupBase + 3
185
248
  }
186
249
  }
187
250
  }
@@ -226,7 +289,7 @@ const identicalCull = (currentElement: BlockElement, neighbor: Block, direction:
226
289
 
227
290
  let needSectionRecomputeOnChange = false
228
291
 
229
- function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) {
292
+ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO: boolean, bucket: MesherGeometryBucket, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) {
230
293
  const position = cursor
231
294
  // const key = `${position.x},${position.y},${position.z}`
232
295
  // if (!globalThis.allowedBlocks.includes(key)) return
@@ -264,7 +327,7 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
264
327
  const { su } = texture
265
328
  const { sv } = texture
266
329
 
267
- const ndx = Math.floor(attr.positions.length / 3)
330
+ const ndx = Math.floor(bucket.positions.length / 3)
268
331
 
269
332
  tsLog(`[TS] Base index: ${ndx}`)
270
333
 
@@ -345,11 +408,9 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
345
408
 
346
409
  const aos: number[] = []
347
410
  const neighborPos = position.plus(new Vec3(...dir))
348
- // 10%
349
411
  const { smoothLighting, shadingTheme, cardinalLight } = world.config
350
- const faceLight = world.getLight(neighborPos, undefined, undefined, block.name)
351
- const sideShading = getSideShading(dir, shadingTheme, cardinalLight)
352
- const baseLight = sideShading * faceLight / 15
412
+ const baseChannels = world.getChannelLightNorm(neighborPos)
413
+ const faceDir = dir as [number, number, number]
353
414
  for (const pos of corners) {
354
415
  let vertex = [
355
416
  (pos[0] ? maxx : minx),
@@ -370,19 +431,21 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
370
431
 
371
432
  tsLog(`[TS] Corner ${pos.join(',')}: vertex=[${vertex.map(v => v.toFixed(3)).join(',')}], worldPos=[${worldPos.map(v => v.toFixed(3)).join(',')}]`)
372
433
 
373
- attr.positions.push(...worldPos)
434
+ bucket.positions.push(...worldPos)
374
435
 
375
- attr.normals.push(...dir)
436
+ bucket.normals.push(...dir)
376
437
 
377
438
  const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
378
439
  const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
379
440
  const finalU = baseu * su + u
380
441
  const finalV = basev * sv + v
381
442
  tsLog(`[TS] 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}`)
382
- attr.uvs.push(finalU, finalV)
443
+ bucket.uvs.push(finalU, finalV)
383
444
  }
384
445
 
385
- let light = 1
446
+ let skyLightNorm = baseChannels.sky
447
+ let blockLightNorm = baseChannels.block
448
+ let ao = 3
386
449
  if (doAO) {
387
450
  const dx = pos[0] * 2 - 1
388
451
  const dy = pos[1] * 2 - 1
@@ -394,8 +457,6 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
394
457
  const side2 = world.getBlock(cursor.offset(...side2Dir))
395
458
  const corner = world.getBlock(cursor.offset(...cornerDir))
396
459
 
397
- let cornerLightResult = faceLight
398
-
399
460
  if (smoothLighting) {
400
461
  const dirVec = new Vec3(...dir)
401
462
  const getVec = (v: Vec3) => {
@@ -405,37 +466,35 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
405
466
  return v.plus(dirVec)
406
467
  }
407
468
  const side1LightDir = getVec(new Vec3(...side1Dir))
408
- const side1Light = world.getLight(cursor.plus(side1LightDir))
409
469
  const side2DirLight = getVec(new Vec3(...side2Dir))
410
- const side2Light = world.getLight(cursor.plus(side2DirLight))
411
470
  const cornerLightDir = getVec(new Vec3(...cornerDir))
412
- const cornerLight = world.getLight(cursor.plus(cornerLightDir))
413
- // interpolate
414
- const lights = [side1Light, side2Light, cornerLight, faceLight]
415
- cornerLightResult = lights.reduce((acc, cur) => acc + cur, 0) / lights.length
471
+ const s1 = world.getChannelLightNorm(cursor.plus(side1LightDir))
472
+ const s2 = world.getChannelLightNorm(cursor.plus(side2DirLight))
473
+ const sc = world.getChannelLightNorm(cursor.plus(cornerLightDir))
474
+ blockLightNorm = (s1.block + s2.block + sc.block + baseChannels.block) / 4
475
+ skyLightNorm = (s1.sky + s2.sky + sc.sky + baseChannels.sky) / 4
416
476
  }
417
477
 
418
478
  const side1Block = world.shouldMakeAo(side1) ? 1 : 0
419
479
  const side2Block = world.shouldMakeAo(side2) ? 1 : 0
420
480
  const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0
421
481
 
422
- // TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block)
423
-
424
- const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
425
- light = vertexLightFromAo(ao, cornerLightResult, sideShading, shadingTheme)
482
+ ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
426
483
  aos.push(ao)
427
484
 
428
- // Log AO and light for this corner (corner index is aos.length - 1)
429
485
  const cornerIdx = aos.length - 1
430
- tsLog(`[TS] Corner ${cornerIdx} AO=${ao}, light=${light.toFixed(3)}`)
486
+ tsLog(`[TS] Corner ${cornerIdx} AO=${ao}, sky=${skyLightNorm.toFixed(3)}, block=${blockLightNorm.toFixed(3)}`)
431
487
  }
432
488
 
433
489
  if (!needTiles) {
434
- attr.colors.push(tint[0] * light, tint[1] * light, tint[2] * light)
490
+ const tintAo = vertexTintAoColor(tint as [number, number, number], ao, faceDir, shadingTheme, cardinalLight)
491
+ bucket.colors.push(tintAo[0], tintAo[1], tintAo[2])
492
+ bucket.skyLights.push(skyLightNorm)
493
+ bucket.blockLights.push(blockLightNorm)
435
494
  }
436
495
  }
437
496
 
438
- const lightWithColor = [baseLight * tint[0], baseLight * tint[1], baseLight * tint[2]] as [number, number, number]
497
+ const lightWithColor = [baseChannels.sky * tint[0], baseChannels.sky * tint[1], baseChannels.sky * tint[2]] as [number, number, number]
439
498
 
440
499
  if (needTiles) {
441
500
  const tiles = attr.tiles as Tiles
@@ -451,7 +510,7 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
451
510
  side,
452
511
  textureIndex: eFace.texture.tileIndex,
453
512
  neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
454
- light: baseLight,
513
+ light: Math.max(baseChannels.block, baseChannels.sky),
455
514
  tint: lightWithColor,
456
515
  //@ts-expect-error debug prop
457
516
  texture: eFace.texture.debugName || block.name,
@@ -465,22 +524,22 @@ function renderElement(world: World, cursor: Vec3, element: BlockElement, doAO:
465
524
  tri1 = [ndx, ndx + 3, ndx + 2]
466
525
  tri2 = [ndx, ndx + 1, ndx + 3]
467
526
  tsLog(`[TS] Indices (AO optimized): tri1=[${tri1.join(',')}], tri2=[${tri2.join(',')}], aos=[${aos.join(',')}]`)
468
- attr.indices[attr.indicesCount++] = tri1[0]
469
- attr.indices[attr.indicesCount++] = tri1[1]
470
- attr.indices[attr.indicesCount++] = tri1[2]
471
- attr.indices[attr.indicesCount++] = tri2[0]
472
- attr.indices[attr.indicesCount++] = tri2[1]
473
- attr.indices[attr.indicesCount++] = tri2[2]
527
+ bucket.indices[bucket.indicesCount++] = tri1[0]
528
+ bucket.indices[bucket.indicesCount++] = tri1[1]
529
+ bucket.indices[bucket.indicesCount++] = tri1[2]
530
+ bucket.indices[bucket.indicesCount++] = tri2[0]
531
+ bucket.indices[bucket.indicesCount++] = tri2[1]
532
+ bucket.indices[bucket.indicesCount++] = tri2[2]
474
533
  } else {
475
534
  tri1 = [ndx, ndx + 1, ndx + 2]
476
535
  tri2 = [ndx + 2, ndx + 1, ndx + 3]
477
536
  tsLog(`[TS] Indices (standard): tri1=[${tri1.join(',')}], tri2=[${tri2.join(',')}], aos=[${aos.join(',')}]`)
478
- attr.indices[attr.indicesCount++] = tri1[0]
479
- attr.indices[attr.indicesCount++] = tri1[1]
480
- attr.indices[attr.indicesCount++] = tri1[2]
481
- attr.indices[attr.indicesCount++] = tri2[0]
482
- attr.indices[attr.indicesCount++] = tri2[1]
483
- attr.indices[attr.indicesCount++] = tri2[2]
537
+ bucket.indices[bucket.indicesCount++] = tri1[0]
538
+ bucket.indices[bucket.indicesCount++] = tri1[1]
539
+ bucket.indices[bucket.indicesCount++] = tri1[2]
540
+ bucket.indices[bucket.indicesCount++] = tri2[0]
541
+ bucket.indices[bucket.indicesCount++] = tri2[1]
542
+ bucket.indices[bucket.indicesCount++] = tri2[2]
484
543
  }
485
544
  }
486
545
  }
@@ -516,11 +575,9 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
516
575
  positions: [],
517
576
  normals: [],
518
577
  colors: [],
578
+ skyLights: [],
579
+ blockLights: [],
519
580
  uvs: [],
520
- t_positions: [],
521
- t_normals: [],
522
- t_colors: [],
523
- t_uvs: [],
524
581
  indices: [],
525
582
  indicesCount: 0, // Track current index position
526
583
  using32Array: true,
@@ -534,13 +591,34 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
534
591
  blocksCount: 0
535
592
  }
536
593
 
594
+ const opaqueBucket: MesherGeometryBucket = {
595
+ positions: attr.positions as number[],
596
+ normals: attr.normals as number[],
597
+ colors: attr.colors as number[],
598
+ skyLights: attr.skyLights as number[],
599
+ blockLights: attr.blockLights as number[],
600
+ uvs: attr.uvs as number[],
601
+ indices: attr.indices as number[],
602
+ indicesCount: attr.indicesCount,
603
+ }
604
+ const blendBucket: MesherGeometryBucket = {
605
+ positions: [],
606
+ normals: [],
607
+ colors: [],
608
+ skyLights: [],
609
+ blockLights: [],
610
+ uvs: [],
611
+ indices: [],
612
+ indicesCount: 0,
613
+ }
614
+
537
615
  const cursor = new Vec3(0, 0, 0)
538
616
  for (cursor.y = sy; cursor.y < sy + readHeight; cursor.y++) {
539
617
  for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
540
618
  for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
541
619
  let block = world.getBlock(cursor, blockProvider, attr)!
542
620
  if (INVISIBLE_BLOCKS.has(block.name)) continue
543
- collectBlockEntityMetadata(block, cursor.x, cursor.y, cursor.z, attr, { disableBlockEntityTextures: world.config.disableBlockEntityTextures })
621
+ collectBlockEntityMetadata(block, cursor.x, cursor.y, cursor.z, attr, { disableBlockEntityTextures: world.config.disableBlockEntityTextures }, world)
544
622
  const biome = block.biome.name
545
623
 
546
624
  if (world.preflat) { // 10% perf
@@ -564,11 +642,11 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
564
642
  const pos = cursor.clone()
565
643
  // eslint-disable-next-line @typescript-eslint/no-loop-func
566
644
  delayedRender.push(() => {
567
- renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, !isWaterlogged)
645
+ renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, blendBucket, attr, !isWaterlogged)
568
646
  })
569
647
  attr.blocksCount++
570
648
  } else if (block.name === 'lava') {
571
- renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, false)
649
+ renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, blendBucket, attr, false)
572
650
  attr.blocksCount++
573
651
  }
574
652
  if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
@@ -608,14 +686,14 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
608
686
 
609
687
  for (const element of model.elements ?? []) {
610
688
  const ao = model.ao ?? block.boundingBox !== 'empty'
611
- if (block.transparent && semiTransparentBlocks.includes(block.name)) {
689
+ if (block.transparent && isSemiTransparentBlockName(block.name)) {
612
690
  const pos = cursor.clone()
613
691
  delayedRender.push(() => {
614
- renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome)
692
+ renderElement(world, pos, element, ao, blendBucket, attr, globalMatrix, globalShift, block, biome)
615
693
  })
616
694
  } else {
617
695
  // 60%
618
- renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome)
696
+ renderElement(world, cursor, element, ao, opaqueBucket, attr, globalMatrix, globalShift, block, biome)
619
697
  }
620
698
  }
621
699
  }
@@ -630,37 +708,13 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
630
708
  }
631
709
  delayedRender = []
632
710
 
633
- let ndx = attr.positions.length / 3
634
- for (let i = 0; i < attr.t_positions!.length / 12; i++) {
635
- attr.indices[attr.indicesCount++] = ndx
636
- attr.indices[attr.indicesCount++] = ndx + 1
637
- attr.indices[attr.indicesCount++] = ndx + 2
638
- attr.indices[attr.indicesCount++] = ndx + 2
639
- attr.indices[attr.indicesCount++] = ndx + 1
640
- attr.indices[attr.indicesCount++] = ndx + 3
641
- // back face
642
- attr.indices[attr.indicesCount++] = ndx
643
- attr.indices[attr.indicesCount++] = ndx + 2
644
- attr.indices[attr.indicesCount++] = ndx + 1
645
- attr.indices[attr.indicesCount++] = ndx + 2
646
- attr.indices[attr.indicesCount++] = ndx + 3
647
- attr.indices[attr.indicesCount++] = ndx + 1
648
- ndx += 4
649
- }
650
-
651
- attr.positions.push(...attr.t_positions!)
652
- attr.normals.push(...attr.t_normals!)
653
- attr.colors.push(...attr.t_colors!)
654
- attr.uvs.push(...attr.t_uvs!)
655
-
656
- delete attr.t_positions
657
- delete attr.t_normals
658
- delete attr.t_colors
659
- delete attr.t_uvs
711
+ attr.indicesCount = opaqueBucket.indicesCount
660
712
 
661
713
  attr.positions = new Float32Array(attr.positions) as any
662
714
  attr.normals = new Float32Array(attr.normals) as any
663
715
  attr.colors = new Float32Array(attr.colors) as any
716
+ attr.skyLights = new Float32Array(attr.skyLights) as any
717
+ attr.blockLights = new Float32Array(attr.blockLights) as any
664
718
  attr.uvs = new Float32Array(attr.uvs) as any
665
719
  attr.using32Array = arrayNeedsUint32(attr.indices)
666
720
  if (attr.using32Array) {
@@ -669,6 +723,21 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
669
723
  attr.indices = new Uint16Array(attr.indices)
670
724
  }
671
725
 
726
+ if (blendBucket.positions.length > 0) {
727
+ const blendUsing32 = arrayNeedsUint32(blendBucket.indices)
728
+ attr.blend = {
729
+ positions: new Float32Array(blendBucket.positions),
730
+ normals: new Float32Array(blendBucket.normals),
731
+ colors: new Float32Array(blendBucket.colors),
732
+ skyLights: new Float32Array(blendBucket.skyLights),
733
+ blockLights: new Float32Array(blendBucket.blockLights),
734
+ uvs: new Float32Array(blendBucket.uvs),
735
+ indices: blendUsing32
736
+ ? new Uint32Array(blendBucket.indices)
737
+ : new Uint16Array(blendBucket.indices),
738
+ }
739
+ }
740
+
672
741
  tsLog(`[TS] Final geometry summary:`)
673
742
  tsLog(`[TS] Total vertices: ${attr.positions.length / 3}`)
674
743
  tsLog(`[TS] Total triangles: ${attr.indices.length / 3}`)
@@ -30,6 +30,17 @@ export type CustomBlockModels = {
30
30
 
31
31
  export type MesherConfig = typeof defaultMesherConfig
32
32
 
33
+ /** Vertex/index arrays for one opaque or blend geometry bucket. */
34
+ export type MesherGeometryBucketData = {
35
+ positions: Float32Array
36
+ normals: Float32Array
37
+ colors: Float32Array
38
+ skyLights: Float32Array
39
+ blockLights: Float32Array
40
+ uvs: Float32Array
41
+ indices: Uint32Array | Uint16Array
42
+ }
43
+
33
44
  export type MesherGeometryOutput = {
34
45
  sectionYNumber: number,
35
46
  chunkKey: string,
@@ -47,11 +58,11 @@ export type MesherGeometryOutput = {
47
58
  positions: any,
48
59
  normals: any,
49
60
  colors: any,
61
+ skyLights: any,
62
+ blockLights: any,
50
63
  uvs: any,
51
- t_positions?: number[],
52
- t_normals?: number[],
53
- t_colors?: number[],
54
- t_uvs?: number[],
64
+ /** Per-section blend geometry (water, lava, stained glass, ice, etc.). */
65
+ blend?: MesherGeometryBucketData,
55
66
 
56
67
  indices: Uint32Array | Uint16Array | number[],
57
68
  indicesCount: number,
@@ -0,0 +1,40 @@
1
+ //@ts-nocheck
2
+ import { test, expect } from 'vitest'
3
+ import { setup } from '../../mesher-legacy/test/mesherTester'
4
+
5
+ test('renderLiquid blend output satisfies 6/4 quad invariant with both windings', () => {
6
+ const { getGeometry } = setup('1.16.5', [
7
+ [[0, 0, 0], 'water'],
8
+ [[0, -1, 0], 'stone'],
9
+ [[1, 0, 0], 'stone'],
10
+ [[-1, 0, 0], 'stone'],
11
+ [[0, 0, 1], 'stone'],
12
+ [[0, 0, -1], 'stone'],
13
+ ], { noDebugTiles: true })
14
+
15
+ const { attr } = getGeometry()
16
+ const blend = attr.blend
17
+ expect(blend).toBeDefined()
18
+ expect(blend!.positions.length).toBeGreaterThan(0)
19
+
20
+ const quadCount = blend!.positions.length / 3 / 4
21
+ expect(blend!.indices.length / 6).toBe(quadCount)
22
+
23
+ for (let i = 0; i < blend!.indices.length; i += 12) {
24
+ const b = blend!.indices[i]!
25
+ const d = blend!.indices[i + 6]!
26
+ expect(blend!.indices[i]).toBe(b)
27
+ expect(blend!.indices[i + 1]).toBe(b + 1)
28
+ expect(blend!.indices[i + 2]).toBe(b + 2)
29
+ expect(blend!.indices[i + 3]).toBe(b + 2)
30
+ expect(blend!.indices[i + 4]).toBe(b + 1)
31
+ expect(blend!.indices[i + 5]).toBe(b + 3)
32
+ expect(blend!.indices[i + 6]).toBe(d)
33
+ expect(blend!.indices[i + 7]).toBe(d + 2)
34
+ expect(blend!.indices[i + 8]).toBe(d + 1)
35
+ expect(blend!.indices[i + 9]).toBe(d + 1)
36
+ expect(blend!.indices[i + 10]).toBe(d + 2)
37
+ expect(blend!.indices[i + 11]).toBe(d + 3)
38
+ expect(d).toBe(b + 4)
39
+ }
40
+ })
@@ -76,6 +76,18 @@ export class World {
76
76
  this.config.version = version
77
77
  }
78
78
 
79
+ getChannelLightNorm (pos: Vec3): { block: number, sky: number } {
80
+ if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
81
+ if (!this.config.enableLighting) return { block: 0, sky: 1 }
82
+ const column = this.getColumnByPos(pos)
83
+ if (!column || !hasChunkSection(column, pos)) return { block: 0, sky: 1 }
84
+ const loc = posInChunk(pos)
85
+ return {
86
+ block: Math.min(15, column.getBlockLight(loc) + 2) / 15,
87
+ sky: Math.min(15, column.getSkyLight(loc) + 2) / 15,
88
+ }
89
+ }
90
+
79
91
  getLight(pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
80
92
  // for easier testing
81
93
  if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
@@ -0,0 +1,54 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, it } from 'vitest'
3
+ import { Vec3 } from 'vec3'
4
+ import { World } from './world'
5
+
6
+ function mockColumn (blockLight: number, skyLight: number, withSection = true) {
7
+ return {
8
+ getBlockLight: () => blockLight,
9
+ getSkyLight: () => skyLight,
10
+ }
11
+ }
12
+
13
+ describe('World.getChannelLightNorm', () => {
14
+ it('returns full-bright when enableLighting is false', () => {
15
+ const world = new World('1.16.5')
16
+ world.config.enableLighting = false
17
+ expect(world.getChannelLightNorm(new Vec3(0, 64, 0))).toEqual({ block: 0, sky: 1 })
18
+ })
19
+
20
+ it('returns full-bright when column is missing', () => {
21
+ const world = new World('1.16.5')
22
+ world.config.enableLighting = true
23
+ expect(world.getChannelLightNorm(new Vec3(0, 64, 0))).toEqual({ block: 0, sky: 1 })
24
+ })
25
+
26
+ it('returns full-bright when chunk section is missing', () => {
27
+ const world = new World('1.16.5')
28
+ world.config.enableLighting = true
29
+ world.columns['0,0'] = mockColumn(0, 0, false) as any
30
+ expect(world.getChannelLightNorm(new Vec3(8, 64, 8))).toEqual({ block: 0, sky: 1 })
31
+ })
32
+
33
+ it('applies +2 brightness floor per channel in 0-15 space', () => {
34
+ const world = new World('1.16.5')
35
+ world.config.enableLighting = true
36
+ const column = mockColumn(0, 0)
37
+ ;(column as any).sections = { 4: {} }
38
+ world.columns['0,0'] = column as any
39
+ const result = world.getChannelLightNorm(new Vec3(8, 64, 8))
40
+ expect(result.block).toBeCloseTo(2 / 15, 5)
41
+ expect(result.sky).toBeCloseTo(2 / 15, 5)
42
+ })
43
+
44
+ it('clamps brightened channels at 15', () => {
45
+ const world = new World('1.16.5')
46
+ world.config.enableLighting = true
47
+ const column = mockColumn(14, 14)
48
+ ;(column as any).sections = { 4: {} }
49
+ world.columns['0,0'] = column as any
50
+ const result = world.getChannelLightNorm(new Vec3(8, 64, 8))
51
+ expect(result.block).toBe(1)
52
+ expect(result.sky).toBe(1)
53
+ })
54
+ })
@@ -21,7 +21,7 @@ import { createGraphicsBackendOffThread } from '../three/graphicsBackendOffThrea
21
21
  import { WorldRendererThree } from '../three/worldRendererThree'
22
22
  import createGraphicsBackendSingleThread from '../three/graphicsBackendSingleThread'
23
23
 
24
- window.THREE = THREE
24
+ globalThis.THREE = THREE
25
25
 
26
26
  // Scene configuration interface
27
27
  export interface PlaygroundSceneConfig {
@@ -2,6 +2,7 @@
2
2
  import * as THREE from 'three'
3
3
  import { Vec3 } from 'vec3'
4
4
  import { createCanvas } from '../lib/utils'
5
+ import { tintBannerMaterial } from '../lib/blockEntityLightRegistry'
5
6
  import type { WorldRendererThree } from './worldRendererThree'
6
7
 
7
8
  type BannerBlockEntity = {
@@ -228,8 +229,11 @@ export function createBannerMesh(
228
229
  position: Vec3,
229
230
  rotation: number,
230
231
  isWall: boolean,
231
- texture: THREE.Texture
232
- ): THREE.Group & { bannerTexture?: THREE.Texture } {
232
+ texture: THREE.Texture,
233
+ blockLightNorm = 0,
234
+ skyLightNorm = 1,
235
+ skyLevel = 1,
236
+ ): THREE.Group & { bannerTexture?: THREE.Texture, bannerMaterial?: THREE.MeshBasicMaterial } {
233
237
  const bannerWidth = 13.6 / 16
234
238
  const bannerHeight = 28 / 16
235
239
  const clothXOffset = 0
@@ -250,9 +254,11 @@ export function createBannerMesh(
250
254
  heightOffset = 0
251
255
  }
252
256
 
257
+ const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true })
258
+ tintBannerMaterial(material, blockLightNorm, skyLightNorm, skyLevel)
253
259
  const mesh = new THREE.Mesh(
254
260
  new THREE.PlaneGeometry(bannerWidth, bannerHeight),
255
- new THREE.MeshBasicMaterial({ map: texture, transparent: true })
261
+ material,
256
262
  )
257
263
  mesh.renderOrder = 999
258
264
 
@@ -272,6 +278,7 @@ export function createBannerMesh(
272
278
  )
273
279
  group.add(mesh)
274
280
  group.bannerTexture = texture
281
+ group.bannerMaterial = material
275
282
  group.position.set(position.x + 0.5, position.y + heightOffset, position.z + 0.5)
276
283
  return group
277
284
  }